From ad9bb78fc8978128b381f6e5e8eb43eae7b12409 Mon Sep 17 00:00:00 2001 From: filipriec_vm Date: Mon, 12 Jan 2026 00:59:05 +0100 Subject: [PATCH] orchestrator --- examples/simple_usage.rs | 75 ++++++++ src/component/error.rs | 3 +- src/input/bindings.rs | 8 +- src/lib.rs | 6 + src/orchestrator/action_resolver.rs | 21 +++ src/orchestrator/command_handler.rs | 47 +++++ src/orchestrator/core.rs | 231 +++++++++++++++++++++++ src/orchestrator/event_bus.rs | 46 +++++ src/orchestrator/mod.rs | 17 ++ src/orchestrator/mode.rs | 83 +++++++++ src/orchestrator/router.rs | 256 ++++++++++++++++++++++++++ src/orchestrator/state_coordinator.rs | 48 +++++ 12 files changed, 837 insertions(+), 4 deletions(-) create mode 100644 examples/simple_usage.rs create mode 100644 src/orchestrator/action_resolver.rs create mode 100644 src/orchestrator/command_handler.rs create mode 100644 src/orchestrator/core.rs create mode 100644 src/orchestrator/event_bus.rs create mode 100644 src/orchestrator/mod.rs create mode 100644 src/orchestrator/mode.rs create mode 100644 src/orchestrator/router.rs create mode 100644 src/orchestrator/state_coordinator.rs diff --git a/examples/simple_usage.rs b/examples/simple_usage.rs new file mode 100644 index 0000000..2472e20 --- /dev/null +++ b/examples/simple_usage.rs @@ -0,0 +1,75 @@ +extern crate alloc; + +use tui_orchestrator::prelude::*; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum Focus { + Username, + Password, + LoginButton, +} + +impl FocusId for Focus {} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum AppEvent { + LoginAttempt { + username: alloc::string::String, + password: alloc::string::String, + }, + Quit, +} + +struct LoginPage { + username: alloc::string::String, + password: alloc::string::String, +} + +impl Component for LoginPage { + type Focus = Focus; + type Action = ComponentAction; + type Event = AppEvent; + + fn targets(&self) -> &[Self::Focus] { + &[Focus::Username, Focus::Password, Focus::LoginButton] + } + + fn handle( + &mut self, + focus: &Self::Focus, + action: Self::Action, + ) -> Result, ComponentError> { + match (focus, action) { + (Focus::LoginButton, ComponentAction::Select) => Ok(Some(AppEvent::LoginAttempt { + username: self.username.clone(), + password: self.password.clone(), + })), + (Focus::Username, ComponentAction::TypeChar(c)) => { + self.username.push(c); + Ok(None) + } + (Focus::Password, ComponentAction::TypeChar(c)) => { + self.password.push(c); + Ok(None) + } + _ => Ok(None), + } + } +} + +fn main() -> Result<(), ComponentError> { + let mut orch = Orchestrator::new(); + + orch.bind(Key::enter(), ComponentAction::Select); + orch.bind(Key::tab(), ComponentAction::Next); + orch.bind(Key::shift_tab(), ComponentAction::Prev); + + let login_page = LoginPage { + username: alloc::string::String::new(), + password: alloc::string::String::new(), + }; + + orch.register_page(alloc::string::String::from("login"), login_page); + + Ok(()) +} diff --git a/src/component/error.rs b/src/component/error.rs index d5c9bd7..27d92ce 100644 --- a/src/component/error.rs +++ b/src/component/error.rs @@ -2,6 +2,7 @@ #[derive(Debug, Clone, PartialEq, Eq)] pub enum ComponentError { - EmptyTargets, + InvalidAction, InvalidFocus, + NoComponent, } diff --git a/src/input/bindings.rs b/src/input/bindings.rs index acd4c38..123e89f 100644 --- a/src/input/bindings.rs +++ b/src/input/bindings.rs @@ -1,9 +1,6 @@ use super::action::{Action, ComponentAction}; use super::key::Key; -#[cfg(feature = "alloc")] -use hashbrown::HashSet; - /// Maps keys to actions. /// /// When `alloc` feature is enabled, uses HashMap for O(1) lookup. @@ -96,6 +93,11 @@ impl Bindings { pub fn iter(&self) -> impl Iterator { self.bindings.iter() } + + #[cfg(feature = "alloc")] + pub fn iter(&self) -> impl Iterator { + self.bindings.iter() + } } impl Default for Bindings { diff --git a/src/lib.rs b/src/lib.rs index 2fbd70f..bbcefab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,9 +5,15 @@ extern crate alloc; pub mod component; pub mod focus; pub mod input; +pub mod orchestrator; pub mod prelude { pub use crate::component::{Component, ComponentAction, ComponentError}; pub use crate::focus::{FocusError, FocusId, FocusManager, FocusQuery, Focusable}; pub use crate::input::{Action, Bindings, Key, KeyCode, KeyModifiers, MatchResult}; + pub use crate::orchestrator::{ + ActionResolver, CommandHandler, CommandResult, DefaultActionResolver, + DefaultCommandHandler, DefaultStateCoordinator, EventBus, EventHandler, ModeName, + ModeStack, Orchestrator, Router, RouterEvent, StateCoordinator, StateSync, + }; } diff --git a/src/orchestrator/action_resolver.rs b/src/orchestrator/action_resolver.rs new file mode 100644 index 0000000..dc031ef --- /dev/null +++ b/src/orchestrator/action_resolver.rs @@ -0,0 +1,21 @@ +// path_from_the_root: src/orchestrator/action_resolver.rs + +use crate::component::Component; + +pub struct ResolveContext<'a, C: Component> { + pub component: &'a C, + pub focus: &'a C::Focus, + pub action: C::Action, +} + +pub trait ActionResolver { + fn resolve(&mut self, ctx: ResolveContext) -> C::Action; +} + +pub struct DefaultActionResolver; + +impl ActionResolver for DefaultActionResolver { + fn resolve(&mut self, ctx: ResolveContext) -> C::Action { + ctx.action + } +} diff --git a/src/orchestrator/command_handler.rs b/src/orchestrator/command_handler.rs new file mode 100644 index 0000000..0147f33 --- /dev/null +++ b/src/orchestrator/command_handler.rs @@ -0,0 +1,47 @@ +// path_from_the_root: src/orchestrator/command_handler.rs + +use crate::input::Key; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CommandResult { + Resolved(A), + Incomplete, + Unknown, + Exit, +} + +pub trait CommandHandler { + fn is_active(&self) -> bool; + + fn handle(&mut self, key: Key) -> CommandResult; + + fn enter(&mut self); + + fn exit(&mut self); +} + +pub struct DefaultCommandHandler { + _phantom: core::marker::PhantomData, +} + +impl Default for DefaultCommandHandler { + fn default() -> Self { + Self { + _phantom: core::marker::PhantomData, + } + } +} + +impl CommandHandler for DefaultCommandHandler { + fn is_active(&self) -> bool { + false + } + + fn handle(&mut self, _key: Key) -> CommandResult { + CommandResult::Exit + } + + fn enter(&mut self) {} + + fn exit(&mut self) {} +} diff --git a/src/orchestrator/core.rs b/src/orchestrator/core.rs new file mode 100644 index 0000000..0b3e7a2 --- /dev/null +++ b/src/orchestrator/core.rs @@ -0,0 +1,231 @@ +// path_from_the_root: src/orchestrator/core.rs + +extern crate alloc; + +use alloc::boxed::Box; +use alloc::string::String; +use alloc::vec::Vec; + +use crate::component::{Component, ComponentError}; +use crate::input::{Bindings, Key}; +use crate::orchestrator::{ + ActionResolver, CommandHandler, CommandResult, DefaultActionResolver, DefaultCommandHandler, + DefaultStateCoordinator, EventBus, ModeName, ModeStack, ResolveContext, Router, RouterEvent, + StateCoordinator, +}; + +pub struct Orchestrator { + router: Router, + bindings: Bindings, + action_resolver: Box>, + command_handler: Box>, + state_coordinator: Box>, + modes: ModeStack, + event_bus: EventBus, + running: bool, +} + +impl Default for Orchestrator +where + C::Action: Clone + 'static, +{ + fn default() -> Self { + Self::new() + } +} + +impl Orchestrator +where + C::Action: Clone + 'static, + C::Event: Clone, +{ + pub fn new() -> Self { + Self { + router: Router::new(), + bindings: Bindings::new(), + action_resolver: Box::new(DefaultActionResolver), + command_handler: Box::new(DefaultCommandHandler::default()), + state_coordinator: Box::new(DefaultStateCoordinator), + modes: ModeStack::new(), + event_bus: EventBus::new(), + running: true, + } + } + + pub fn register_page(&mut self, id: String, page: C) { + self.router.register(id, page); + } + + pub fn navigate_to(&mut self, id: String) -> Result<(), ComponentError> { + if let Some(RouterEvent::Navigated { from, to }) = self + .router + .navigate(id.clone()) + .map_err(|_| ComponentError::InvalidFocus)? + { + let _ = self.state_coordinator.on_navigate(from, to); + } + Ok(()) + } + + pub fn back(&mut self) -> Result<(), ComponentError> { + if let Some(RouterEvent::Back { to }) = self + .router + .back() + .map_err(|_| ComponentError::InvalidFocus)? + { + let _ = self.state_coordinator.on_navigate(Some(to.clone()), to); + } + Ok(()) + } + + pub fn forward(&mut self) -> Result<(), ComponentError> { + if let Some(RouterEvent::Forward { to }) = self + .router + .forward() + .map_err(|_| ComponentError::InvalidFocus)? + { + let _ = self.state_coordinator.on_navigate(Some(to.clone()), to); + } + Ok(()) + } + + pub fn bind(&mut self, key: Key, action: C::Action) { + self.bindings.bind(key, action); + } + + pub fn process_frame(&mut self, key: Key) -> Result, ComponentError> { + if !self.running { + return Ok(Vec::new()); + } + + let mut events = Vec::new(); + + if self.command_handler.is_active() { + match self.command_handler.handle(key) { + CommandResult::Resolved(action) => { + if let Some(event) = self.handle_action(action)? { + events.push(event); + } + } + CommandResult::Incomplete | CommandResult::Unknown => {} + CommandResult::Exit => { + self.command_handler.exit(); + } + } + } else if let Some(action) = self.bindings.get(&key) { + let action = action.clone(); + + if let Some(_) = self.router.current_id() { + let component = self.router.current().ok_or(ComponentError::NoComponent)?; + let focus = self.router.focus_manager().current().ok_or(ComponentError::NoComponent)?; + + let ctx = ResolveContext { + component, + focus, + action, + }; + let resolved_action = self.action_resolver.resolve(ctx); + + if let Some(event) = self.handle_action(resolved_action)? { + events.push(event); + } + } + } + + for event in &events { + self.event_bus.emit(event.clone()); + } + + Ok(events) + } + + fn handle_action(&mut self, action: C::Action) -> Result, ComponentError> { + let focus = self.router.focus_manager().current().cloned(); + + if let Some(focus) = focus { + let old_focus = self.router.focus_manager().current().cloned(); + + let result = { + let component = self.router.current_mut().ok_or(ComponentError::NoComponent)?; + component.handle(&focus, action)? + }; + + let new_focus = self.router.focus_manager().current().cloned(); + + if old_focus != new_focus { + let _ = self.state_coordinator.on_focus_change(old_focus, new_focus); + } + + Ok(result) + } else { + Ok(None) + } + } + + pub fn current(&self) -> Option<&C> { + self.router.current() + } + + pub fn current_mut(&mut self) -> Option<&mut C> { + self.router.current_mut() + } + + pub fn focus_manager(&self) -> &crate::focus::FocusManager { + self.router.focus_manager() + } + + pub fn focus_manager_mut(&mut self) -> &mut crate::focus::FocusManager { + self.router.focus_manager_mut() + } + + pub fn modes(&self) -> &ModeStack { + &self.modes + } + + pub fn modes_mut(&mut self) -> &mut ModeStack { + &mut self.modes + } + + pub fn push_mode(&mut self, mode: ModeName) { + self.modes.push(mode); + } + + pub fn pop_mode(&mut self) -> Option { + self.modes.pop() + } + + pub fn event_bus(&self) -> &EventBus { + &self.event_bus + } + + pub fn event_bus_mut(&mut self) -> &mut EventBus { + &mut self.event_bus + } + + pub fn set_action_resolver + 'static>(&mut self, resolver: R) { + self.action_resolver = Box::new(resolver); + } + + pub fn set_command_handler + 'static>( + &mut self, + handler: H, + ) { + self.command_handler = Box::new(handler); + } + + pub fn set_state_coordinator + 'static>(&mut self, coordinator: S) { + self.state_coordinator = Box::new(coordinator); + } + + pub fn is_running(&self) -> bool { + self.running + } + + pub fn stop(&mut self) { + self.running = false; + } + + pub fn start(&mut self) { + self.running = true; + } +} diff --git a/src/orchestrator/event_bus.rs b/src/orchestrator/event_bus.rs new file mode 100644 index 0000000..a7c5902 --- /dev/null +++ b/src/orchestrator/event_bus.rs @@ -0,0 +1,46 @@ +// path_from_the_root: src/orchestrator/event_bus.rs + +extern crate alloc; + +use alloc::boxed::Box; +use alloc::vec::Vec; + +pub trait EventHandler { + fn handle(&mut self, event: E); +} + +pub struct EventBus { + handlers: Vec>>, +} + +impl Default for EventBus { + fn default() -> Self { + Self::new() + } +} + +impl EventBus { + pub fn new() -> Self { + Self { + handlers: Vec::new(), + } + } + + pub fn register(&mut self, handler: Box>) { + self.handlers.push(handler); + } + + pub fn emit(&mut self, event: E) { + for handler in &mut self.handlers { + handler.handle(event.clone()); + } + } + + pub fn handler_count(&self) -> usize { + self.handlers.len() + } + + pub fn is_empty(&self) -> bool { + self.handlers.is_empty() + } +} diff --git a/src/orchestrator/mod.rs b/src/orchestrator/mod.rs new file mode 100644 index 0000000..c260c14 --- /dev/null +++ b/src/orchestrator/mod.rs @@ -0,0 +1,17 @@ +// path_from_the_root: src/orchestrator/mod.rs + +pub mod action_resolver; +pub mod command_handler; +pub mod core; +pub mod event_bus; +pub mod mode; +pub mod router; +pub mod state_coordinator; + +pub use action_resolver::{ActionResolver, DefaultActionResolver, ResolveContext}; +pub use command_handler::{CommandHandler, CommandResult, DefaultCommandHandler}; +pub use core::Orchestrator; +pub use event_bus::{EventBus, EventHandler}; +pub use mode::{ModeName, ModeStack}; +pub use router::{Router, RouterEvent}; +pub use state_coordinator::{DefaultStateCoordinator, StateCoordinator, StateSync}; diff --git a/src/orchestrator/mode.rs b/src/orchestrator/mode.rs new file mode 100644 index 0000000..40f51f8 --- /dev/null +++ b/src/orchestrator/mode.rs @@ -0,0 +1,83 @@ +// path_from_the_root: src/orchestrator/mode.rs + +use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::vec::Vec; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ModeName { + General, + Navigation, + Editing, + Command, + Custom(String), +} + +#[derive(Debug, Clone, Default)] +pub struct ModeStack { + modes: Vec, +} + +impl ModeStack { + pub fn new() -> Self { + Self { modes: Vec::new() } + } + + pub fn push(&mut self, mode: ModeName) { + self.modes.push(mode); + } + + pub fn pop(&mut self) -> Option { + self.modes.pop() + } + + pub fn current(&self) -> Option<&ModeName> { + self.modes.last() + } + + pub fn is_empty(&self) -> bool { + self.modes.is_empty() + } + + pub fn len(&self) -> usize { + self.modes.len() + } + + pub fn contains(&self, mode: &ModeName) -> bool { + self.modes.contains(mode) + } + + pub fn clear(&mut self) { + self.modes.clear(); + } + + pub fn reset(&mut self, mode: ModeName) { + self.modes.clear(); + self.modes.push(mode); + } +} + +#[derive(Debug, Clone, Default)] +pub struct ModeResolver { + mappings: BTreeMap>, +} + +impl ModeResolver { + pub fn new() -> Self { + Self { + mappings: BTreeMap::new(), + } + } + + pub fn register(&mut self, key: String, modes: Vec) { + self.mappings.insert(key, modes); + } + + pub fn resolve(&self, key: &str) -> Option<&Vec> { + self.mappings.get(key) + } + + pub fn is_empty(&self) -> bool { + self.mappings.is_empty() + } +} diff --git a/src/orchestrator/router.rs b/src/orchestrator/router.rs new file mode 100644 index 0000000..92bedd5 --- /dev/null +++ b/src/orchestrator/router.rs @@ -0,0 +1,256 @@ +// path_from_the_root: src/orchestrator/router.rs + +extern crate alloc; + +use alloc::string::String; +use alloc::vec::Vec; + +#[cfg(feature = "alloc")] +use hashbrown::HashMap; + +use crate::component::Component; +use crate::focus::{FocusError, FocusManager}; + +#[derive(Debug, Clone)] +pub enum RouterEvent { + Navigated { from: Option, to: String }, + Back { to: String }, + Forward { to: String }, +} + +pub struct Router { + #[cfg(feature = "alloc")] + pages: HashMap, + #[cfg(not(feature = "alloc"))] + pages: Vec<(String, C)>, + current: Option, + history: Vec, + future: Vec, + focus: FocusManager, +} + +impl Default for Router { + fn default() -> Self { + Self::new() + } +} + +impl Router { + pub fn new() -> Self { + Self { + #[cfg(feature = "alloc")] + pages: HashMap::new(), + #[cfg(not(feature = "alloc"))] + pages: Vec::new(), + current: None, + history: Vec::new(), + future: Vec::new(), + focus: FocusManager::new(), + } + } + + pub fn register(&mut self, id: String, mut page: C) { + #[cfg(feature = "alloc")] + { + if self.current.as_ref() == Some(&id) { + let _ = page.on_enter(); + let targets = page.targets(); + self.focus.set_targets(targets.to_vec()); + } + self.pages.insert(id, page); + } + + #[cfg(not(feature = "alloc"))] + { + if self.current.as_ref() == Some(&id) { + let _ = page.on_enter(); + let targets = page.targets(); + self.focus.set_targets(targets.to_vec()); + } + self.pages.push((id, page)); + } + } + + fn get_page_mut(&mut self, id: &str) -> Option<&mut C> { + #[cfg(feature = "alloc")] + { + self.pages.get_mut(id) + } + + #[cfg(not(feature = "alloc"))] + { + self.pages + .iter_mut() + .find(|(page_id, _)| page_id == id) + .map(|(_, page)| page) + } + } + + fn get_page(&self, id: &str) -> Option<&C> { + #[cfg(feature = "alloc")] + { + self.pages.get(id) + } + + #[cfg(not(feature = "alloc"))] + { + self.pages + .iter() + .find(|(page_id, _)| page_id == id) + .map(|(_, page)| page) + } + } + + pub fn navigate(&mut self, id: String) -> Result, FocusError> { + let result = if let Some(current_id) = self.current.take() { + if let Some(current_page) = self.get_page_mut(¤t_id) { + let _ = current_page.on_exit(); + } + self.history.push(current_id.clone()); + self.future.clear(); + Some(RouterEvent::Navigated { + from: Some(current_id), + to: id.clone(), + }) + } else { + Some(RouterEvent::Navigated { + from: None, + to: id.clone(), + }) + }; + + let targets = if let Some(page) = self.get_page_mut(&id) { + let _ = page.on_enter(); + Some(page.targets().to_vec()) + } else { + None + }; + + if let Some(targets) = targets { + self.focus.set_targets(targets); + } + + self.current = Some(id); + Ok(result) + } + + pub fn back(&mut self) -> Result, FocusError> { + if let Some(current) = self.current.take() { + if let Some(from) = self.history.pop() { + self.future.push(current.clone()); + let to = from.clone(); + + if let Some(current_page) = self.get_page_mut(¤t) { + let _ = current_page.on_exit(); + } + + self.current = Some(to.clone()); + + let targets = if let Some(page) = self.get_page_mut(&to) { + let _ = page.on_enter(); + Some(page.targets().to_vec()) + } else { + None + }; + + if let Some(targets) = targets { + self.focus.set_targets(targets); + } + + Ok(Some(RouterEvent::Back { to })) + } else { + self.current = Some(current); + Ok(None) + } + } else { + Ok(None) + } + } + + pub fn forward(&mut self) -> Result, FocusError> { + if let Some(current) = self.current.take() { + if let Some(from) = self.future.pop() { + self.history.push(current.clone()); + let to = from.clone(); + + if let Some(current_page) = self.get_page_mut(¤t) { + let _ = current_page.on_exit(); + } + + self.current = Some(to.clone()); + + let targets = if let Some(page) = self.get_page_mut(&to) { + let _ = page.on_enter(); + Some(page.targets().to_vec()) + } else { + None + }; + + if let Some(targets) = targets { + self.focus.set_targets(targets); + } + + Ok(Some(RouterEvent::Forward { to })) + } else { + self.current = Some(current); + Ok(None) + } + } else { + Ok(None) + } + } + + pub fn current(&self) -> Option<&C> { + self.current.as_ref().and_then(|id| self.get_page(id)) + } + + pub fn current_mut(&mut self) -> Option<&mut C> { + if let Some(id) = self.current.clone() { + self.get_page_mut(&id) + } else { + None + } + } + + pub fn current_id(&self) -> Option<&String> { + self.current.as_ref() + } + + pub fn focus_manager(&self) -> &FocusManager { + &self.focus + } + + pub fn focus_manager_mut(&mut self) -> &mut FocusManager { + &mut self.focus + } + + pub fn history(&self) -> &[String] { + &self.history + } + + pub fn future(&self) -> &[String] { + &self.future + } + + pub fn page_count(&self) -> usize { + #[cfg(feature = "alloc")] + { + self.pages.len() + } + #[cfg(not(feature = "alloc"))] + { + self.pages.len() + } + } + + pub fn has_page(&self, id: &str) -> bool { + #[cfg(feature = "alloc")] + { + self.pages.contains_key(id) + } + #[cfg(not(feature = "alloc"))] + { + self.pages.iter().any(|(page_id, _)| page_id == id) + } + } +} diff --git a/src/orchestrator/state_coordinator.rs b/src/orchestrator/state_coordinator.rs new file mode 100644 index 0000000..c21a549 --- /dev/null +++ b/src/orchestrator/state_coordinator.rs @@ -0,0 +1,48 @@ +// path_from_the_root: src/orchestrator/state_coordinator.rs + +use alloc::string::String; +use alloc::vec::Vec; + +use crate::component::Component; + +pub enum StateSync { + Synced, + Conflict { details: String }, +} + +pub trait StateCoordinator { + fn on_navigate(&mut self, from: Option, to: String) -> Result; + + fn on_focus_change( + &mut self, + old: Option, + new: Option, + ) -> Result; + + fn on_mode_change(&mut self, _old: Vec, _new: Vec) + -> Result; +} + +pub struct DefaultStateCoordinator; + +impl StateCoordinator for DefaultStateCoordinator { + fn on_navigate(&mut self, _from: Option, _to: String) -> Result { + Ok(StateSync::Synced) + } + + fn on_focus_change( + &mut self, + _old: Option, + _new: Option, + ) -> Result { + Ok(StateSync::Synced) + } + + fn on_mode_change( + &mut self, + _old: Vec, + _new: Vec, + ) -> Result { + Ok(StateSync::Synced) + } +}