app.rs
1 use crate::effects; 2 use rand::{ 3 SeedableRng, 4 distr::{Distribution, Uniform}, 5 rngs::SmallRng, 6 }; 7 use ratatui::widgets::ListState; 8 use tachyonfx::{Duration, EffectManager}; 9 10 #[cfg(not(target_arch = "wasm32"))] 11 use std::time::Instant; 12 #[cfg(target_arch = "wasm32")] 13 use web_time::Instant; 14 15 const TASKS: [&str; 24] = [ 16 "Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10", 17 "Item11", "Item12", "Item13", "Item14", "Item15", "Item16", "Item17", "Item18", "Item19", 18 "Item20", "Item21", "Item22", "Item23", "Item24", 19 ]; 20 21 const LOGS: [(&str, &str); 26] = [ 22 ("Event1", "INFO"), 23 ("Event2", "INFO"), 24 ("Event3", "CRITICAL"), 25 ("Event4", "ERROR"), 26 ("Event5", "INFO"), 27 ("Event6", "INFO"), 28 ("Event7", "WARNING"), 29 ("Event8", "INFO"), 30 ("Event9", "INFO"), 31 ("Event10", "INFO"), 32 ("Event11", "CRITICAL"), 33 ("Event12", "INFO"), 34 ("Event13", "INFO"), 35 ("Event14", "INFO"), 36 ("Event15", "INFO"), 37 ("Event16", "INFO"), 38 ("Event17", "ERROR"), 39 ("Event18", "ERROR"), 40 ("Event19", "INFO"), 41 ("Event20", "INFO"), 42 ("Event21", "WARNING"), 43 ("Event22", "INFO"), 44 ("Event23", "INFO"), 45 ("Event24", "WARNING"), 46 ("Event25", "INFO"), 47 ("Event26", "INFO"), 48 ]; 49 50 const EVENTS: [(&str, u64); 24] = [ 51 ("B1", 9), 52 ("B2", 12), 53 ("B3", 5), 54 ("B4", 8), 55 ("B5", 2), 56 ("B6", 4), 57 ("B7", 5), 58 ("B8", 9), 59 ("B9", 14), 60 ("B10", 15), 61 ("B11", 1), 62 ("B12", 0), 63 ("B13", 4), 64 ("B14", 6), 65 ("B15", 4), 66 ("B16", 6), 67 ("B17", 4), 68 ("B18", 7), 69 ("B19", 13), 70 ("B20", 8), 71 ("B21", 11), 72 ("B22", 9), 73 ("B23", 3), 74 ("B24", 5), 75 ]; 76 77 #[derive(Clone)] 78 pub struct RandomSignal { 79 distribution: Uniform<u64>, 80 rng: SmallRng, 81 } 82 83 impl RandomSignal { 84 pub fn new(lower: u64, upper: u64) -> Self { 85 Self { 86 distribution: Uniform::new(lower, upper).unwrap(), 87 rng: SmallRng::seed_from_u64(0), 88 } 89 } 90 } 91 92 impl Iterator for RandomSignal { 93 type Item = u64; 94 fn next(&mut self) -> Option<u64> { 95 Some(self.distribution.sample(&mut self.rng)) 96 } 97 } 98 99 #[derive(Clone)] 100 pub struct SinSignal { 101 x: f64, 102 interval: f64, 103 period: f64, 104 scale: f64, 105 } 106 107 impl SinSignal { 108 pub const fn new(interval: f64, period: f64, scale: f64) -> Self { 109 Self { 110 x: 0.0, 111 interval, 112 period, 113 scale, 114 } 115 } 116 } 117 118 impl Iterator for SinSignal { 119 type Item = (f64, f64); 120 fn next(&mut self) -> Option<Self::Item> { 121 let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale); 122 self.x += self.interval; 123 Some(point) 124 } 125 } 126 127 pub struct TabsState<'a> { 128 pub titles: Vec<&'a str>, 129 pub index: usize, 130 } 131 132 impl<'a> TabsState<'a> { 133 pub const fn new(titles: Vec<&'a str>) -> Self { 134 Self { titles, index: 0 } 135 } 136 pub fn next(&mut self) { 137 self.index = (self.index + 1) % self.titles.len(); 138 } 139 140 pub fn previous(&mut self) { 141 if self.index > 0 { 142 self.index -= 1; 143 } else { 144 self.index = self.titles.len() - 1; 145 } 146 } 147 } 148 149 pub struct StatefulList<T> { 150 pub state: ListState, 151 pub items: Vec<T>, 152 } 153 154 impl<T> StatefulList<T> { 155 pub fn with_items(items: Vec<T>) -> Self { 156 Self { 157 state: ListState::default(), 158 items, 159 } 160 } 161 162 pub fn next(&mut self) { 163 let i = match self.state.selected() { 164 Some(i) => { 165 if i >= self.items.len() - 1 { 166 0 167 } else { 168 i + 1 169 } 170 } 171 None => 0, 172 }; 173 self.state.select(Some(i)); 174 } 175 176 pub fn previous(&mut self) { 177 let i = match self.state.selected() { 178 Some(i) => { 179 if i == 0 { 180 self.items.len() - 1 181 } else { 182 i - 1 183 } 184 } 185 None => 0, 186 }; 187 self.state.select(Some(i)); 188 } 189 } 190 191 pub struct Signal<S: Iterator> { 192 source: S, 193 pub points: Vec<S::Item>, 194 tick_rate: usize, 195 } 196 197 impl<S> Signal<S> 198 where 199 S: Iterator, 200 { 201 fn on_tick(&mut self) { 202 self.points.drain(0..self.tick_rate); 203 self.points 204 .extend(self.source.by_ref().take(self.tick_rate)); 205 } 206 } 207 208 pub struct Signals { 209 pub sin1: Signal<SinSignal>, 210 pub sin2: Signal<SinSignal>, 211 pub window: [f64; 2], 212 } 213 214 impl Signals { 215 fn on_tick(&mut self) { 216 self.sin1.on_tick(); 217 self.sin2.on_tick(); 218 self.window[0] += 1.0; 219 self.window[1] += 1.0; 220 } 221 } 222 223 pub struct Server<'a> { 224 pub user: &'a str, 225 pub hostname: &'a str, 226 pub chassis: &'a str, 227 pub os: &'a str, 228 pub kernel: &'a str, 229 pub display: &'a str, 230 pub desktop: &'a str, 231 pub cpu: &'a str, 232 pub gpu: &'a str, 233 pub memory: &'a str, 234 pub disk: &'a str, 235 pub uptime: &'a str, 236 pub terminal: &'a str, 237 pub location: &'a str, 238 pub coords: (f64, f64), 239 pub status: &'a str, 240 } 241 242 pub struct App<'a> { 243 pub title: &'a str, 244 pub should_quit: bool, 245 pub tabs: TabsState<'a>, 246 pub show_chart: bool, 247 pub progress: f64, 248 pub sparkline: Signal<RandomSignal>, 249 pub tasks: StatefulList<&'a str>, 250 pub logs: StatefulList<(&'a str, &'a str)>, 251 pub signals: Signals, 252 pub barchart: Vec<(&'a str, u64)>, 253 pub servers: Vec<Server<'a>>, 254 pub enhanced_graphics: bool, 255 pub effects: EffectManager<EffectKey>, 256 pub last_frame: Instant, 257 } 258 259 #[derive(Clone, Copy, Debug, Default, Ord, PartialOrd, Eq, PartialEq)] 260 pub enum EffectKey { 261 #[default] 262 ChangeTab, 263 } 264 265 impl<'a> App<'a> { 266 pub fn new(title: &'a str, enhanced_graphics: bool) -> Self { 267 let mut rand_signal = RandomSignal::new(0, 100); 268 let sparkline_points = rand_signal.by_ref().take(300).collect(); 269 let mut sin_signal = SinSignal::new(0.2, 3.0, 18.0); 270 let sin1_points = sin_signal.by_ref().take(100).collect(); 271 let mut sin_signal2 = SinSignal::new(0.1, 2.0, 10.0); 272 let sin2_points = sin_signal2.by_ref().take(200).collect(); 273 274 let mut effects = EffectManager::default(); 275 effects.add_effect(effects::startup()); 276 effects.add_effect(effects::pulsate_selected_tab()); 277 App { 278 title, 279 should_quit: false, 280 tabs: TabsState::new(vec!["👋 ~/.config", "🧮 /etc/infra", "~/workspace"]), 281 show_chart: true, 282 progress: 0.0, 283 sparkline: Signal { 284 source: rand_signal, 285 points: sparkline_points, 286 tick_rate: 1, 287 }, 288 tasks: StatefulList::with_items(TASKS.to_vec()), 289 logs: StatefulList::with_items(LOGS.to_vec()), 290 signals: Signals { 291 sin1: Signal { 292 source: sin_signal, 293 points: sin1_points, 294 tick_rate: 5, 295 }, 296 sin2: Signal { 297 source: sin_signal2, 298 points: sin2_points, 299 tick_rate: 10, 300 }, 301 window: [0.0, 20.0], 302 }, 303 barchart: EVENTS.to_vec(), 304 servers: vec![ 305 // Surface Pro 7 — already provided 306 Server { 307 user: "mfarabi", 308 hostname: "guix", 309 chassis: "Microsoft Surface Pro 7", 310 os: "GNU GUIX", 311 kernel: "Linux Libre", 312 display: "1366x768 @ 60Hz in 13\"", 313 desktop: "EXWM", 314 cpu: "Intel Core i5- @ GHz", 315 gpu: "Integrated (TBD)", 316 memory: "TBD", 317 disk: "TBD", 318 uptime: "TBD", 319 terminal: "TBD", 320 location: "Ottawa", 321 coords: (45.42, -75.00), 322 status: "Idle", 323 }, 324 // ... other Surface entries ... 325 Server { 326 user: "mfarabi", 327 hostname: "freebsd", 328 chassis: "HP EliteBook 820 G2", 329 os: "FreeBSD 14.3-RELEASE", 330 kernel: "FreeBSD 14.3-RELEASE", 331 display: "1366x768 @ 60Hz in 13\"", 332 desktop: "Hyprland", 333 cpu: "Intel Core i5-5300U(4) @ 2.29 GHz", 334 gpu: "Intel Device 1616", 335 memory: "16 GB", 336 disk: "0.5 TB", 337 uptime: "TBD", 338 terminal: "zsh + kitty", 339 location: "TBD", 340 coords: (0.0, 0.0), 341 status: "Up", 342 }, 343 Server { 344 user: "mfarabi", 345 hostname: "mfarabi", 346 chassis: "MacBook Air M1 2020", 347 os: "macOS Sequoia", 348 kernel: "Darwin 24.5.0", 349 display: "2880x1800 @ 60Hz in 13\"", 350 desktop: "Quartz", 351 cpu: "Apple M1(8) @ 3.20 GHz", 352 gpu: "Apple M1(7)", 353 memory: "8 GB", 354 disk: "0.526 TB", 355 uptime: "TBD", 356 terminal: "zsh + kitty", 357 location: "TBD", 358 coords: (0.0, 0.0), 359 status: "Up", 360 }, 361 Server { 362 user: "mfarabi", 363 hostname: "ubuntu", 364 chassis: "ASUS", 365 os: "FreeBSD 14.3-RELEASE", 366 kernel: "FreeBSD 14.3-RELEASE", 367 display: "1366x768 @ 60Hz in 13\"", 368 desktop: "Hyprland", 369 cpu: "Intel Core i5-5300U(4) @ 2.29 GHz", 370 gpu: "Intel Device 1616", 371 memory: "16 GB", 372 disk: "0.5 TB", 373 uptime: "TBD", 374 terminal: "zsh + kitty", 375 location: "TBD", 376 coords: (0.0, 0.0), 377 status: "Up", 378 }, 379 Server { 380 user: "mfarabi", 381 hostname: "ubuntu", 382 chassis: "MSI GS65", 383 os: "Ubuntu 24.04", 384 kernel: "linux-6.8", 385 display: "TBD", 386 desktop: "N/A", 387 cpu: "TBD", 388 gpu: "TBD", 389 memory: "TBD", 390 disk: "TBD", 391 uptime: "TBD", 392 terminal: "zsh + kitty", 393 location: "TBD", 394 coords: (0.0, 0.0), 395 status: "Up", 396 }, 397 Server { 398 user: "mfarabi", 399 hostname: "ubuntu", 400 chassis: "MSI GS76", 401 os: "Ubuntu 24.04", 402 kernel: "linux-6.8", 403 display: "TBD", 404 desktop: "N/A", 405 cpu: "TBD", 406 gpu: "TBD", 407 memory: "TBD", 408 disk: "TBD", 409 uptime: "TBD", 410 terminal: "zsh + kitty", 411 location: "TBD", 412 coords: (0.0, 0.0), 413 status: "Up", 414 }, 415 Server { 416 user: "mfarabi", 417 hostname: "archlinux", 418 chassis: "Framework 16", 419 os: "Arch Linux", 420 kernel: "linux-6.15.2", 421 display: "2560x1600 @ 165Hz in 16\"", 422 desktop: "Hyprland", 423 cpu: "AMD Ryzen 9 7940HS @ 5.26 GHz", 424 gpu: "AMD Radeon RX 7700S & AMD Radeon 780M", 425 memory: "64 GB", 426 disk: "4 TB", 427 uptime: "41 mins", 428 terminal: "zsh + kitty", 429 location: "TBD", 430 coords: (0.0, 0.0), 431 status: "Up", 432 }, 433 Server { 434 user: "TBD", 435 hostname: "TBD", 436 chassis: "TBD", 437 os: "NixOS", 438 kernel: "linux-6.15.2", 439 display: "N/A", 440 desktop: "N/A", 441 cpu: "TBD", 442 gpu: "TBD", 443 memory: "TBD", 444 disk: "TBD", 445 uptime: "TBD", 446 terminal: "TBD", 447 location: "TBD", 448 coords: (0.0, 0.0), 449 status: "Up", 450 }, 451 Server { 452 user: "TBD", 453 hostname: "TBD", 454 chassis: "TBD", 455 os: "Windows + NixOS WSL", 456 kernel: "N/A", 457 display: "N/A", 458 desktop: "N/A", 459 cpu: "TBD", 460 gpu: "TBD", 461 memory: "TBD", 462 disk: "TBD", 463 uptime: "TBD", 464 terminal: "TBD", 465 location: "TBD", 466 coords: (0.0, 0.0), 467 status: "Down", 468 }, 469 Server { 470 user: "mfarabi", 471 hostname: "stm32", 472 chassis: "STM32F3DISCOVERY", 473 os: "N/A", 474 kernel: "N/A", 475 display: "N/A", 476 desktop: "N/A", 477 cpu: "TBD", 478 gpu: "N/A", 479 memory: "N/A", 480 disk: "N/A", 481 uptime: "N/A", 482 terminal: "N/A", 483 location: "TBD", 484 coords: (0.0, 0.0), 485 status: "Idle", 486 }, 487 Server { 488 user: "mfarabi", 489 hostname: "esp32", 490 chassis: "Espressif ESP32", 491 os: "N/A", 492 kernel: "N/A", 493 display: "N/A", 494 desktop: "N/A", 495 cpu: "TBD", 496 gpu: "N/A", 497 memory: "N/A", 498 disk: "N/A", 499 uptime: "N/A", 500 terminal: "N/A", 501 location: "TBD", 502 coords: (0.0, 0.0), 503 status: "Idle", 504 }, 505 Server { 506 user: "mfarabi", 507 hostname: "arduino-uno", 508 chassis: "Arduino Uno", 509 os: "N/A", 510 kernel: "N/A", 511 display: "N/A", 512 desktop: "N/A", 513 cpu: "TBD", 514 gpu: "N/A", 515 memory: "N/A", 516 disk: "N/A", 517 uptime: "N/A", 518 terminal: "N/A", 519 location: "TBD", 520 coords: (0.0, 0.0), 521 status: "Idle", 522 }, 523 Server { 524 user: "mfarabi", 525 hostname: "arduino-mega", 526 chassis: "Arduino Mega", 527 os: "N/A", 528 kernel: "N/A", 529 display: "N/A", 530 desktop: "N/A", 531 cpu: "TBD", 532 gpu: "N/A", 533 memory: "N/A", 534 disk: "N/A", 535 uptime: "N/A", 536 terminal: "N/A", 537 location: "TBD", 538 coords: (0.0, 0.0), 539 status: "Idle", 540 }, 541 Server { 542 user: "mfarabi", 543 hostname: "rpi", 544 chassis: "Raspberry Pi B", 545 os: "TBD", 546 kernel: "TBD", 547 display: "N/A", 548 desktop: "N/A", 549 cpu: "TBD", 550 gpu: "TBD", 551 memory: "TBD", 552 disk: "TBD", 553 uptime: "TBD", 554 terminal: "TBD", 555 location: "TBD", 556 coords: (0.0, 0.0), 557 status: "Down", 558 }, 559 Server { 560 user: "mfarabi", 561 hostname: "dlink", 562 chassis: "D-LINK DIR 1750", 563 os: "TBD", 564 kernel: "TBD", 565 display: "N/A", 566 desktop: "N/A", 567 cpu: "TBD", 568 gpu: "N/A", 569 memory: "TBD", 570 disk: "TBD", 571 uptime: "TBD", 572 terminal: "N/A", 573 location: "TBD", 574 coords: (0.0, 0.0), 575 status: "Down", 576 }, 577 ], 578 enhanced_graphics, 579 effects, 580 last_frame: Instant::now(), 581 } 582 } 583 584 pub fn on_up(&mut self) { 585 self.tasks.previous(); 586 } 587 588 pub fn on_down(&mut self) { 589 self.tasks.next(); 590 } 591 592 pub fn on_right(&mut self) { 593 self.tabs.next(); 594 self.add_transition_tab_effect(); 595 } 596 597 pub fn on_left(&mut self) { 598 self.tabs.previous(); 599 self.add_transition_tab_effect(); 600 } 601 602 pub fn on_key(&mut self, c: char) { 603 match c { 604 'q' => { 605 self.should_quit = true; 606 } 607 't' => { 608 self.show_chart = !self.show_chart; 609 } 610 _ => {} 611 } 612 } 613 614 pub fn on_tick(&mut self) -> Duration { 615 // Update progress 616 self.progress += 0.001; 617 if self.progress > 1.0 { 618 self.progress = 0.0; 619 } 620 621 self.sparkline.on_tick(); 622 self.signals.on_tick(); 623 624 let log = self.logs.items.pop().unwrap(); 625 self.logs.items.insert(0, log); 626 627 let event = self.barchart.pop().unwrap(); 628 self.barchart.insert(0, event); 629 630 // calculate elapsed time since last frame 631 let now = Instant::now(); 632 let elapsed = now.duration_since(self.last_frame).as_millis() as u32; 633 self.last_frame = now; 634 635 Duration::from_millis(elapsed) 636 } 637 638 fn add_transition_tab_effect(&mut self) { 639 let effect = effects::change_tab(); 640 self.effects.add_unique_effect(EffectKey::ChangeTab, effect); 641 } 642 }