steer_tui/tui/widgets/input_panel/
mode_title.rs

1//! Mode title widget for displaying input mode information and keybinds
2
3use ratatui::style::{Modifier, Style};
4use ratatui::text::{Line, Span};
5
6use crate::tui::InputMode;
7use crate::tui::get_spinner_char;
8use crate::tui::theme::{Component, Theme};
9
10/// Widget for displaying the input mode title with keybinds
11#[derive(Debug)]
12pub struct ModeTitleWidget<'a> {
13    mode: InputMode,
14    is_processing: bool,
15    spinner_state: usize,
16    theme: &'a Theme,
17    has_content: bool,
18}
19
20impl<'a> ModeTitleWidget<'a> {
21    /// Create a new mode title widget
22    pub fn new(
23        mode: InputMode,
24        is_processing: bool,
25        spinner_state: usize,
26        theme: &'a Theme,
27        has_content: bool,
28    ) -> Self {
29        Self {
30            mode,
31            is_processing,
32            spinner_state,
33            theme,
34            has_content,
35        }
36    }
37
38    /// Render the mode title as a Line
39    pub fn render(&self) -> Line<'static> {
40        let mut spans = vec![];
41
42        // Add spinner if processing
43        if self.is_processing {
44            spans.push(Span::styled(
45                format!(" {}", get_spinner_char(self.spinner_state)),
46                self.theme.style(Component::ToolCall),
47            ));
48        }
49
50        // Add mode title content
51        spans.push(Span::raw(" "));
52
53        let formatted_mode = self.get_formatted_mode();
54        if let Some(mode) = formatted_mode {
55            spans.push(mode);
56            spans.push(Span::styled(" │ ", self.theme.style(Component::DimText)));
57        }
58
59        // Add mode-specific keybinds
60        let keybinds = self.get_mode_keybinds();
61        spans.extend(format_keybinds(&keybinds, self.theme));
62
63        spans.push(Span::raw(" "));
64        Line::from(spans)
65    }
66
67    /// Get the formatted mode name with styling
68    fn get_formatted_mode(&self) -> Option<Span<'static>> {
69        let mode_name = match self.mode {
70            InputMode::Simple => return None,
71            InputMode::VimNormal => "NORMAL",
72            InputMode::VimInsert => "INSERT",
73            InputMode::BashCommand => "Bash",
74            InputMode::AwaitingApproval => "Awaiting Approval",
75            InputMode::ConfirmExit => "Confirm Exit",
76            InputMode::EditMessageSelection => "Edit Selection",
77            InputMode::FuzzyFinder => "Search",
78            InputMode::Setup => "Setup",
79        };
80
81        let component = match self.mode {
82            InputMode::ConfirmExit => Component::ErrorBold,
83            InputMode::BashCommand => Component::CommandPrompt,
84            InputMode::AwaitingApproval => Component::ErrorBold,
85            InputMode::EditMessageSelection => Component::SelectionHighlight,
86            InputMode::FuzzyFinder => Component::SelectionHighlight,
87            _ => Component::ModelInfo,
88        };
89
90        Some(Span::styled(mode_name, self.theme.style(component)))
91    }
92
93    /// Get the keybinds for the current mode
94    fn get_mode_keybinds(&self) -> Vec<(&'static str, &'static str)> {
95        match self.mode {
96            InputMode::Simple => {
97                if self.has_content {
98                    vec![("Enter", "send"), ("ESC ESC", "clear")]
99                } else {
100                    vec![
101                        ("Enter", "send"),
102                        ("ESC ESC", "edit previous"),
103                        ("!", "bash"),
104                        ("/", "command"),
105                        ("@", "file"),
106                    ]
107                }
108            }
109            InputMode::VimNormal => {
110                if self.has_content {
111                    vec![("i", "insert"), ("ESC ESC", "clear"), ("hjkl", "move")]
112                } else {
113                    vec![
114                        ("i", "insert"),
115                        ("ESC ESC", "edit previous"),
116                        ("!", "bash"),
117                        ("/", "command"),
118                    ]
119                }
120            }
121            InputMode::VimInsert => {
122                vec![("Esc", "normal"), ("ESC ESC", "clear"), ("Enter", "send")]
123            }
124            InputMode::BashCommand => {
125                vec![("Enter", "execute"), ("Esc", "cancel")]
126            }
127            InputMode::AwaitingApproval => {
128                // No keybinds for this mode
129                vec![]
130            }
131            InputMode::ConfirmExit => {
132                vec![("y/Y", "confirm"), ("any other key", "cancel")]
133            }
134            InputMode::EditMessageSelection => {
135                vec![("↑↓", "navigate"), ("Enter", "select"), ("Esc", "cancel")]
136            }
137            InputMode::FuzzyFinder => {
138                vec![("↑↓", "navigate"), ("Enter", "select"), ("Esc", "cancel")]
139            }
140            InputMode::Setup => {
141                // No keybinds shown during setup mode
142                vec![]
143            }
144        }
145    }
146}
147
148/// Helper function to format keybind hints with consistent styling
149fn format_keybind(key: &str, description: &str, theme: &Theme) -> Vec<Span<'static>> {
150    vec![
151        Span::styled(
152            format!("[{key}]"),
153            Style::default().add_modifier(Modifier::BOLD),
154        ),
155        Span::styled(format!(" {description}"), theme.style(Component::DimText)),
156    ]
157}
158
159/// Format a list of keybinds with separators
160fn format_keybinds(keybinds: &[(&str, &str)], theme: &Theme) -> Vec<Span<'static>> {
161    let mut spans = Vec::new();
162    for (i, (key, description)) in keybinds.iter().enumerate() {
163        spans.extend(format_keybind(key, description, theme));
164        if i < keybinds.len() - 1 {
165            spans.push(Span::styled(" │ ", theme.style(Component::DimText)));
166        }
167    }
168    spans
169}