feat: emphasize keybinding overview headings
This commit is contained in:
@@ -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
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user