1#![allow(clippy::print_stdout, clippy::print_stderr)]
4
5mod format;
6mod ui;
7
8use std::io;
9use std::sync::mpsc;
10use std::thread;
11use std::time::{Duration, Instant};
12
13use anyhow::Result;
14use crossterm::{
15 event::{
16 self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind,
17 MouseEventKind,
18 },
19 execute,
20 terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
21};
22use ratatui::{Terminal, backend::CrosstermBackend, layout::Rect, widgets::TableState};
23
24use mi6_core::{GlobalStats, SessionInfo, SessionMonitor};
25
26const REFRESH_INTERVALS_MS: &[u64] = &[100, 200, 300, 400, 500, 1000, 2000, 3000, 4000, 5000, 10000];
28
29const DEFAULT_REFRESH_IDX: usize = 2;
31
32struct RefreshData {
34 sessions: Vec<SessionInfo>,
35 global_stats: GlobalStats,
36 error: Option<String>,
37}
38
39pub struct App {
41 pub sessions: Vec<SessionInfo>,
42 pub global_stats: GlobalStats,
43 pub table_state: TableState,
44 pub error_message: Option<String>,
45 refresh_interval_idx: usize,
46 pub show_dead: bool,
47 data_rx: mpsc::Receiver<RefreshData>,
48 interval_tx: mpsc::Sender<Duration>,
49 pub(crate) table_area: Rect,
51}
52
53impl App {
54 fn new() -> Self {
55 let (data_tx, data_rx) = mpsc::channel();
56 let (interval_tx, interval_rx) = mpsc::channel();
57
58 let initial_interval = Duration::from_millis(REFRESH_INTERVALS_MS[DEFAULT_REFRESH_IDX]);
60 thread::spawn(move || {
61 refresh_thread(data_tx, interval_rx, initial_interval);
62 });
63
64 Self {
66 sessions: Vec::new(),
67 global_stats: GlobalStats::default(),
68 table_state: TableState::default(),
69 error_message: None,
70 refresh_interval_idx: DEFAULT_REFRESH_IDX,
71 show_dead: false,
72 data_rx,
73 interval_tx,
74 table_area: Rect::default(),
75 }
76 }
77
78 fn poll_data(&mut self) -> bool {
81 let mut got_data = false;
82 while let Ok(data) = self.data_rx.try_recv() {
83 self.sessions = data.sessions;
84 self.global_stats = data.global_stats;
85 self.error_message = data.error;
86 got_data = true;
87 }
88 got_data
89 }
90
91 pub fn visible_sessions(&self) -> Vec<&SessionInfo> {
92 if self.show_dead {
93 self.sessions.iter().collect()
94 } else {
95 let five_min_ago = chrono::Utc::now() - chrono::Duration::minutes(5);
100 self.sessions
101 .iter()
102 .filter(|s| s.is_alive || (s.pid.is_none() && s.last_activity > five_min_ago))
103 .collect()
104 }
105 }
106
107 fn toggle_show_dead(&mut self) {
108 self.show_dead = !self.show_dead;
109 self.table_state.select(None);
111 }
112
113 fn refresh_interval(&self) -> Duration {
114 Duration::from_millis(REFRESH_INTERVALS_MS[self.refresh_interval_idx])
115 }
116
117 pub fn refresh_interval_display(&self) -> String {
118 let ms = REFRESH_INTERVALS_MS[self.refresh_interval_idx];
119 if ms >= 1000 {
120 format!("{}s", ms / 1000)
121 } else {
122 format!("{ms}ms")
123 }
124 }
125
126 fn increase_refresh_interval(&mut self) {
127 if self.refresh_interval_idx < REFRESH_INTERVALS_MS.len() - 1 {
128 self.refresh_interval_idx += 1;
129 let _ = self.interval_tx.send(self.refresh_interval());
130 }
131 }
132
133 fn decrease_refresh_interval(&mut self) {
134 if self.refresh_interval_idx > 0 {
135 self.refresh_interval_idx -= 1;
136 let _ = self.interval_tx.send(self.refresh_interval());
137 }
138 }
139
140 fn next_row(&mut self) {
141 let count = self.visible_sessions().len();
142 if count == 0 {
143 return;
144 }
145 let i = match self.table_state.selected() {
146 Some(i) => (i + 1).min(count - 1),
147 None => 0,
148 };
149 self.table_state.select(Some(i));
150 }
151
152 fn previous_row(&mut self) {
153 let count = self.visible_sessions().len();
154 if count == 0 {
155 return;
156 }
157 let i = match self.table_state.selected() {
158 Some(i) => i.saturating_sub(1),
159 None => 0,
160 };
161 self.table_state.select(Some(i));
162 }
163
164 fn handle_click(&mut self, x: u16, y: u16) {
166 if x < self.table_area.x
168 || x >= self.table_area.x + self.table_area.width
169 || y < self.table_area.y
170 || y >= self.table_area.y + self.table_area.height
171 {
172 return;
173 }
174
175 let y_in_table = y.saturating_sub(self.table_area.y);
177 if y_in_table < 2 {
178 return;
180 }
181
182 let visual_row = (y_in_table - 2) as usize;
184 let row_index = self.table_state.offset() + visual_row;
185 let count = self.visible_sessions().len();
186 if row_index < count {
187 self.table_state.select(Some(row_index));
188 }
189 }
190}
191
192fn refresh_thread(
194 data_tx: mpsc::Sender<RefreshData>,
195 interval_rx: mpsc::Receiver<Duration>,
196 initial_interval: Duration,
197) {
198 let mut monitor = match SessionMonitor::new_fast() {
200 Ok(m) => m,
201 Err(e) => {
202 let _ = data_tx.send(RefreshData {
203 sessions: Vec::new(),
204 global_stats: GlobalStats::default(),
205 error: Some(format!("Failed to open fyi database: {e}")),
206 });
207 return;
208 }
209 };
210
211 if let Ok(sessions) = monitor.get_sessions_fast() {
213 let global_stats = monitor.get_global_stats().unwrap_or_default();
214 let _ = data_tx.send(RefreshData {
215 sessions,
216 global_stats,
217 error: None,
218 });
219 }
220
221 monitor.refresh();
223
224 let mut interval = initial_interval;
225
226 loop {
227 let (sessions, error) = match monitor.get_sessions() {
228 Ok(s) => (s, None),
229 Err(e) => (Vec::new(), Some(format!("Error: {e}"))),
230 };
231
232 let global_stats = monitor.get_global_stats().unwrap_or_default();
233
234 if data_tx
236 .send(RefreshData {
237 sessions,
238 global_stats,
239 error,
240 })
241 .is_err()
242 {
243 return;
244 }
245
246 let sleep_start = Instant::now();
248 while sleep_start.elapsed() < interval {
249 if let Ok(new_interval) = interval_rx.try_recv() {
250 interval = new_interval;
251 }
252 thread::sleep(Duration::from_millis(50));
253 }
254
255 monitor.refresh();
257 }
258}
259
260fn run_app<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, mut app: App) -> Result<()> {
261 let mut needs_redraw = true;
262
263 loop {
264 if app.poll_data() {
266 needs_redraw = true;
267 }
268
269 if event::poll(Duration::from_millis(16))? {
271 match event::read()? {
272 Event::Key(key) => {
273 if key.kind == KeyEventKind::Press {
274 needs_redraw = true;
275 match key.code {
276 KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
277 KeyCode::Down | KeyCode::Char('j') => app.next_row(),
278 KeyCode::Up | KeyCode::Char('k') => app.previous_row(),
279 KeyCode::Char('a') => app.toggle_show_dead(),
280 KeyCode::Char('+') | KeyCode::Char('=') => app.increase_refresh_interval(),
281 KeyCode::Char('-') => app.decrease_refresh_interval(),
282 _ => {}
283 }
284 }
285 }
286 Event::Mouse(mouse) => {
287 match mouse.kind {
288 MouseEventKind::Down(event::MouseButton::Left) => {
289 app.handle_click(mouse.column, mouse.row);
290 needs_redraw = true;
291 }
292 MouseEventKind::ScrollDown => {
293 app.next_row();
294 needs_redraw = true;
295 }
296 MouseEventKind::ScrollUp => {
297 app.previous_row();
298 needs_redraw = true;
299 }
300 _ => {}
301 }
302 }
303 Event::Resize(_, _) => {
304 needs_redraw = true;
305 }
306 _ => {}
307 }
308 }
309
310 if needs_redraw {
312 terminal.draw(|f| ui::render(f, &mut app))?;
313 needs_redraw = false;
314 }
315 }
316}
317
318pub fn run() -> Result<()> {
320 enable_raw_mode()?;
322 let mut stdout = io::stdout();
323 execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
324 let backend = CrosstermBackend::new(stdout);
325 let mut terminal = Terminal::new(backend)?;
326
327 let app = App::new();
329 let result = run_app(&mut terminal, app);
330
331 disable_raw_mode()?;
333 execute!(
334 terminal.backend_mut(),
335 LeaveAlternateScreen,
336 DisableMouseCapture
337 )?;
338 terminal.show_cursor()?;
339
340 if let Err(e) = result {
341 eprintln!("Error: {e}");
342 }
343
344 Ok(())
345}