use std::path::PathBuf; use crossterm::event::KeyEvent; use ratatui::prelude::Rect; use tokio::sync::mpsc; use tracing::{debug, info}; use crate::{ action::Action, buffer::{command::Command, keymap::Keymap}, components::{Component, buffer::BufferComponent}, config::{Config, Mode}, tui::{Event, Tui}, }; pub struct App { config: Config, tick_rate: f64, frame_rate: f64, components: Vec>, should_quit: bool, should_suspend: bool, /// MJB-LLR-203: resolves only the `Global` scope. Every other mode belongs /// to the buffer — see MJB-DR-004 for why ownership is split. global_keymap: Keymap, action_tx: mpsc::UnboundedSender, action_rx: mpsc::UnboundedReceiver, } impl App { pub fn new(tick_rate: f64, frame_rate: f64, file: Option) -> color_eyre::Result { let (action_tx, action_rx) = mpsc::unbounded_channel(); let config = Config::new()?; let global_keymap = Keymap::new(&config.keybindings); // MJB-LLR-204: the buffer is the only component. The template's // FpsCounter and Home widgets are gone, not merely unregistered. let buffer = BufferComponent::new(config.clone(), file)?; Ok(Self { tick_rate, frame_rate, components: vec![Box::new(buffer)], should_quit: false, should_suspend: false, config, global_keymap, action_tx, action_rx, }) } pub async fn run(&mut self) -> color_eyre::Result<()> { let mut tui = Tui::new()? .tick_rate(self.tick_rate) .frame_rate(self.frame_rate); tui.enter()?; for component in self.components.iter_mut() { component.register_action_handler(self.action_tx.clone())?; } for component in self.components.iter_mut() { component.register_config_handler(self.config.clone())?; } for component in self.components.iter_mut() { component.init(tui.size()?)?; } let action_tx = self.action_tx.clone(); loop { self.handle_events(&mut tui).await?; self.handle_actions(&mut tui)?; if self.should_suspend { tui.suspend()?; action_tx.send(Action::Resume)?; action_tx.send(Action::ClearScreen)?; tui.enter()?; } else if self.should_quit { tui.stop()?; break; } } tui.exit()?; Ok(()) } async fn handle_events(&mut self, tui: &mut Tui) -> color_eyre::Result<()> { let Some(event) = tui.next_event().await else { return Ok(()); }; let action_tx = self.action_tx.clone(); match event { Event::Quit => action_tx.send(Action::Quit)?, Event::Tick => action_tx.send(Action::Tick)?, Event::Render => action_tx.send(Action::Render)?, Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?, // MJB-LLR-203: a global binding consumes the key outright. The // template forwarded every key both here and to every component, // which would fire `Quit` while typing in insert mode. Event::Key(key) if self.handle_global_key(key)? => return Ok(()), _ => {} } for component in self.components.iter_mut() { if let Some(action) = component.handle_events(Some(event.clone()))? { action_tx.send(action)?; } } Ok(()) } /// Returns whether the key was consumed by the global keymap. fn handle_global_key(&mut self, key: KeyEvent) -> color_eyre::Result { let Some(command) = self.global_keymap.lookup_single(Mode::Global, key) else { return Ok(false); }; let action = match command { Command::Quit => Action::Quit, Command::Suspend => Action::Suspend, // Anything else bound globally is not an application concern; let // the buffer handle it in its own mode. _ => return Ok(false), }; info!("Global binding matched: {command}"); self.action_tx.send(action)?; Ok(true) } fn handle_actions(&mut self, tui: &mut Tui) -> color_eyre::Result<()> { while let Ok(action) = self.action_rx.try_recv() { if action != Action::Tick && action != Action::Render { debug!("{action:?}"); } match action { // MJB-LLR-205: `Tick` no longer drains a pending-key buffer. // Chord resolution is time-independent (MJB-LLR-154). Action::Quit => self.should_quit = true, Action::Suspend => self.should_suspend = true, Action::Resume => self.should_suspend = false, Action::ClearScreen => tui.terminal.clear()?, Action::Resize(w, h) => self.handle_resize(tui, w, h)?, Action::Render => self.render(tui)?, Action::Error(ref err) => tracing::error!(?err), _ => {} } for component in self.components.iter_mut() { if let Some(action) = component.update(action.clone())? { self.action_tx.send(action)? }; } } Ok(()) } fn handle_resize(&mut self, tui: &mut Tui, w: u16, h: u16) -> color_eyre::Result<()> { tui.resize(Rect::new(0, 0, w, h))?; self.render(tui)?; Ok(()) } fn render(&mut self, tui: &mut Tui) -> color_eyre::Result<()> { tui.draw(|frame| { for component in self.components.iter_mut() { if let Err(err) = component.draw(frame, frame.area()) { let _ = self .action_tx .send(Action::Error(format!("Failed to draw: {:?}", err))); } } })?; Ok(()) } }