aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-30 23:58:02 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-31 00:02:08 +0300
commitbdc189e32e7c9dd61811fe17e50ac171ca94c647 (patch)
tree730e0e14d571e9bb693ad583b02e24622e4258d4 /src
parentfc4e1bbf6b0fa2ec37445a5b72356514c7c6be6a (diff)
feat: support custom command descriptions
refs #362
Diffstat (limited to 'src')
-rw-r--r--src/app/input/navigate.rs2
-rw-r--r--src/app/input/terminal.rs2
-rw-r--r--src/config/keybinds.rs24
-rw-r--r--src/ui.rs76
-rw-r--r--src/ui/keybind_help.rs122
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())
+ );
+ }
}
diff --git a/src/ui.rs b/src/ui.rs
index c9999a9a..b9d9827a 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -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),
]),
));
}