use crossterm::event::KeyEvent;
use crate::agent::ui::theme::current_theme;
use crate::tui::Component;
use crate::tui::keybindings;
use crate::tui::keybindings::get_keybindings;
const VERSION: &str = env!("CARGO_PKG_VERSION");
fn key_text(action_id: &str) -> String {
let keys = keybindings::get_keybindings().get_keys(action_id);
if keys.is_empty() {
String::new()
} else {
keys[0].clone()
}
}
fn key_hint(action_id: &str, description: &str) -> String {
let kt = key_text(action_id);
if kt.is_empty() {
return String::new();
}
let theme = current_theme();
let key_part = theme.fg("dim", &kt);
let desc_part = theme.fg("muted", &format!(" {}", description));
format!("{}{}", key_part, desc_part)
}
fn raw_key_hint(key: &str, description: &str) -> String {
let theme = current_theme();
let key_part = theme.fg("dim", key);
let desc_part = theme.fg("muted", &format!(" {}", description));
format!("{}{}", key_part, desc_part)
}
pub struct HeaderComponent {
expanded: bool,
cached_lines: std::cell::RefCell<Option<Vec<String>>>,
}
impl HeaderComponent {
pub fn new() -> Self {
Self {
expanded: false,
cached_lines: std::cell::RefCell::new(None),
}
}
fn build_lines(&self, _width: usize) -> Vec<String> {
let logo = {
let theme = current_theme();
format!(
"{}{}",
theme.bold(&theme.fg("accent", "rab")),
theme.fg("dim", &format!(" v{}", VERSION)),
)
};
if self.expanded {
let mut lines: Vec<String> = Vec::new();
lines.push(logo);
lines.push(String::new());
lines.push(key_hint("app.interrupt", "to interrupt"));
lines.push(key_hint("app.clear", "to clear"));
lines.push(raw_key_hint(
&format!("{} twice", key_text("app.clear")),
"to exit",
));
lines.push(key_hint("app.exit", "to exit (empty)"));
lines.push(key_hint("app.suspend", "to suspend"));
lines.push(key_hint("tui.editor.deleteToLineEnd", "to delete to end"));
lines.push(key_hint("app.thinking.cycle", "to cycle thinking level"));
lines.push(raw_key_hint(
&format!(
"{}/{}",
key_text("app.model.cycleForward"),
key_text("app.model.cycleBackward")
),
"to cycle models",
));
lines.push(key_hint("app.model.select", "to select model"));
lines.push(key_hint("app.tools.expand", "to expand tools"));
lines.push(key_hint("app.thinking.toggle", "to expand thinking"));
lines.push(key_hint("app.editor.external", "for external editor"));
lines.push(raw_key_hint("/", "for commands"));
lines.push(raw_key_hint("!", "to run bash"));
lines.push(raw_key_hint("!!", "to run bash (no context)"));
lines.push(key_hint("app.message.followUp", "to queue follow-up"));
lines.push(key_hint(
"app.message.dequeue",
"to edit all queued messages",
));
lines.push(raw_key_hint("drop files", "to attach"));
lines
} else {
let parts = [
key_hint("app.interrupt", "interrupt"),
raw_key_hint(
&format!("{}/{}", key_text("app.clear"), key_text("app.exit")),
"clear/exit",
),
raw_key_hint("/", "commands"),
raw_key_hint("!", "bash"),
key_hint("app.tools.expand", "more"),
];
let separator = {
let theme = current_theme();
theme.fg("muted", " · ")
};
let compact_line = parts.join(&separator);
let compact_onboarding = {
let theme = current_theme();
theme.fg(
"dim",
&format!(
"Press {} to show full startup help and loaded resources.",
key_text("app.tools.expand"),
),
)
};
vec![logo, compact_line, String::new(), compact_onboarding]
}
}
}
impl Default for HeaderComponent {
fn default() -> Self {
Self::new()
}
}
impl Component for HeaderComponent {
fn handle_input(&mut self, key: &KeyEvent) -> bool {
let kb = get_keybindings();
if kb.matches(key, keybindings::ACTION_APP_TOOLS_EXPAND) {
self.expanded = !self.expanded;
*self.cached_lines.borrow_mut() = None;
return false;
}
if self.expanded && kb.matches(key, keybindings::ACTION_APP_ESCAPE) {
self.expanded = false;
*self.cached_lines.borrow_mut() = None;
return true;
}
false
}
fn set_expanded(&mut self, expanded: bool) {
self.expanded = expanded;
*self.cached_lines.borrow_mut() = None;
}
fn render(&self, width: usize) -> Vec<String> {
if let Some(ref cached) = *self.cached_lines.borrow() {
return cached.clone();
}
let lines = self.build_lines(width);
*self.cached_lines.borrow_mut() = Some(lines.clone());
lines
}
fn invalidate(&mut self) {
*self.cached_lines.borrow_mut() = None;
}
}