From 2f702ad446fcd6a261a3bea0ab2807d70eca43e2 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Tue, 21 Apr 2026 13:07:27 -0700 Subject: refactor: Replace ad-hoc dispatch with FSM + driver architecture (#3434) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the tangled dispatch handler system (`tui/dispatch.rs`, `tui/state.rs`) with a pure finite state machine + driver architecture. The FSM handles all state transitions as explicit `(State, Event) → (NewState, Effects)` mappings. The driver executes IO effects and bridges the TUI to the FSM. --- crates/atuin-ai/src/tui/components/atuin_ai.rs | 7 +++---- crates/atuin-ai/src/tui/components/input_box.rs | 12 ++++++++---- crates/atuin-ai/src/tui/components/select.rs | 7 +++---- 3 files changed, 14 insertions(+), 12 deletions(-) (limited to 'crates/atuin-ai/src/tui/components') diff --git a/crates/atuin-ai/src/tui/components/atuin_ai.rs b/crates/atuin-ai/src/tui/components/atuin_ai.rs index c7227fbd..31dff1c3 100644 --- a/crates/atuin-ai/src/tui/components/atuin_ai.rs +++ b/crates/atuin-ai/src/tui/components/atuin_ai.rs @@ -5,11 +5,10 @@ //! Tab) are handled in the bubble phase so child components like the //! permission Select can consume them first. -use std::sync::mpsc; - use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use eye_declare::{Elements, EventResult, Hooks, component, props}; +use crate::commands::inline::DriverEventSender; use crate::tui::events::AiTuiEvent; use crate::tui::state::AppMode; @@ -28,7 +27,7 @@ pub(crate) struct AtuinAi { #[derive(Default)] pub(crate) struct AtuinAiState { - tx: Option>, + tx: Option, } #[component(props = AtuinAi, state = AtuinAiState, children = Elements)] @@ -38,7 +37,7 @@ fn atuin_ai( hooks: &mut Hooks, children: Elements, ) -> Elements { - hooks.use_context::>(|tx, _, state| { + hooks.use_context::(|tx, _, state| { state.tx = tx.cloned(); }); diff --git a/crates/atuin-ai/src/tui/components/input_box.rs b/crates/atuin-ai/src/tui/components/input_box.rs index 6e041418..6b81322c 100644 --- a/crates/atuin-ai/src/tui/components/input_box.rs +++ b/crates/atuin-ai/src/tui/components/input_box.rs @@ -6,7 +6,7 @@ //! //! On Enter, sends `AiTuiEvent::SubmitInput` via the context-provided channel. -use std::sync::{Arc, Mutex, mpsc}; +use std::sync::{Arc, Mutex}; use crossterm::event::KeyModifiers; use eye_declare::{Canvas, Elements, EventResult, Hooks, component, element, props}; @@ -19,6 +19,7 @@ use ratatui_core::{ }; use tui_textarea::TextArea; +use crate::commands::inline::DriverEventSender; use crate::tui::{events::AiTuiEvent, slash::SlashCommandSearchResult}; /// A bordered text input box backed by tui-textarea. @@ -41,7 +42,7 @@ pub(crate) struct InputBox { pub(crate) struct InputBoxState { textarea: Arc>>, - tx: Option>, + tx: Option, } impl Default for InputBoxState { @@ -97,10 +98,13 @@ fn input_box( state: &InputBoxState, hooks: &mut Hooks, ) -> Elements { - hooks.use_focusable(props.active); + // Always focusable so focus isn't lost when the permission Select is + // removed from the tree. The `active` prop controls visual state and + // whether keystrokes are processed, not focusability. + hooks.use_focusable(true); hooks.use_autofocus(); - hooks.use_context::>(|tx, _, state| { + hooks.use_context::(|tx, _, state| { state.tx = tx.cloned(); }); diff --git a/crates/atuin-ai/src/tui/components/select.rs b/crates/atuin-ai/src/tui/components/select.rs index 5abbe655..771d7830 100644 --- a/crates/atuin-ai/src/tui/components/select.rs +++ b/crates/atuin-ai/src/tui/components/select.rs @@ -1,10 +1,9 @@ -use std::sync::mpsc; - use crossterm::event::KeyCode; use eye_declare::{Elements, EventResult, Hooks, Span, Text, View, component, element, props}; use ratatui::style::Style; use typed_builder::TypedBuilder; +use crate::commands::inline::DriverEventSender; use crate::tui::events::AiTuiEvent; type OnSelectFn = Box Option + Send + Sync + 'static>; @@ -24,7 +23,7 @@ pub(crate) struct SelectOption { #[derive(Default)] pub(crate) struct PermissionSelectorState { selected_option: usize, - tx: Option>, + tx: Option, } #[props] @@ -42,7 +41,7 @@ pub(crate) fn permission_selector( hooks.use_focusable(true); hooks.use_autofocus(); - hooks.use_context::>(|tx, _, state| { + hooks.use_context::(|tx, _, state| { state.tx = tx.cloned(); }); -- cgit v1.3.1