diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-30 23:58:02 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-31 00:02:08 +0300 |
| commit | bdc189e32e7c9dd61811fe17e50ac171ca94c647 (patch) | |
| tree | 730e0e14d571e9bb693ad583b02e24622e4258d4 /src | |
| parent | fc4e1bbf6b0fa2ec37445a5b72356514c7c6be6a (diff) | |
feat: support custom command descriptions
refs #362
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/input/navigate.rs | 2 | ||||
| -rw-r--r-- | src/app/input/terminal.rs | 2 | ||||
| -rw-r--r-- | src/config/keybinds.rs | 24 | ||||
| -rw-r--r-- | src/ui.rs | 76 | ||||
| -rw-r--r-- | src/ui/keybind_help.rs | 122 |
5 files changed, 163 insertions, 63 deletions
diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs index 6a7e8d7c..8d09c00a 100644 --- a/src/app/input/navigate.rs +++ b/src/app/input/navigate.rs @@ -1781,6 +1781,7 @@ last_pane = "prefix+tab" label: "prefix+m".into(), command, action: crate::config::CustomCommandAction::Shell, + description: None, }]; app.handle_key(TerminalKey::new( @@ -1841,6 +1842,7 @@ last_pane = "prefix+tab" label: "prefix+m".into(), command, action: crate::config::CustomCommandAction::Pane, + description: None, }]; app.handle_key(TerminalKey::new( diff --git a/src/app/input/terminal.rs b/src/app/input/terminal.rs index 5447e2b0..401a981d 100644 --- a/src/app/input/terminal.rs +++ b/src/app/input/terminal.rs @@ -815,6 +815,7 @@ mod tests { label: "ctrl+alt+g".into(), command, action: crate::config::CustomCommandAction::Shell, + description: None, }]; app.handle_terminal_key(TerminalKey::new( @@ -863,6 +864,7 @@ mod tests { label: "ctrl+alt+g".into(), command: "printf direct-pane".into(), action: crate::config::CustomCommandAction::Pane, + description: None, }]; app.handle_terminal_key(TerminalKey::new( diff --git a/src/config/keybinds.rs b/src/config/keybinds.rs index 401154bc..6ae8c4a1 100644 --- a/src/config/keybinds.rs +++ b/src/config/keybinds.rs @@ -63,6 +63,8 @@ pub struct CommandKeybindConfig { /// Command execution mode. Default: "shell". #[serde(rename = "type")] pub action_type: CommandKeybindType, + /// Optional user-defined description for this custom command. + pub description: Option<String>, } impl Default for CommandKeybindConfig { @@ -71,6 +73,7 @@ impl Default for CommandKeybindConfig { key: BindingConfig::empty(), command: String::new(), action_type: CommandKeybindType::Shell, + description: None, } } } @@ -238,6 +241,7 @@ pub struct CustomCommandKeybind { pub label: String, pub command: String, pub action: CustomCommandAction, + pub description: Option<String>, } /// Parsed keybinds for Herdr actions. @@ -536,6 +540,7 @@ impl Config { label, command: command.command.clone(), action, + description: command.description.clone(), }); } @@ -1668,4 +1673,23 @@ new_workspace = "prefix+n" diag.contains("kept keys.new_workspace") && diag.contains("disabled keys.next_tab") })); } + + #[test] + fn custom_command_with_description_parses() { + let config: Config = toml::from_str( + r#" +[[keys.command]] +key = "prefix+y" +command = "echo hello" +description = "say hello" +"#, + ) + .unwrap(); + let keybinds = config.keybinds(); + assert_eq!(keybinds.custom_commands.len(), 1); + assert_eq!( + keybinds.custom_commands[0].description, + Some("say hello".to_string()) + ); + } } @@ -1019,23 +1019,79 @@ mod tests { .1 .clone(); - assert!(workspace_tab.contains(&("unset".to_string(), "previous workspace"))); - assert!(workspace_tab.contains(&("unset".to_string(), "next workspace"))); - assert!(workspace_tab.contains(&("unset".to_string(), "previous agent"))); - assert!(workspace_tab.contains(&("unset".to_string(), "next agent"))); - assert!(workspace_tab.contains(&("unset".to_string(), "focus agent 1-9"))); - assert!(workspace_tab.contains(&("unset".to_string(), "switch workspace 1-9"))); + assert!(workspace_tab + .iter() + .any(|(key, label)| key == "unset" && label.as_ref() == "previous workspace")); + assert!(workspace_tab + .iter() + .any(|(key, label)| key == "unset" && label.as_ref() == "next workspace")); + assert!(workspace_tab + .iter() + .any(|(key, label)| key == "unset" && label.as_ref() == "previous agent")); + assert!(workspace_tab + .iter() + .any(|(key, label)| key == "unset" && label.as_ref() == "next agent")); + assert!(workspace_tab + .iter() + .any(|(key, label)| key == "unset" && label.as_ref() == "focus agent 1-9")); + assert!(workspace_tab + .iter() + .any(|(key, label)| key == "unset" && label.as_ref() == "switch workspace 1-9")); assert!(panes .iter() - .any(|(key, label)| key == "prefix+h" && *label == "focus pane left")); + .any(|(key, label)| key == "prefix+h" && label.as_ref() == "focus pane left")); assert!(panes .iter() - .any(|(key, label)| key == "prefix+j" && *label == "focus pane down")); + .any(|(key, label)| key == "prefix+j" && label.as_ref() == "focus pane down")); assert!(panes .iter() - .any(|(key, label)| key == "prefix+k" && *label == "focus pane up")); + .any(|(key, label)| key == "prefix+k" && label.as_ref() == "focus pane up")); assert!(panes .iter() - .any(|(key, label)| key == "prefix+l" && *label == "focus pane right")); + .any(|(key, label)| key == "prefix+l" && label.as_ref() == "focus pane right")); + } + + #[test] + fn keybind_help_shows_custom_command_descriptions() { + let mut app = crate::app::state::AppState::test_new(); + app.keybinds.custom_commands = vec![ + crate::config::CustomCommandKeybind { + bindings: crate::config::ActionKeybinds::prefix("alt+g"), + label: "prefix+alt+g".to_string(), + command: "lazygit".to_string(), + action: crate::config::CustomCommandAction::Pane, + description: Some("open lazygit".to_string()), + }, + crate::config::CustomCommandKeybind { + bindings: crate::config::ActionKeybinds::prefix("alt+h"), + label: "prefix+alt+h".to_string(), + command: "echo hello".to_string(), + action: crate::config::CustomCommandAction::Shell, + description: None, + }, + ]; + + let groups = keybind_help_groups(&app); + let custom = groups + .iter() + .find(|(name, _)| *name == "custom") + .expect("custom group") + .1 + .clone(); + assert!(custom + .iter() + .any(|(key, label)| key == "prefix+alt+g" && label.as_ref() == "open lazygit")); + assert!(custom + .iter() + .any(|(key, label)| key == "prefix+alt+h" && label.as_ref() == "custom command")); + + let rendered_help = keybind_help_lines(&app) + .into_iter() + .flat_map(|(_, line)| line.spans) + .map(|span| span.content.into_owned()) + .collect::<Vec<_>>() + .join(""); + assert!(rendered_help.contains("open lazygit")); + assert!(rendered_help.contains("custom command")); } } diff --git a/src/ui/keybind_help.rs b/src/ui/keybind_help.rs index 4fb559c6..76a68f0d 100644 --- a/src/ui/keybind_help.rs +++ b/src/ui/keybind_help.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use ratatui::{ layout::{Constraint, Layout, Rect}, style::{Modifier, Style}, @@ -14,6 +16,13 @@ use super::widgets::{ }; use crate::app::AppState; +pub(super) type HelpEntry = (String, Cow<'static, str>); +pub(super) type HelpGroup = (&'static str, Vec<HelpEntry>); + +fn help_entry(key: impl Into<String>, label: &'static str) -> HelpEntry { + (key.into(), Cow::Borrowed(label)) +} + fn keybind_label(bindings: &crate::config::ActionKeybinds) -> String { bindings.label().unwrap_or_else(|| "unset".to_string()) } @@ -41,24 +50,22 @@ fn indexed_label(bindings: &[crate::config::IndexedKeybind]) -> String { } } -pub(super) fn keybind_help_groups( - app: &AppState, -) -> Vec<(&'static str, Vec<(String, &'static str)>)> { +pub(super) fn keybind_help_groups(app: &AppState) -> Vec<HelpGroup> { let kb = &app.keybinds; let mut groups = Vec::new(); groups.push(( "global", vec![ - ( + help_entry( crate::config::format_key_combo((app.prefix_code, app.prefix_mods)), "prefix mode", ), - (keybind_label(&kb.help), "keybinds"), - (keybind_label(&kb.settings), "settings"), - (keybind_label(&kb.detach), "detach"), - (keybind_label(&kb.reload_config), "reload config"), - ( + help_entry(keybind_label(&kb.help), "keybinds"), + help_entry(keybind_label(&kb.settings), "settings"), + help_entry(keybind_label(&kb.detach), "detach"), + help_entry(keybind_label(&kb.reload_config), "reload config"), + help_entry( keybind_label(&kb.open_notification_target), "open notification target", ), @@ -68,8 +75,8 @@ pub(super) fn keybind_help_groups( groups.push(( "navigation", vec![ - ("esc".to_string(), "back"), - ( + help_entry("esc", "back"), + help_entry( format!( "{} / {}", keybind_label(&kb.navigate.workspace_up), @@ -77,7 +84,7 @@ pub(super) fn keybind_help_groups( ), "workspace list", ), - ( + help_entry( format!( "{} / {} / {} / {} / left / right", keybind_label(&kb.navigate.pane_left), @@ -87,59 +94,59 @@ pub(super) fn keybind_help_groups( ), "move focus", ), - ("tab / shift+tab".to_string(), "cycle pane"), - ("enter".to_string(), "open workspace"), - ("1..9".to_string(), "switch workspace"), + help_entry("tab / shift+tab", "cycle pane"), + help_entry("enter", "open workspace"), + help_entry("1..9", "switch workspace"), ], )); let workspace_tab = vec![ - (keybind_label(&kb.workspace_picker), "workspace navigation"), - (keybind_label(&kb.goto), "session navigator"), - (keybind_label(&kb.new_workspace), "new workspace"), - (keybind_label(&kb.new_worktree), "new worktree"), - (keybind_label(&kb.open_worktree), "open worktree"), - ( + help_entry(keybind_label(&kb.workspace_picker), "workspace navigation"), + help_entry(keybind_label(&kb.goto), "session navigator"), + help_entry(keybind_label(&kb.new_workspace), "new workspace"), + help_entry(keybind_label(&kb.new_worktree), "new worktree"), + help_entry(keybind_label(&kb.open_worktree), "open worktree"), + help_entry( keybind_label(&kb.remove_worktree), "delete worktree checkout", ), - (keybind_label(&kb.rename_workspace), "rename workspace"), - (keybind_label(&kb.close_workspace), "close workspace"), - (keybind_label(&kb.previous_workspace), "previous workspace"), - (keybind_label(&kb.next_workspace), "next workspace"), - (indexed_label(&kb.switch_workspace), "switch workspace 1-9"), - (keybind_label(&kb.previous_agent), "previous agent"), - (keybind_label(&kb.next_agent), "next agent"), - (indexed_label(&kb.focus_agent), "focus agent 1-9"), - (keybind_label(&kb.new_tab), "new tab"), - (keybind_label(&kb.rename_tab), "rename tab"), - (keybind_label(&kb.previous_tab), "previous tab"), - (keybind_label(&kb.next_tab), "next tab"), - (indexed_label(&kb.switch_tab), "switch tab 1-9"), - (keybind_label(&kb.close_tab), "close tab"), + help_entry(keybind_label(&kb.rename_workspace), "rename workspace"), + help_entry(keybind_label(&kb.close_workspace), "close workspace"), + help_entry(keybind_label(&kb.previous_workspace), "previous workspace"), + help_entry(keybind_label(&kb.next_workspace), "next workspace"), + help_entry(indexed_label(&kb.switch_workspace), "switch workspace 1-9"), + help_entry(keybind_label(&kb.previous_agent), "previous agent"), + help_entry(keybind_label(&kb.next_agent), "next agent"), + help_entry(indexed_label(&kb.focus_agent), "focus agent 1-9"), + help_entry(keybind_label(&kb.new_tab), "new tab"), + help_entry(keybind_label(&kb.rename_tab), "rename tab"), + help_entry(keybind_label(&kb.previous_tab), "previous tab"), + help_entry(keybind_label(&kb.next_tab), "next tab"), + help_entry(indexed_label(&kb.switch_tab), "switch tab 1-9"), + help_entry(keybind_label(&kb.close_tab), "close tab"), ]; groups.push(("workspaces / tabs", workspace_tab)); let panes = vec![ - (keybind_label(&kb.split_vertical), "split vertical"), - (keybind_label(&kb.split_horizontal), "split horizontal"), - (keybind_label(&kb.close_pane), "close pane"), - (keybind_label(&kb.rename_pane), "rename pane"), - (keybind_label(&kb.edit_scrollback), "edit scrollback"), - (keybind_label(&kb.copy_mode), "copy mode"), - (keybind_label(&kb.zoom), "zoom pane"), - (keybind_label(&kb.resize_mode), "resize mode"), - (keybind_label(&kb.toggle_sidebar), "toggle sidebar"), - (keybind_label(&kb.focus_pane_left), "focus pane left"), - (keybind_label(&kb.focus_pane_down), "focus pane down"), - (keybind_label(&kb.focus_pane_up), "focus pane up"), - (keybind_label(&kb.focus_pane_right), "focus pane right"), - (keybind_label(&kb.cycle_pane_next), "cycle pane next"), - ( + help_entry(keybind_label(&kb.split_vertical), "split vertical"), + help_entry(keybind_label(&kb.split_horizontal), "split horizontal"), + help_entry(keybind_label(&kb.close_pane), "close pane"), + help_entry(keybind_label(&kb.rename_pane), "rename pane"), + help_entry(keybind_label(&kb.edit_scrollback), "edit scrollback"), + help_entry(keybind_label(&kb.copy_mode), "copy mode"), + help_entry(keybind_label(&kb.zoom), "zoom pane"), + help_entry(keybind_label(&kb.resize_mode), "resize mode"), + help_entry(keybind_label(&kb.toggle_sidebar), "toggle sidebar"), + help_entry(keybind_label(&kb.focus_pane_left), "focus pane left"), + help_entry(keybind_label(&kb.focus_pane_down), "focus pane down"), + help_entry(keybind_label(&kb.focus_pane_up), "focus pane up"), + help_entry(keybind_label(&kb.focus_pane_right), "focus pane right"), + help_entry(keybind_label(&kb.cycle_pane_next), "cycle pane next"), + help_entry( keybind_label(&kb.cycle_pane_previous), "cycle pane previous", ), - (keybind_label(&kb.last_pane), "last pane"), + help_entry(keybind_label(&kb.last_pane), "last pane"), ]; groups.push(("panes", panes)); @@ -148,7 +155,16 @@ pub(super) fn keybind_help_groups( "custom", kb.custom_commands .iter() - .map(|binding| (binding.label.clone(), "custom command")) + .map(|binding| { + ( + binding.label.clone(), + binding + .description + .clone() + .map(Cow::Owned) + .unwrap_or(Cow::Borrowed("custom command")), + ) + }) .collect(), )); } @@ -186,7 +202,7 @@ pub(crate) fn keybind_help_lines(app: &AppState) -> Vec<(usize, Line<'static>)> width, Line::from(vec![ Span::styled(padded_key, key_style), - Span::styled(label.to_string(), label_style), + Span::styled(label.into_owned(), label_style), ]), )); } |
