/ tui / src / app.rs
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  }