1use crate::app::App;
9use crate::chart::{Backend as ChartBackend, detect_for};
10use crate::draw::draw;
11use crate::event::apply;
12use crate::keymap;
13use crate::transport::{RawEvent, Transport};
14use std::io;
15use std::time::Duration;
16
17pub const POLL_INTERVAL: Duration = Duration::from_millis(200);
20
21pub fn run_loop<T>(transport: &mut T, app: &mut App) -> io::Result<()>
32where
33 T: Transport,
34 <T::Backend as ratatui::backend::Backend>::Error: std::error::Error + Send + Sync + 'static,
35{
36 while !app.should_quit {
37 transport
38 .terminal_mut()
39 .draw(|frame| draw(frame, app))
40 .map_err(io::Error::other)?;
41 emit_pending_chart(transport, app)?;
42 app.drain_console();
43 if let Some(event) = transport.poll(POLL_INTERVAL)? {
44 handle_event(app, event);
45 }
46 }
47 Ok(())
48}
49
50fn emit_pending_chart<T>(transport: &mut T, app: &mut App) -> io::Result<()>
56where
57 T: Transport,
58{
59 let Some(spec) = app.take_pending_chart() else {
60 return Ok(());
61 };
62 if !matches!(detect_for(transport), ChartBackend::Kitty) {
63 return Ok(());
64 }
65 let bytes = plotting::kitty::render_kitty(&spec, plotting::kitty::KittyOpts::default());
66 transport.write_passthrough(bytes.as_bytes())
67}
68
69fn handle_event(app: &mut App, event: RawEvent) {
70 match event {
71 RawEvent::Key(key) => {
72 if let Some(intent) = keymap::translate(app, key) {
73 apply(app, intent);
74 }
75 }
76 RawEvent::Resize(_, _) => {
77 }
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use crate::transport::RawEvent;
90 use crate::transport::tests_support::{DisconnectedTransport, ScriptedTransport};
91 use crate::widgets::EditMode;
92 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
93 use sqlx::types::Uuid;
94
95 fn key(code: KeyCode) -> RawEvent {
96 RawEvent::Key(KeyEvent::new(code, KeyModifiers::NONE))
97 }
98
99 fn fresh_app() -> App {
100 App::new(Uuid::new_v4(), EditMode::Emacs)
101 }
102
103 #[test]
104 fn run_loop_exits_on_quit_intent() {
105 let mut app = fresh_app();
106 let mut transport = ScriptedTransport::new(40, 10, vec![key(KeyCode::Char('q'))]);
107 run_loop(&mut transport, &mut app).unwrap();
108 assert!(app.should_quit);
109 }
110
111 #[test]
112 fn run_loop_applies_tab_selection() {
113 let mut app = fresh_app();
114 let mut transport = ScriptedTransport::new(
115 40,
116 10,
117 vec![key(KeyCode::Char('1')), key(KeyCode::Char('q'))],
118 );
119 run_loop(&mut transport, &mut app).unwrap();
120 assert_eq!(app.active_tab, crate::app::Tab::Accounts);
121 }
122
123 #[test]
124 fn run_loop_routes_cmdline_submit() {
125 let mut app = fresh_app();
126 let events = vec![
127 key(KeyCode::Char(':')),
128 key(KeyCode::Char('v')),
129 key(KeyCode::Char('e')),
130 key(KeyCode::Char('r')),
131 key(KeyCode::Char('s')),
132 key(KeyCode::Char('i')),
133 key(KeyCode::Char('o')),
134 key(KeyCode::Char('n')),
135 key(KeyCode::Enter),
136 key(KeyCode::Char('q')),
137 ];
138 let mut transport = ScriptedTransport::new(60, 10, events);
139 run_loop(&mut transport, &mut app).unwrap();
140 assert!(!app.command_line_active);
141 assert!(app.status.contains("version"));
142 }
143
144 #[test]
145 fn handle_event_tolerates_resize() {
146 let mut app = fresh_app();
147 handle_event(&mut app, RawEvent::Resize(80, 24));
148 assert!(!app.should_quit);
149 assert_eq!(app.active_tab, crate::app::Tab::Reports);
150 }
151
152 #[test]
153 fn run_loop_exits_without_polling_when_app_already_quitting() {
154 let mut app = fresh_app();
155 app.request_quit();
156 let mut transport = DisconnectedTransport::new(40, 10);
160 run_loop(&mut transport, &mut app).expect("should return Ok");
161 }
162
163 #[test]
164 fn run_loop_propagates_poll_errors() {
165 let mut app = fresh_app();
166 let mut transport = DisconnectedTransport::new(40, 10);
167 let err = run_loop(&mut transport, &mut app).expect_err("poll err bubbles");
168 assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof);
169 assert!(!app.should_quit);
172 }
173
174 #[test]
175 fn run_loop_ignores_unbound_keys() {
176 let mut app = fresh_app();
178 let mut transport =
179 ScriptedTransport::new(40, 10, vec![key(KeyCode::F(5)), key(KeyCode::Char('q'))]);
180 run_loop(&mut transport, &mut app).unwrap();
181 assert!(app.should_quit);
182 }
183}