1use std::io;
12use std::time::{Duration, Instant};
13
14use crate::backend::Backend;
15use crate::event::{Event, HitMap, KeyCode, KeyEvent, KeyModifiers};
16use crate::ontology::OntologyRegistry;
17use crate::terminal::Terminal;
18
19pub enum Command<Msg> {
21 None,
23 Quit,
25 Batch(Vec<Command<Msg>>),
27 Message(Msg),
29 SetTickRate(Duration),
31 ExportOntology,
33 AgentAction {
35 agent_id: String,
36 action: String,
37 params: serde_json::Value,
38 },
39 Task(Box<dyn FnOnce() -> Msg + Send>),
42}
43
44impl<Msg: std::fmt::Debug> std::fmt::Debug for Command<Msg> {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 Self::None => write!(f, "None"),
48 Self::Quit => write!(f, "Quit"),
49 Self::Batch(cmds) => f.debug_tuple("Batch").field(cmds).finish(),
50 Self::Message(msg) => f.debug_tuple("Message").field(msg).finish(),
51 Self::SetTickRate(d) => f.debug_tuple("SetTickRate").field(d).finish(),
52 Self::ExportOntology => write!(f, "ExportOntology"),
53 Self::AgentAction {
54 agent_id,
55 action,
56 params,
57 } => f
58 .debug_struct("AgentAction")
59 .field("agent_id", agent_id)
60 .field("action", action)
61 .field("params", params)
62 .finish(),
63 Self::Task(_) => write!(f, "Task(<fn>)"),
64 }
65 }
66}
67
68pub trait Model: Sized {
70 type Msg: Send + 'static;
72
73 fn update(&mut self, msg: Self::Msg) -> Command<Self::Msg>;
75
76 fn view(&self, frame: &mut crate::terminal::Frame<'_>);
78
79 fn handle_event(&self, event: Event) -> Option<Self::Msg>;
82
83 fn init(&self) -> Command<Self::Msg> {
85 Command::None
86 }
87
88 fn register_ontology(&self, _registry: &mut OntologyRegistry) {}
90}
91
92pub struct ProgramOptions {
94 pub tick_rate: Option<Duration>,
96 pub alternate_screen: bool,
98 pub mouse_capture: bool,
100 pub raw_mode: bool,
102}
103
104impl Default for ProgramOptions {
105 fn default() -> Self {
106 Self {
107 tick_rate: Some(Duration::from_millis(16)), alternate_screen: true,
109 mouse_capture: true,
110 raw_mode: true,
111 }
112 }
113}
114
115pub struct Program<M: Model, B: Backend> {
117 model: M,
118 terminal: Terminal<B>,
119 options: ProgramOptions,
120 hit_map: HitMap,
121 running: bool,
122 ontology: OntologyRegistry,
123}
124
125impl<M: Model, B: Backend> Program<M, B> {
126 pub fn new(model: M, backend: B) -> io::Result<Self> {
128 Ok(Self {
129 model,
130 terminal: Terminal::new(backend)?,
131 options: ProgramOptions::default(),
132 hit_map: HitMap::new(),
133 running: true,
134 ontology: OntologyRegistry::new(),
135 })
136 }
137
138 pub fn with_options(mut self, options: ProgramOptions) -> Self {
140 self.options = options;
141 self
142 }
143
144 pub fn model(&self) -> &M {
146 &self.model
147 }
148
149 pub fn ontology(&self) -> &OntologyRegistry {
151 &self.ontology
152 }
153
154 pub fn run(mut self) -> io::Result<M> {
156 if self.options.alternate_screen {
158 self.terminal.backend_mut().enter_alternate_screen()?;
159 }
160 if self.options.raw_mode {
161 self.terminal.backend_mut().enable_raw_mode()?;
162 }
163 if self.options.mouse_capture {
164 self.terminal.backend_mut().enable_mouse_capture()?;
165 }
166
167 let init_cmd = self.model.init();
169 self.process_command(init_cmd);
170
171 self.model.register_ontology(&mut self.ontology);
173
174 let mut last_tick = Instant::now();
175
176 while self.running {
177 let model = &self.model;
179 self.terminal.draw(|frame| {
180 model.view(frame);
181 })?;
182
183 let timeout = self
185 .options
186 .tick_rate
187 .map(|rate| rate.saturating_sub(last_tick.elapsed()))
188 .unwrap_or(Duration::from_millis(100));
189
190 if crossterm::event::poll(timeout)? {
191 let raw_event = crossterm::event::read()?;
192 let event = convert_crossterm_event(raw_event);
193
194 if let Event::Key(ref k) = event {
198 if k.kind == crate::event::KeyEventKind::Release {
199 continue;
200 }
201 }
202
203 if let Event::Mouse(ref mouse) = event {
205 if mouse.is_click() {
206 let _hit = self.hit_map.hit_test(mouse.column, mouse.row);
207 }
209 }
210
211 if let Some(msg) = self.model.handle_event(event) {
212 let cmd = self.model.update(msg);
213 self.process_command(cmd);
214 }
215 }
216
217 if let Some(tick_rate) = self.options.tick_rate {
219 if last_tick.elapsed() >= tick_rate {
220 if let Some(msg) = self.model.handle_event(Event::Tick) {
221 let cmd = self.model.update(msg);
222 self.process_command(cmd);
223 }
224 last_tick = Instant::now();
225 }
226 }
227 }
228
229 if self.options.mouse_capture {
231 self.terminal.backend_mut().disable_mouse_capture()?;
232 }
233 if self.options.raw_mode {
234 self.terminal.backend_mut().disable_raw_mode()?;
235 }
236 if self.options.alternate_screen {
237 self.terminal.backend_mut().leave_alternate_screen()?;
238 }
239 self.terminal.backend_mut().show_cursor()?;
240
241 Ok(self.model)
242 }
243
244 fn process_command(&mut self, cmd: Command<M::Msg>) {
245 match cmd {
246 Command::None => {}
247 Command::Quit => {
248 self.running = false;
249 }
250 Command::Batch(cmds) => {
251 for c in cmds {
252 self.process_command(c);
253 }
254 }
255 Command::Message(msg) => {
256 let cmd = self.model.update(msg);
257 self.process_command(cmd);
258 }
259 Command::SetTickRate(rate) => {
260 self.options.tick_rate = Some(rate);
261 }
262 Command::ExportOntology => {
263 self.model.register_ontology(&mut self.ontology);
264 }
266 Command::AgentAction {
267 agent_id: _,
268 action: _,
269 params: _,
270 } => {
271 }
274 Command::Task(task) => {
275 let msg = task();
278 let cmd = self.model.update(msg);
279 self.process_command(cmd);
280 }
281 }
282 }
283}
284
285fn convert_crossterm_event(event: crossterm::event::Event) -> Event {
287 match event {
288 crossterm::event::Event::Key(key) => Event::Key(KeyEvent {
289 code: convert_key_code(key.code),
290 modifiers: convert_key_modifiers(key.modifiers),
291 kind: match key.kind {
292 crossterm::event::KeyEventKind::Press => crate::event::KeyEventKind::Press,
293 crossterm::event::KeyEventKind::Release => crate::event::KeyEventKind::Release,
294 crossterm::event::KeyEventKind::Repeat => crate::event::KeyEventKind::Repeat,
295 },
296 }),
297 crossterm::event::Event::Mouse(mouse) => Event::Mouse(crate::event::MouseEvent {
298 kind: match mouse.kind {
299 crossterm::event::MouseEventKind::Down(btn) => {
300 crate::event::MouseEventKind::Down(convert_mouse_button(btn))
301 }
302 crossterm::event::MouseEventKind::Up(btn) => {
303 crate::event::MouseEventKind::Up(convert_mouse_button(btn))
304 }
305 crossterm::event::MouseEventKind::Drag(btn) => {
306 crate::event::MouseEventKind::Drag(convert_mouse_button(btn))
307 }
308 crossterm::event::MouseEventKind::Moved => crate::event::MouseEventKind::Moved,
309 crossterm::event::MouseEventKind::ScrollDown => {
310 crate::event::MouseEventKind::ScrollDown
311 }
312 crossterm::event::MouseEventKind::ScrollUp => {
313 crate::event::MouseEventKind::ScrollUp
314 }
315 crossterm::event::MouseEventKind::ScrollLeft => {
316 crate::event::MouseEventKind::ScrollLeft
317 }
318 crossterm::event::MouseEventKind::ScrollRight => {
319 crate::event::MouseEventKind::ScrollRight
320 }
321 },
322 column: mouse.column,
323 row: mouse.row,
324 modifiers: convert_key_modifiers(mouse.modifiers),
325 }),
326 crossterm::event::Event::Resize(width, height) => Event::Resize(width, height),
327 crossterm::event::Event::FocusGained => Event::FocusGained,
328 crossterm::event::Event::FocusLost => Event::FocusLost,
329 crossterm::event::Event::Paste(text) => Event::Paste(text),
330 }
331}
332
333fn convert_key_code(code: crossterm::event::KeyCode) -> KeyCode {
334 match code {
335 crossterm::event::KeyCode::Backspace => KeyCode::Backspace,
336 crossterm::event::KeyCode::Enter => KeyCode::Enter,
337 crossterm::event::KeyCode::Left => KeyCode::Left,
338 crossterm::event::KeyCode::Right => KeyCode::Right,
339 crossterm::event::KeyCode::Up => KeyCode::Up,
340 crossterm::event::KeyCode::Down => KeyCode::Down,
341 crossterm::event::KeyCode::Home => KeyCode::Home,
342 crossterm::event::KeyCode::End => KeyCode::End,
343 crossterm::event::KeyCode::PageUp => KeyCode::PageUp,
344 crossterm::event::KeyCode::PageDown => KeyCode::PageDown,
345 crossterm::event::KeyCode::Tab => KeyCode::Tab,
346 crossterm::event::KeyCode::BackTab => KeyCode::BackTab,
347 crossterm::event::KeyCode::Delete => KeyCode::Delete,
348 crossterm::event::KeyCode::Insert => KeyCode::Insert,
349 crossterm::event::KeyCode::F(n) => KeyCode::F(n),
350 crossterm::event::KeyCode::Char(c) => KeyCode::Char(c),
351 crossterm::event::KeyCode::Esc => KeyCode::Esc,
352 _ => KeyCode::Null,
353 }
354}
355
356fn convert_key_modifiers(mods: crossterm::event::KeyModifiers) -> KeyModifiers {
357 let mut result = KeyModifiers::NONE;
358 if mods.contains(crossterm::event::KeyModifiers::SHIFT) {
359 result |= KeyModifiers::SHIFT;
360 }
361 if mods.contains(crossterm::event::KeyModifiers::CONTROL) {
362 result |= KeyModifiers::CONTROL;
363 }
364 if mods.contains(crossterm::event::KeyModifiers::ALT) {
365 result |= KeyModifiers::ALT;
366 }
367 if mods.contains(crossterm::event::KeyModifiers::SUPER) {
368 result |= KeyModifiers::SUPER;
369 }
370 result
371}
372
373fn convert_mouse_button(btn: crossterm::event::MouseButton) -> crate::event::MouseButton {
374 match btn {
375 crossterm::event::MouseButton::Left => crate::event::MouseButton::Left,
376 crossterm::event::MouseButton::Right => crate::event::MouseButton::Right,
377 crossterm::event::MouseButton::Middle => crate::event::MouseButton::Middle,
378 }
379}