sim_restaurant/tui/
app.rs

1// Copyright (c) 2026 Graphcore Ltd. All rights reserved.
2
3use crossterm::event::{KeyCode, KeyEvent};
4
5use crate::recording::{PlotStat, RecordedSimulation};
6
7const PLAYBACK_STEPS: [usize; 6] = [1, 2, 4, 8, 16, 32];
8const WINDOW_SIZES: [u64; 7] = [30, 60, 120, 300, 600, 1800, 3600];
9
10#[derive(Clone, Copy, Debug)]
11pub struct PlotConfig {
12    pub stat: PlotStat,
13    pub windowed: bool,
14}
15
16#[derive(Debug)]
17pub struct App {
18    pub recording: RecordedSimulation,
19    pub frame_index: usize,
20    pub playing: bool,
21    pub selected_plot: usize,
22    pub plots: [PlotConfig; 4],
23    pub speed_index: usize,
24    pub window_size_index: usize,
25}
26
27impl App {
28    #[must_use]
29    pub fn new(recording: RecordedSimulation) -> Self {
30        Self {
31            recording,
32            frame_index: 0,
33            playing: true,
34            selected_plot: 0,
35            plots: [
36                PlotConfig {
37                    stat: PlotStat::TillQueueLen,
38                    windowed: false,
39                },
40                PlotConfig {
41                    stat: PlotStat::ActiveTillWorkers,
42                    windowed: false,
43                },
44                PlotConfig {
45                    stat: PlotStat::KitchenQueueLen,
46                    windowed: false,
47                },
48                PlotConfig {
49                    stat: PlotStat::ActiveKitchenWorkers,
50                    windowed: false,
51                },
52            ],
53            speed_index: 2,
54            window_size_index: 3,
55        }
56    }
57
58    #[must_use]
59    pub fn current_tick(&self) -> u64 {
60        self.recording
61            .timeline
62            .get(self.frame_index)
63            .map_or(0, |point| point.tick)
64    }
65
66    #[must_use]
67    pub fn current_snapshot(&self) -> &crate::recording::SimulationSnapshot {
68        &self.recording.timeline[self.frame_index].snapshot
69    }
70
71    #[must_use]
72    pub fn speed_label(&self) -> String {
73        format!("{}x", PLAYBACK_STEPS[self.speed_index])
74    }
75
76    #[must_use]
77    pub fn window_size_ticks(&self) -> u64 {
78        WINDOW_SIZES[self.window_size_index]
79    }
80
81    pub fn advance(&mut self) {
82        if self.frame_index + 1 >= self.recording.timeline.len() {
83            self.playing = false;
84            return;
85        }
86
87        let step = PLAYBACK_STEPS[self.speed_index];
88        self.frame_index = (self.frame_index + step).min(self.recording.timeline.len() - 1);
89    }
90
91    pub fn rewind(&mut self) {
92        let step = PLAYBACK_STEPS[self.speed_index];
93        self.frame_index = self.frame_index.saturating_sub(step);
94    }
95
96    #[must_use]
97    pub fn recent_events(&self, count: usize) -> Vec<&crate::recording::TimelineEvent> {
98        let current_tick = self.current_tick();
99        self.recording
100            .events
101            .iter()
102            .filter(|event| event.tick <= current_tick)
103            .rev()
104            .take(count)
105            .collect()
106    }
107
108    pub fn handle_tick(&mut self) {
109        if self.playing {
110            self.advance();
111        }
112    }
113
114    #[must_use]
115    pub fn handle_key(&mut self, key: KeyEvent) -> bool {
116        match key.code {
117            KeyCode::Char('q') | KeyCode::Esc => return true,
118            KeyCode::Char(' ') => {
119                self.playing = !self.playing;
120            }
121            KeyCode::Left | KeyCode::Char('h') => {
122                self.playing = false;
123                self.rewind();
124            }
125            KeyCode::Right | KeyCode::Char('l') => {
126                self.playing = false;
127                self.advance();
128            }
129            KeyCode::Char('g') | KeyCode::Home => {
130                self.playing = false;
131                self.frame_index = 0;
132            }
133            KeyCode::Char('G') | KeyCode::End => {
134                self.playing = false;
135                self.frame_index = self.recording.timeline.len().saturating_sub(1);
136            }
137            KeyCode::Char('[') | KeyCode::Char('-') => {
138                self.speed_index = self.speed_index.saturating_sub(1);
139            }
140            KeyCode::Char(']') | KeyCode::Char('+') => {
141                self.speed_index = (self.speed_index + 1).min(PLAYBACK_STEPS.len() - 1);
142            }
143            KeyCode::Char('{') => {
144                self.window_size_index = self.window_size_index.saturating_sub(1);
145            }
146            KeyCode::Char('}') => {
147                self.window_size_index = (self.window_size_index + 1).min(WINDOW_SIZES.len() - 1);
148            }
149            KeyCode::Char('1') => self.selected_plot = 0,
150            KeyCode::Char('2') => self.selected_plot = 1,
151            KeyCode::Char('3') => self.selected_plot = 2,
152            KeyCode::Char('4') => self.selected_plot = 3,
153            KeyCode::Char('w') => {
154                let plot = &mut self.plots[self.selected_plot];
155                plot.windowed = !plot.windowed;
156            }
157            KeyCode::Up | KeyCode::Char('k') => {
158                self.plots[self.selected_plot].stat =
159                    self.plots[self.selected_plot].stat.previous();
160            }
161            KeyCode::Down | KeyCode::Char('j') => {
162                self.plots[self.selected_plot].stat = self.plots[self.selected_plot].stat.next();
163            }
164            _ => {}
165        }
166        false
167    }
168}