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