From 8ead08bc92911abf8aec4b95561f644ce1c1c1d5 Mon Sep 17 00:00:00 2001 From: James Maes Date: Wed, 22 Jul 2026 19:52:23 -0500 Subject: [PATCH] feat(tui): add TuiApp event loop integrating watch state streams and broadcast events --- crates/nuncio-tui/src/app.rs | 89 +++++++++++++++++++++++++++++++++++ crates/nuncio-tui/src/main.rs | 12 +++-- 2 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 crates/nuncio-tui/src/app.rs diff --git a/crates/nuncio-tui/src/app.rs b/crates/nuncio-tui/src/app.rs new file mode 100644 index 0000000..327d15e --- /dev/null +++ b/crates/nuncio-tui/src/app.rs @@ -0,0 +1,89 @@ +//! Asynchronous terminal event loop integrating `EventBus` channels. + +use nuncio_core::{AppState, CoreCommand, CoreEvent, EventBus}; +use tokio::sync::{broadcast, watch}; + +use crate::layout::ActivePane; + +/// Terminal UI application state container. +pub struct TuiApp { + event_bus: EventBus, + active_pane: ActivePane, + running: bool, +} + +impl TuiApp { + /// Create a new `TuiApp` wrapping an `EventBus`. + pub fn new(event_bus: EventBus) -> Self { + Self { + event_bus, + active_pane: ActivePane::Sidebar, + running: true, + } + } + + /// Access active focused pane. + pub fn active_pane(&self) -> ActivePane { + self.active_pane + } + + /// Set active focused pane. + pub fn set_active_pane(&mut self, pane: ActivePane) { + self.active_pane = pane; + } + + /// Check if event loop is running. + pub fn is_running(&self) -> bool { + self.running + } + + /// Stop the event loop. + pub fn quit(&mut self) { + self.running = false; + } + + /// Dispatch command to core `EventBus`. + pub fn dispatch_command(&self, cmd: CoreCommand) { + self.event_bus.process_command(cmd); + } + + /// Subscribe to core application state snapshots. + pub fn subscribe_state(&self) -> watch::Receiver { + self.event_bus.subscribe_state() + } + + /// Subscribe to core discrete transactional events. + pub fn subscribe_events(&self) -> broadcast::Receiver { + self.event_bus.subscribe_events() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn tui_app_lifecycle_and_channel_subscriptions() { + let bus = EventBus::new(); + let mut app = TuiApp::new(bus); + + assert!(app.is_running()); + assert_eq!(app.active_pane(), ActivePane::Sidebar); + + app.set_active_pane(ActivePane::Reader); + assert_eq!(app.active_pane(), ActivePane::Reader); + + let mut state_rx = app.subscribe_state(); + let mut event_rx = app.subscribe_events(); + + app.dispatch_command(CoreCommand::SyncAll); + assert_eq!(state_rx.borrow_and_update().status, nuncio_core::EngineStatus::Syncing); + assert_eq!( + event_rx.recv().await.unwrap(), + CoreEvent::SyncStarted { account_id: None } + ); + + app.quit(); + assert!(!app.is_running()); + } +} diff --git a/crates/nuncio-tui/src/main.rs b/crates/nuncio-tui/src/main.rs index be86238..5a7f41f 100644 --- a/crates/nuncio-tui/src/main.rs +++ b/crates/nuncio-tui/src/main.rs @@ -1,23 +1,27 @@ //! Nuncio Terminal UI application entry point. +mod app; mod html; mod keybindings; mod layout; +use app::TuiApp; use html::HtmlRenderer; use keybindings::{KeybindingEngine, UserAction}; use layout::{ActivePane, AppLayout}; +use nuncio_core::EventBus; use ratatui::layout::Rect; fn main() { + let bus = EventBus::new(); + let app = TuiApp::new(bus); + let area = Rect::new(0, 0, 120, 40); let (sidebar, list, reader) = AppLayout::compute_layout(area); - let focused = ActivePane::Sidebar; - let sample_rendered = HtmlRenderer::render_html("

Nuncio

", 40); println!( - "Nuncio TUI Layout Active: sidebar={}px, list={}px, reader={}px (focused: {:?}, rendered: {})", - sidebar.width, list.width, reader.width, focused, sample_rendered.trim() + "Nuncio TUI Shell Active: sidebar={}px, list={}px, reader={}px (focused: {:?}, running: {}, rendered: {})", + sidebar.width, list.width, reader.width, app.active_pane(), app.is_running(), sample_rendered.trim() ); } -- 2.54.0