aboutsummaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
authorcorrius <alvaro.correa@taxdown.es>2026-07-24 21:52:17 +0200
committerGitHub <noreply@github.com>2026-07-24 22:52:17 +0300
commit38d2b0780c50e4773d6948660873386261cde71d (patch)
treeb899dd7ce1dfd1a70650be0209a48132c1539a25 /src/ui
parente9b22084e03880f43d1f1daaf5d9f45c717b77c4 (diff)
feat: filter keybind help (#1832)
* feat: filter keybind help refs #1825 * fix: recognize shifted slash in keybind help --------- Co-authored-by: Ogulcan Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/keybind_help.rs152
1 files changed, 132 insertions, 20 deletions
diff --git a/src/ui/keybind_help.rs b/src/ui/keybind_help.rs
index 8385af36..25c999fa 100644
--- a/src/ui/keybind_help.rs
+++ b/src/ui/keybind_help.rs
@@ -181,6 +181,26 @@ pub(super) fn keybind_help_groups(app: &AppState) -> Vec<HelpGroup> {
groups
}
+fn filter_keybind_help_groups(groups: Vec<HelpGroup>, query: &str) -> Vec<HelpGroup> {
+ if query.is_empty() {
+ return groups;
+ }
+
+ let query = query.to_lowercase();
+ groups
+ .into_iter()
+ .filter_map(|(group, entries)| {
+ let entries = entries
+ .into_iter()
+ .filter(|(key, label)| {
+ key.to_lowercase().contains(&query) || label.to_lowercase().contains(&query)
+ })
+ .collect::<Vec<_>>();
+ (!entries.is_empty()).then_some((group, entries))
+ })
+ .collect()
+}
+
pub(crate) fn keybind_help_lines(app: &AppState) -> Vec<(usize, Line<'static>)> {
let heading_style = Style::default()
.fg(app.palette.accent)
@@ -190,7 +210,7 @@ pub(crate) fn keybind_help_lines(app: &AppState) -> Vec<(usize, Line<'static>)>
.add_modifier(Modifier::BOLD);
let label_style = Style::default().fg(app.palette.text);
- let groups = keybind_help_groups(app);
+ let groups = filter_keybind_help_groups(keybind_help_groups(app), &app.keybind_help.query);
let key_width = groups
.iter()
.flat_map(|(_, entries)| entries.iter().map(|(key, _)| key.chars().count()))
@@ -199,6 +219,17 @@ pub(crate) fn keybind_help_lines(app: &AppState) -> Vec<(usize, Line<'static>)>
let mut lines = Vec::new();
+ if groups.is_empty() {
+ let message = " no matching keybinds";
+ return vec![(
+ message.chars().count(),
+ Line::from(Span::styled(
+ message,
+ Style::default().fg(app.palette.overlay1),
+ )),
+ )];
+ }
+
for (group, entries) in groups {
lines.push((
group.len() + 1,
@@ -240,17 +271,38 @@ pub(super) fn render_keybind_help_overlay(app: &AppState, frame: &mut Frame) {
frame,
release_notes_close_button_rect(header_rows[0]),
Some("esc"),
- "close",
+ if app.keybind_help.search_focused {
+ "back"
+ } else {
+ "close"
+ },
Style::default()
.fg(panel_contrast_fg(&app.palette))
.bg(app.palette.accent)
.add_modifier(Modifier::BOLD),
);
- frame.render_widget(
- Paragraph::new(" available commands and configured shortcuts")
- .style(Style::default().fg(app.palette.overlay1)),
- header_rows[1],
- );
+ let search_line = if app.keybind_help.search_focused {
+ Line::from(vec![
+ Span::styled(
+ " / ",
+ Style::default()
+ .fg(app.palette.accent)
+ .add_modifier(Modifier::BOLD),
+ ),
+ Span::styled(
+ app.keybind_help.query.as_str(),
+ Style::default()
+ .fg(app.palette.text)
+ .add_modifier(Modifier::BOLD),
+ ),
+ ])
+ } else {
+ Line::from(Span::styled(
+ " press / to filter by command or shortcut",
+ Style::default().fg(app.palette.overlay0),
+ ))
+ };
+ frame.render_widget(Paragraph::new(search_line), header_rows[1]);
let body_area = stack.content;
let metrics = crate::pane::ScrollMetrics {
@@ -292,17 +344,77 @@ pub(super) fn render_keybind_help_overlay(app: &AppState, frame: &mut Frame) {
);
}
- frame.render_widget(
- Paragraph::new(Line::from(vec![
- Span::styled(" scroll ", Style::default().fg(app.palette.overlay0)),
- Span::styled("wheel ↑↓", Style::default().fg(app.palette.text)),
- Span::styled(" · ", Style::default().fg(app.palette.overlay0)),
- Span::styled("jump", Style::default().fg(app.palette.overlay0)),
- Span::styled(" pgup / pgdn ", Style::default().fg(app.palette.text)),
- Span::styled(" · ", Style::default().fg(app.palette.overlay0)),
- Span::styled("close", Style::default().fg(app.palette.overlay0)),
- Span::styled(" esc / enter ", Style::default().fg(app.palette.text)),
- ])),
- stack.footer.unwrap_or_default(),
- );
+ let footer = if app.keybind_help.search_focused {
+ Line::from(vec![
+ Span::styled(" filter ", Style::default().fg(app.palette.overlay0)),
+ Span::styled("type/backspace", Style::default().fg(app.palette.text)),
+ Span::styled(" · ", Style::default().fg(app.palette.overlay0)),
+ Span::styled("clear ", Style::default().fg(app.palette.overlay0)),
+ Span::styled("ctrl+u", Style::default().fg(app.palette.text)),
+ Span::styled(" · ", Style::default().fg(app.palette.overlay0)),
+ Span::styled("scroll ", Style::default().fg(app.palette.overlay0)),
+ Span::styled("↑↓/pgup/pgdn", Style::default().fg(app.palette.text)),
+ Span::styled(" · ", Style::default().fg(app.palette.overlay0)),
+ Span::styled("back ", Style::default().fg(app.palette.overlay0)),
+ Span::styled("esc", Style::default().fg(app.palette.text)),
+ ])
+ } else {
+ Line::from(vec![
+ Span::styled(" search ", Style::default().fg(app.palette.overlay0)),
+ Span::styled("/", Style::default().fg(app.palette.text)),
+ Span::styled(" · ", Style::default().fg(app.palette.overlay0)),
+ Span::styled("scroll ", Style::default().fg(app.palette.overlay0)),
+ Span::styled("j/k/↑↓/pgup/pgdn", Style::default().fg(app.palette.text)),
+ Span::styled(" · ", Style::default().fg(app.palette.overlay0)),
+ Span::styled("close ", Style::default().fg(app.palette.overlay0)),
+ Span::styled("esc/enter", Style::default().fg(app.palette.text)),
+ ])
+ };
+ frame.render_widget(Paragraph::new(footer), stack.footer.unwrap_or_default());
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn groups() -> Vec<HelpGroup> {
+ vec![
+ (
+ "workspaces / tabs",
+ vec![
+ help_entry("w", "workspace navigation"),
+ help_entry("c", "new tab"),
+ ],
+ ),
+ (
+ "panes",
+ vec![
+ help_entry("v", "split vertical"),
+ help_entry("x", "close pane"),
+ ],
+ ),
+ ]
+ }
+
+ #[test]
+ fn keybind_help_filter_matches_labels_case_insensitively() {
+ let filtered = filter_keybind_help_groups(groups(), "WoRk");
+
+ assert_eq!(filtered.len(), 1);
+ assert_eq!(filtered[0].0, "workspaces / tabs");
+ assert_eq!(filtered[0].1.len(), 1);
+ assert_eq!(filtered[0].1[0].1, "workspace navigation");
+ }
+
+ #[test]
+ fn keybind_help_filter_matches_shortcuts_without_matching_group_headings() {
+ let filtered = filter_keybind_help_groups(groups(), "x");
+
+ assert_eq!(filtered.len(), 1);
+ assert_eq!(filtered[0].0, "panes");
+ assert_eq!(filtered[0].1.len(), 1);
+ assert_eq!(filtered[0].1[0].1, "close pane");
+
+ assert!(filter_keybind_help_groups(groups(), "panes").is_empty());
+ }
}