Widget System

This page documents how widgets are managed, composed, and interact within the TUI. The widget system provides a flexible architecture for building interactive terminal interfaces.

Widget Architecture

┌─────────────────────────────────────────────────────────────────┐
│                         App                                      │
│  widgets: HashMap<&'static str, Box<dyn Widget>>                │
│  widget_priority_order: Vec<&'static str>                       │
└─────────────────────────────────┬───────────────────────────────┘

        ┌─────────────────────────┼─────────────────────────┐
        │                         │                         │
        ▼                         ▼                         ▼
┌───────────────┐       ┌───────────────┐       ┌───────────────┐
│   StatusBar   │       │  ChatView     │       │ QuestionPanel │
│   priority:10 │       │  priority:50  │       │ priority:110  │
└───────────────┘       └───────────────┘       └───────────────┘

Widget Registration

Widgets are registered with unique IDs:

impl App {
    pub fn register_widget<W: Widget>(&mut self, widget: W) {
        let id = widget.id();
        self.widgets.insert(id, Box::new(widget));
        self.rebuild_priority_order();
    }

    fn rebuild_priority_order(&mut self) {
        let mut ordered: Vec<_> = self.widgets.iter()
            .map(|(id, w)| (*id, w.priority()))
            .collect();
        ordered.sort_by(|a, b| b.1.cmp(&a.1)); // Descending
        self.widget_priority_order = ordered.into_iter()
            .map(|(id, _)| id)
            .collect();
    }
}

Built-in Widgets

Core Widgets

WidgetIDPriorityPurpose
StatusBarstatus_bar10Display status information
ChatViewchat_view50Conversation display
TextInputtext_input-User text input (special handling)

Interactive Widgets

WidgetIDPriorityPurpose
PermissionPanelpermission_panel110Permission requests
QuestionPanelquestion_panel110User questions
SlashPopupslash_popup100Command completion

Overlay Widgets

WidgetIDPriorityPurpose
ThemePickertheme_picker120Theme selection
SessionPickersession_picker120Session switching

Widget Storage

Widgets are stored as type-erased trait objects:

pub widgets: HashMap<&'static str, Box<dyn Widget>>

This enables:

  • Dynamic widget registration
  • Uniform handling in loops
  • Runtime polymorphism

Widget Access

Access widgets with type casting:

// Immutable access
pub fn widget<W: Widget + 'static>(&self, id: &str) -> Option<&W> {
    self.widgets.get(id)
        .and_then(|w| w.as_any().downcast_ref::<W>())
}

// Mutable access
pub fn widget_mut<W: Widget + 'static>(&mut self, id: &str) -> Option<&mut W> {
    self.widgets.get_mut(id)
        .and_then(|w| w.as_any_mut().downcast_mut::<W>())
}

Usage Example

// Access StatusBar
if let Some(status_bar) = self.widget_mut::<StatusBar>(STATUS_BAR) {
    status_bar.update_data(new_data);
}

// Check if QuestionPanel is active
if let Some(panel) = self.widget::<QuestionPanel>(QUESTION_PANEL) {
    if panel.is_active() {
        // Handle active state
    }
}

Priority System

Priority determines key event handling order:

fn handle_key(&mut self, key_event: KeyEvent) {
    // Process widgets in priority order (highest first)
    for widget_id in &self.widget_priority_order {
        if let Some(widget) = self.widgets.get_mut(widget_id) {
            if widget.is_active() {
                match widget.handle_key(key_event, &ctx) {
                    WidgetKeyResult::Handled => return,
                    WidgetKeyResult::Action(action) => {
                        self.process_widget_action(action);
                        return;
                    }
                    WidgetKeyResult::NotHandled => continue,
                }
            }
        }
    }
}

Priority Guidelines

Priority RangeWidget Type
120+Overlay widgets (always first)
100-119Modal widgets (block input)
50-99Interactive content widgets
10-49Display-only widgets
0-9Background widgets

Focus Management

The active widget system manages focus:

// Check if any modal is active
fn overlay_active(&self) -> bool {
    self.widgets.values().any(|w| w.is_overlay() && w.is_active())
}

// Check if input should be blocked
fn input_blocked(&self) -> bool {
    self.widgets.values().any(|w| w.blocks_input() && w.is_active())
}

Widget Activation

Widgets are activated programmatically:

// Activate QuestionPanel
fn activate_question_panel(&mut self, tool_use_id: String, request: AskUserQuestionsRequest) {
    if let Some(widget) = self.widgets.get_mut(QUESTION_PANEL) {
        if let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>() {
            panel.activate(tool_use_id, self.session_id, request, self.current_turn_id.clone());
        }
    }
}

