feat: emphasize keybinding overview headings

This commit is contained in:
2026-10-11 18:08:50 +02:00
parent aaa13ab905
commit df63eb0d85
2 changed files with 86 additions and 17 deletions
+1
View File
@@ -77,6 +77,7 @@ These are the default bindings. Overrides in `[keys]` replace an action's bindin
**Ctrl-C quits from any mode or popup.** **Ctrl-C quits from any mode or popup.**
The bottom bar shows essential actions for the current context. Press `?` in normal mode—including selectors and previews—for the full reference. Scroll help with `j` / `k`, arrows, Ctrl-J / Ctrl-K, or PageDown / PageUp; `?` or Esc closes it without changing the underlying view or dialog. In text fields, `?` remains ordinary input. The bottom bar shows essential actions for the current context. Press `?` in normal mode—including selectors and previews—for the full reference. Scroll help with `j` / `k`, arrows, Ctrl-J / Ctrl-K, or PageDown / PageUp; `?` or Esc closes it without changing the underlying view or dialog. In text fields, `?` remains ordinary input.
The overview title and action headings are bold; binding values, descriptions, and context labels remain normal weight.
Back and forward navigation restore cached videos, selection, and pagination, including replaced temporary tabs. Removed views are skipped; empty history leaves the current view unchanged. Opening a different view after going back discards the old forward branch. Esc first closes a dialog or leaves insert mode without navigating. Minus and plus remain ordinary text inside input fields. Back and forward navigation restore cached videos, selection, and pagination, including replaced temporary tabs. Removed views are skipped; empty history leaves the current view unchanged. Opening a different view after going back discards the old forward branch. Esc first closes a dialog or leaves insert mode without navigating. Minus and plus remain ordinary text inside input fields.
+85 -17
View File
@@ -1,6 +1,10 @@
use std::{fmt::Write as _, io}; use std::{fmt::Write as _, io};
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::{
style::Stylize,
text::{Line, Span, Text},
};
use toml_edit::{Array, Item}; use toml_edit::{Array, Item};
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -419,25 +423,28 @@ impl Bindings {
.map_or("unbound", String::as_str) .map_or("unbound", String::as_str)
} }
pub fn help_text(&self) -> String { pub fn help_text(&self) -> Text<'static> {
let mut text = String::from("Keyboard bindings\n\n"); let mut text = Text::from(Line::from(Span::raw("Keyboard bindings").bold()));
text.push_line("");
for definition in DEFINITIONS { for definition in DEFINITIONS {
let aliases = &self.labels[definition.action as usize]; let aliases = &self.labels[definition.action as usize];
let bindings = if aliases.is_empty() { let mut body = String::from(": ");
"unbound".to_owned() if aliases.is_empty() {
body.push_str("unbound");
} else { } else {
aliases.join(", ") for (index, alias) in aliases.iter().enumerate() {
}; if index != 0 {
let _ = write!( body.push_str(", ");
text, }
"{}: {} — {} [", body.push_str(alias);
definition.name, bindings, definition.description }
); }
let _ = write!(body, " — {} [", definition.description);
for (index, context) in definition.contexts.iter().enumerate() { for (index, context) in definition.contexts.iter().enumerate() {
if index != 0 { if index != 0 {
text.push_str(", "); body.push_str(", ");
} }
text.push_str(match context { body.push_str(match context {
Context::Normal => "video list", Context::Normal => "video list",
Context::Info => "video overview", Context::Info => "video overview",
Context::Description => "video description", Context::Description => "video description",
@@ -449,11 +456,18 @@ impl Bindings {
Context::Help => "help", Context::Help => "help",
}); });
} }
text.push_str("]\n"); body.push(']');
text.push_line(Line::from(vec![
Span::raw(definition.name).bold(),
Span::raw(body),
]));
} }
text.push_str("\nText input inserts literal characters; normal-mode commands do not run there.\n\ text.push_line("");
Shared overview/description bindings cycle overview → description → overview; distinct bindings open either view directly.\n\ text.push_line(
Preview close takes priority over history back. Description scrolling takes priority over video selection on shared bindings.\n"); "Text input inserts literal characters; normal-mode commands do not run there.",
);
text.push_line("Shared overview/description bindings cycle overview → description → overview; distinct bindings open either view directly.");
text.push_line("Preview close takes priority over history back. Description scrolling takes priority over video selection on shared bindings.");
text text
} }
} }
@@ -519,6 +533,60 @@ mod tests {
bindings.resolve(context, KeyEvent::new(code, modifiers)) bindings.resolve(context, KeyEvent::new(code, modifiers))
} }
#[test]
fn help_headings_are_bold_but_bindings_and_descriptions_are_not() {
use ratatui::{
buffer::Buffer,
layout::Rect,
style::Modifier,
widgets::{Paragraph, Widget, Wrap},
};
let bindings = configured("[keys]\napp.interrupt = 'f24'\n").unwrap();
for width in [21, 64] {
let area = Rect::new(0, 0, width, 8);
let mut buffer = Buffer::empty(area);
Paragraph::new(bindings.help_text())
.wrap(Wrap { trim: false })
.render(area, &mut buffer);
assert!(buffer[(0, 0)].modifier.contains(Modifier::BOLD));
for x in 0.."app.interrupt".len() as u16 {
assert!(buffer[(x, 2)].modifier.contains(Modifier::BOLD));
}
assert!(
!buffer[("app.interrupt".len() as u16, 2)]
.modifier
.contains(Modifier::BOLD)
);
let mut found_binding = false;
for y in 2..area.height {
let row = (0..width)
.map(|x| buffer[(x, y)].symbol())
.collect::<String>();
if y > 2 && row.starts_with("app.quit") {
break;
}
let body_start = if y == 2 {
"app.interrupt".len() as u16
} else {
0
};
for x in body_start..width {
assert!(!buffer[(x, y)].modifier.contains(Modifier::BOLD));
}
if let Some(x) = row.find("F24") {
found_binding = true;
for x in x as u16..x as u16 + 3 {
assert!(!buffer[(x, y)].modifier.contains(Modifier::BOLD));
}
}
}
assert!(
found_binding,
"configured binding must remain visible after wrapping"
);
}
}
#[test] #[test]
fn contextual_cycle_and_direct_preview_bindings() { fn contextual_cycle_and_direct_preview_bindings() {
let defaults = Bindings::default(); let defaults = Bindings::default();