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::view::Tab::Accounts);
121 }
122
123 #[test]
124 fn run_loop_routes_cmdline_submit() {
125 use crate::view::Tab;
126 let mut app = fresh_app();
127 let events = vec![
128 key(KeyCode::Char(':')),
129 key(KeyCode::Char('v')),
130 key(KeyCode::Char('e')),
131 key(KeyCode::Char('r')),
132 key(KeyCode::Char('s')),
133 key(KeyCode::Char('i')),
134 key(KeyCode::Char('o')),
135 key(KeyCode::Char('n')),
136 key(KeyCode::Enter),
137 key(KeyCode::Char('q')),
138 ];
139 let mut transport = ScriptedTransport::new(60, 10, events);
140 run_loop(&mut transport, &mut app).unwrap();
141 assert!(!app.cmdline.active);
142 assert_eq!(app.active_tab, Tab::Console);
144 assert!(
145 app.console
146 .scrollback
147 .iter()
148 .any(|l| l.contains("(get-version)")),
149 "form should appear in scrollback"
150 );
151 }
152
153 #[test]
154 fn handle_event_tolerates_resize() {
155 let mut app = fresh_app();
156 handle_event(&mut app, RawEvent::Resize(80, 24));
157 assert!(!app.should_quit);
158 assert_eq!(app.active_tab, crate::view::Tab::Reports);
159 }
160
161 #[test]
162 fn run_loop_exits_without_polling_when_app_already_quitting() {
163 let mut app = fresh_app();
164 app.request_quit();
165 let mut transport = DisconnectedTransport::new(40, 10);
169 run_loop(&mut transport, &mut app).expect("should return Ok");
170 }
171
172 #[test]
173 fn run_loop_propagates_poll_errors() {
174 let mut app = fresh_app();
175 let mut transport = DisconnectedTransport::new(40, 10);
176 let err = run_loop(&mut transport, &mut app).expect_err("poll err bubbles");
177 assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof);
178 assert!(!app.should_quit);
181 }
182
183 #[test]
184 fn run_loop_ignores_unbound_keys() {
185 let mut app = fresh_app();
187 let mut transport =
188 ScriptedTransport::new(40, 10, vec![key(KeyCode::F(5)), key(KeyCode::Char('q'))]);
189 run_loop(&mut transport, &mut app).unwrap();
190 assert!(app.should_quit);
191 }
192}