use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
struct ShortcutItem {
key: &'static str,
verb: &'static str,
}
const SHORTCUTS: &[ShortcutItem] = &[
ShortcutItem {
key: "enter",
verb: "send message",
},
ShortcutItem {
key: "shift+enter",
verb: "new line",
},
ShortcutItem {
key: "tab",
verb: "complete",
},
ShortcutItem {
key: "esc",
verb: "clear / cancel",
},
ShortcutItem {
key: "\u{2191}/\u{2193}",
verb: "history",
},
ShortcutItem {
key: "/",
verb: "commands",
},
ShortcutItem {
key: "@",
verb: "file mention",
},
ShortcutItem {
key: "shift+tab",
verb: "cycle trust mode",
},
ShortcutItem {
key: "ctrl+l",
verb: "jump to bottom",
},
ShortcutItem {
key: "ctrl+r",
verb: "reverse history search",
},
ShortcutItem {
key: "ctrl+o",
verb: "tool output in full",
},
ShortcutItem {
key: "pgup/pgdn",
verb: "scroll history",
},
ShortcutItem {
key: "ctrl+c",
verb: "cancel / quit",
},
ShortcutItem {
key: "ctrl+d",
verb: "quit (when empty)",
},
ShortcutItem {
key: "?",
verb: "toggle this overlay",
},
];
const NARROW_TERMINAL_WIDTH: u16 = 80;
const CHROME_LINES: u16 = 3;
pub fn overlay_height(width: u16) -> u16 {
let item_lines = if width < NARROW_TERMINAL_WIDTH {
SHORTCUTS.len() as u16
} else {
SHORTCUTS.len().div_ceil(2) as u16
};
item_lines + CHROME_LINES
}
pub fn build_overlay_lines(width: u16) -> Vec<Line<'static>> {
let header_style = Style::default()
.fg(Color::Rgb(198, 165, 106))
.add_modifier(Modifier::BOLD);
let hint_style = Style::default().fg(Color::Rgb(124, 111, 100));
let mut lines = Vec::with_capacity(overlay_height(width) as usize);
lines.push(Line::from(vec![
Span::styled(" Shortcuts", header_style),
Span::styled(" press ", hint_style),
Span::styled(
"?",
Style::default()
.fg(Color::Rgb(198, 165, 106))
.add_modifier(Modifier::BOLD),
),
Span::styled(" or ", hint_style),
Span::styled(
"esc",
Style::default()
.fg(Color::Rgb(198, 165, 106))
.add_modifier(Modifier::BOLD),
),
Span::styled(" to close", hint_style),
]));
lines.push(Line::from(""));
if width < NARROW_TERMINAL_WIDTH {
let key_col_width = max_key_width(SHORTCUTS);
for item in SHORTCUTS {
lines.push(format_row(item, key_col_width));
}
} else {
let rows = SHORTCUTS.len().div_ceil(2);
let key_col_width = max_key_width(SHORTCUTS);
let left_verb_width = SHORTCUTS
.iter()
.take(rows)
.map(|i| i.verb.chars().count())
.max()
.unwrap_or(0);
for (row, left) in SHORTCUTS.iter().take(rows).enumerate() {
let right = SHORTCUTS.get(row + rows);
let mut spans = vec![
Span::raw(" "),
Span::styled(pad_right(left.key, key_col_width), accent_style()),
Span::raw(" "),
Span::styled(pad_right(left.verb, left_verb_width), verb_style()),
];
if let Some(right) = right {
spans.extend([
Span::raw(" "),
Span::styled(pad_right(right.key, key_col_width), accent_style()),
Span::raw(" "),
Span::styled(right.verb.to_string(), verb_style()),
]);
}
lines.push(Line::from(spans));
}
}
lines.push(Line::from(""));
lines
}
fn pad_right(s: &str, width: usize) -> String {
let visible = s.chars().count();
if visible >= width {
s.to_string()
} else {
let mut out = String::with_capacity(s.len() + (width - visible));
out.push_str(s);
for _ in 0..(width - visible) {
out.push(' ');
}
out
}
}
fn max_key_width(items: &[ShortcutItem]) -> usize {
items
.iter()
.map(|i| i.key.chars().count())
.max()
.unwrap_or(0)
}
fn accent_style() -> Style {
Style::default().fg(Color::Rgb(198, 165, 106))
}
fn verb_style() -> Style {
Style::default().fg(Color::Rgb(140, 140, 140))
}
fn format_row(item: &ShortcutItem, key_col_width: usize) -> Line<'static> {
Line::from(vec![
Span::raw(" "),
Span::styled(pad_right(item.key, key_col_width), accent_style()),
Span::raw(" "),
Span::styled(item.verb.to_string(), verb_style()),
])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn overlay_height_two_column_for_wide_terminal() {
let h = overlay_height(120);
let expected = SHORTCUTS.len().div_ceil(2) as u16 + CHROME_LINES;
assert_eq!(h, expected, "wide layout uses two columns");
}
#[test]
fn overlay_height_single_column_for_narrow_terminal() {
let h = overlay_height(60);
let expected = SHORTCUTS.len() as u16 + CHROME_LINES;
assert_eq!(h, expected, "narrow layout falls back to one column");
}
#[test]
fn overlay_height_uses_two_columns_at_threshold() {
assert_eq!(overlay_height(80), overlay_height(120));
}
#[test]
fn build_overlay_lines_contains_every_verb() {
let lines = build_overlay_lines(120);
let text: String = lines
.iter()
.flat_map(|l| l.spans.iter().map(|s| s.content.as_ref()))
.collect();
for item in SHORTCUTS {
assert!(
text.contains(item.verb),
"verb {:?} missing from rendered overlay (text={text})",
item.verb,
);
}
}
#[test]
fn header_advertises_dismiss_keys() {
let lines = build_overlay_lines(120);
let header: String = lines[0].spans.iter().map(|s| s.content.as_ref()).collect();
assert!(header.contains('?'), "header must mention `?`: {header}");
assert!(
header.contains("esc"),
"header must mention `esc`: {header}"
);
assert!(
header.to_lowercase().contains("close"),
"header must say how to dismiss: {header}",
);
}
#[test]
fn narrow_layout_lines_match_height() {
let lines = build_overlay_lines(60);
assert_eq!(lines.len() as u16, overlay_height(60));
}
#[test]
fn wide_layout_lines_match_height() {
let lines = build_overlay_lines(120);
assert_eq!(lines.len() as u16, overlay_height(120));
}
#[test]
fn pad_right_does_not_truncate() {
assert_eq!(pad_right("abcdef", 3), "abcdef");
}
#[test]
fn pad_right_pads_to_width() {
let padded = pad_right("ab", 5);
assert_eq!(padded, "ab ");
assert_eq!(padded.chars().count(), 5);
}
#[test]
fn visual_dump() {
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::widgets::{Paragraph, Widget};
for width in [120u16, 80, 60] {
let h = overlay_height(width);
let area = Rect::new(0, 0, width, h);
let mut buf = Buffer::empty(area);
Paragraph::new(build_overlay_lines(width)).render(area, &mut buf);
eprintln!("\n=== width={width}, height={h} ===");
for y in 0..area.height {
let row: String = (0..area.width)
.map(|x| buf.cell((x, y)).map(|c| c.symbol()).unwrap_or(" "))
.collect();
eprintln!("|{}|", row.trim_end());
}
}
}
}