rab/agent/ui/components/
header.rs1use crossterm::event::KeyEvent;
2
3use crate::agent::ui::theme::ThemeKey;
4use crate::agent::ui::theme::current_theme;
5use crate::tui::Component;
6use crate::tui::keybindings;
7use crate::tui::keybindings::get_keybindings;
8const VERSION: &str = env!("CARGO_PKG_VERSION");
9
10fn key_text(action_id: &str) -> String {
12 let keys = keybindings::get_keybindings().get_keys(action_id);
13 if keys.is_empty() {
14 String::new()
15 } else {
16 keys[0].clone()
17 }
18}
19
20fn key_hint(action_id: &str, description: &str) -> String {
22 let kt = key_text(action_id);
23 if kt.is_empty() {
24 return String::new();
25 }
26 let theme = current_theme();
27 let key_part = theme.fg_key(ThemeKey::Dim, &kt);
28 let desc_part = theme.fg_key(ThemeKey::Muted, &format!(" {}", description));
29 format!("{}{}", key_part, desc_part)
30}
31
32fn raw_key_hint(key: &str, description: &str) -> String {
34 let theme = current_theme();
35 let key_part = theme.fg_key(ThemeKey::Dim, key);
36 let desc_part = theme.fg_key(ThemeKey::Muted, &format!(" {}", description));
37 format!("{}{}", key_part, desc_part)
38}
39
40pub struct HeaderComponent {
43 expanded: bool,
44 cached_lines: Option<Vec<String>>,
45}
46
47impl HeaderComponent {
48 pub fn new() -> Self {
49 Self {
50 expanded: false,
51 cached_lines: None,
52 }
53 }
54
55 fn build_lines(&self, _width: usize) -> Vec<String> {
56 let logo = {
57 let theme = current_theme();
58 format!(
59 "{}{}",
60 theme.bold(&theme.fg_key(ThemeKey::Accent, "rab")),
61 theme.fg_key(ThemeKey::Dim, &format!(" v{}", VERSION)),
62 )
63 };
64
65 if self.expanded {
66 let mut lines: Vec<String> = Vec::new();
68 lines.push(logo);
69 lines.push(String::new());
70
71 lines.push(key_hint("app.interrupt", "to interrupt"));
72 lines.push(key_hint("app.clear", "to clear"));
73 lines.push(raw_key_hint(
74 &format!("{} twice", key_text("app.clear")),
75 "to exit",
76 ));
77 lines.push(key_hint("app.exit", "to exit (empty)"));
78 lines.push(key_hint("app.suspend", "to suspend"));
79 lines.push(key_hint("tui.editor.deleteToLineEnd", "to delete to end"));
80 lines.push(key_hint("app.thinking.cycle", "to cycle thinking level"));
81 lines.push(raw_key_hint(
82 &format!(
83 "{}/{}",
84 key_text("app.model.cycleForward"),
85 key_text("app.model.cycleBackward")
86 ),
87 "to cycle models",
88 ));
89 lines.push(key_hint("app.model.select", "to select model"));
90 lines.push(key_hint("app.tools.expand", "to expand tools"));
91 lines.push(key_hint("app.thinking.toggle", "to expand thinking"));
92 lines.push(key_hint("app.editor.external", "for external editor"));
93 lines.push(raw_key_hint("/", "for commands"));
94 lines.push(raw_key_hint("!", "to run bash"));
95 lines.push(raw_key_hint("!!", "to run bash (no context)"));
96 lines.push(key_hint("app.message.followUp", "to queue follow-up"));
97 lines.push(key_hint(
98 "app.message.dequeue",
99 "to edit all queued messages",
100 ));
101 lines.push(raw_key_hint("drop files", "to attach"));
102
103 lines
104 } else {
105 let parts = [
107 key_hint("app.interrupt", "interrupt"),
108 raw_key_hint(
109 &format!("{}/{}", key_text("app.clear"), key_text("app.exit")),
110 "clear/exit",
111 ),
112 raw_key_hint("/", "commands"),
113 raw_key_hint("!", "bash"),
114 key_hint("app.tools.expand", "more"),
115 ];
116 let separator = {
117 let theme = current_theme();
118 theme.fg_key(ThemeKey::Muted, " · ")
119 };
120 let compact_line = parts.join(&separator);
121
122 let compact_onboarding = {
123 let theme = current_theme();
124 theme.fg(
125 "dim",
126 &format!(
127 "Press {} to show full startup help and loaded resources.",
128 key_text("app.tools.expand"),
129 ),
130 )
131 };
132
133 vec![logo, compact_line, String::new(), compact_onboarding]
134 }
135 }
136}
137
138impl Default for HeaderComponent {
139 fn default() -> Self {
140 Self::new()
141 }
142}
143
144impl Component for HeaderComponent {
145 fn handle_input(&mut self, key: &KeyEvent) -> bool {
146 let kb = get_keybindings();
147 if kb.matches(key, keybindings::ACTION_APP_TOOLS_EXPAND) {
148 self.expanded = !self.expanded;
149 self.cached_lines = None;
150 return false;
153 }
154 if self.expanded && kb.matches(key, keybindings::ACTION_APP_ESCAPE) {
156 self.expanded = false;
157 self.cached_lines = None;
158 return true;
159 }
160 false
161 }
162
163 fn set_expanded(&mut self, expanded: bool) {
164 self.expanded = expanded;
165 self.cached_lines = None;
166 }
167
168 fn render(&mut self, width: usize) -> Vec<String> {
169 if let Some(ref cached) = self.cached_lines {
170 return cached.clone();
171 }
172 let lines = self.build_lines(width);
173 self.cached_lines = Some(lines.clone());
174 lines
175 }
176
177 fn invalidate(&mut self) {
178 self.cached_lines = None;
179 }
180}