// Deactivate
fn deactivate_question_panel(&mut self) {
    if let Some(widget) = self.widgets.get_mut(QUESTION_PANEL) {
        if let Some(panel) = widget.as_any_mut().downcast_mut::<QuestionPanel>() {
            panel.deactivate();
        }
    }
}

Widget Actions

Widgets communicate via actions:

pub enum WidgetAction {
    SubmitQuestion {
        tool_use_id: String,
        response: AskUserQuestionsResponse,
    },
    CancelQuestion {
        tool_use_id: String,
    },
    SubmitPermission {
        tool_use_id: String,
        response: PermissionResponse,
    },
    CancelPermission {
        tool_use_id: String,
    },
    SwitchSession {
        session_id: i64,
    },
    ExecuteCommand {
        command: String,
    },
    Close,
}

Action Processing

fn process_widget_action(&mut self, action: WidgetAction) {
    match action {
        WidgetAction::SubmitQuestion { tool_use_id, response } => {
            self.submit_question_response(tool_use_id, response);
        }
        WidgetAction::CancelQuestion { tool_use_id } => {
            self.cancel_question(tool_use_id);
        }
        WidgetAction::SwitchSession { session_id } => {
            self.switch_session(session_id);
        }
        WidgetAction::ExecuteCommand { command } => {
            self.execute_slash_command(&command);
        }
        WidgetAction::Close => {
            self.close_active_overlay();
        }
        // ... other actions
    }
}

Widget Key Context

Context passed to widgets for key handling:

pub struct WidgetKeyContext<'a> {
    pub theme: &'a Theme,
    pub nav: NavigationHelper<'a>,
}

pub struct NavigationHelper<'a> {
    bindings: &'a KeyBindings,
}

impl NavigationHelper<'_> {
    pub fn is_up(&self, key: KeyEvent) -> bool { /* ... */ }
    pub fn is_down(&self, key: KeyEvent) -> bool { /* ... */ }
    pub fn is_select(&self, key: KeyEvent) -> bool { /* ... */ }
    pub fn is_cancel(&self, key: KeyEvent) -> bool { /* ... */ }
}

ConversationView

The conversation display uses a separate trait:

pub trait ConversationView: Send + 'static {
    fn add_user_message(&mut self, content: String);
    fn add_assistant_message(&mut self, content: String);
    fn append_streaming(&mut self, text: &str);
    fn complete_streaming(&mut self);
    fn is_streaming(&self) -> bool;
    fn add_tool_message(&mut self, tool_use_id: &str, name: &str, title: &str);
    fn update_tool_status(&mut self, tool_use_id: &str, status: ToolStatus);
    fn scroll_up(&mut self);
    fn scroll_down(&mut self);
    fn render(&mut self, frame: &mut Frame, area: Rect, theme: &Theme, pending: Option<&str>);
    fn save_state(&self) -> Box<dyn Any + Send>;
    fn restore_state(&mut self, state: Box<dyn Any + Send>);
    fn clear(&mut self);
}

Conversation Factory

Custom conversation views can be provided:

pub type ConversationViewFactory = Box<dyn Fn() -> Box<dyn ConversationView> + Send>;

// Default factory
let factory: ConversationViewFactory = Box::new(|| {
    Box::new(ChatView::new())
});

Widget Composition

Widgets can be composed in the layout:

// Register multiple widgets
app.register_widget(StatusBar::new());
app.register_widget(PermissionPanel::new());
app.register_widget(QuestionPanel::new());
app.register_widget(SlashPopupState::new());
app.register_widget(ThemePickerState::new());
app.register_widget(SessionPickerState::new());

All registered widgets participate in:

  • Key event dispatch (by priority)
  • Rendering (if active)
  • Layout computation

Custom Widgets

Create custom widgets by implementing the trait:

pub struct MyWidget {
    active: bool,
    data: String,
}

impl Widget for MyWidget {
    fn id(&self) -> &'static str {
        "my_widget"
    }

    fn priority(&self) -> u8 {
        75
    }

    fn is_active(&self) -> bool {
        self.active
    }

    fn handle_key(&mut self, key: KeyEvent, ctx: &WidgetKeyContext) -> WidgetKeyResult {
        if ctx.nav.is_cancel(key) {
            self.active = false;
            return WidgetKeyResult::Action(WidgetAction::Close);
        }
        WidgetKeyResult::NotHandled
    }

    fn render(&mut self, frame: &mut Frame, area: Rect, theme: &Theme) {
        let block = Block::default()
            .title("My Widget")
            .borders(Borders::ALL);
        frame.render_widget(block, area);
    }

    fn as_any(&self) -> &dyn Any { self }
    fn as_any_mut(&mut self) -> &mut dyn Any { self }
    fn into_any(self: Box<Self>) -> Box<dyn Any> { self }
}

Next Steps