Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,51 @@ only while a tree is active. The footer shows just the fold/unfold shortcut
border; explicitly remapping a tree action to `'` will override it while
the tree is active.

### Shell command prompt

Press `:` in the normal view or compare mode to run a shell command from the
repository root without leaving the TUI. Remap the prompt shortcut in your config,
or set it to `""` to disable it:

```yaml
keybinding:
universal:
customCommandPrompt: ":" # e.g. "<c-x>" to remap; "" to disable
```

The prompt uses `$SHELL`, falling back to `sh` when it is unset or empty. Bash
loads `~/.bash_aliases` and `~/.bashrc` and enables alias expansion; zsh keeps
native `.zshenv` startup (including changes to `ZDOTDIR`) and then loads
`${ZDOTDIR:-$HOME}/.zshrc`. Fish keeps its native startup configuration.
Aliases and functions defined there can be used, but a noninteractive guard in
your rc file may skip their definitions. Definitions that exist only in your
current interactive shell are not inherited. Bash, zsh, and Fish restore the
repository root after startup and clear positional arguments (`$argv` in Fish)
before evaluating your command, even if startup changed them. Paths and commands
are passed as literal arguments or environment values, not interpolated into
wrapper code. Fish uses a Fish-native wrapper; other shells receive a direct
native `-c` invocation (their startup may change the working directory). Use
your shell's syntax rather than POSIX syntax.

Commands run asynchronously and **noninteractively** with no input/TTY: editors,
password prompts, and other interactive programs are not supported here. Press
`Esc` while a command is running to cancel it; commands time out after five
minutes. Output includes the exit status, stdout, and stderr, with a 1 MiB capture
cap per stream (excess output is truncated). Background children are terminated
when the job ends, including on completion, cancellation, or timeout; this is
not a way to launch persistent background services. Processes that explicitly
create their own process group or session can escape cleanup; this prompt is not a
sandbox. The process runner currently requires Unix (macOS/Linux).

Input preserves pasted newlines and quoted spacing; `Enter` executes the whole
command and `Esc` dismisses without running it. Scroll command results with
`j`/`k`, arrow keys, the mouse wheel, `PgUp`/`PgDn`, or `g`/`G`; `y` copies the result and
`Esc`/`Enter` closes it. The command log retains a bounded output preview.

Configured `customCommands` still use `sh -c` rather than the prompt's `$SHELL`
and rc-file loading, but now run asynchronously with the same cancellation,
timeout, and output limits.

### Upgrade

Detects how you installed (brew / npm / bun / cargo / install.sh) and upgrades in place:
Expand Down Expand Up @@ -144,6 +189,7 @@ Persisted State lives at `~/.local/state/lazygitrs/state.yml` and `~/.local/stat
**New config properties:**

- `git.commit.generateCommand` — shell command for AI-generated commit messages. See [What's different](#whats-different) for examples.
- `keybinding.universal.customCommandPrompt` — shell prompt shortcut (`":"` by default; `""` disables it). See [Shell command prompt](#shell-command-prompt).
- `~/.config/lazygitrs/themes/*.json` — drop custom theme files here. See [Themes](#themes).

### Themes
Expand Down
40 changes: 40 additions & 0 deletions src/config/keybindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ pub struct UniversalKeybinding {
pub start_search: String,
#[serde(rename = "optionMenu")]
pub option_menu: String,
#[serde(rename = "customCommandPrompt")]
pub custom_command_prompt: String,
pub edit: String,
#[serde(rename = "openFile")]
pub open_file: String,
Expand Down Expand Up @@ -165,6 +167,7 @@ impl Default for UniversalKeybinding {
prev_match: "N".into(),
start_search: "/".into(),
option_menu: "x".into(),
custom_command_prompt: ":".into(),
edit: "e".into(),
open_file: "o".into(),
scroll_up_main: "<pgup>".into(),
Expand Down Expand Up @@ -468,6 +471,43 @@ pub fn parse_key(s: &str) -> Option<KeyEvent> {
mod tests {
use super::*;

#[test]
fn custom_command_prompt_defaults_when_omitted() {
assert_eq!(
KeybindingConfig::default().universal.custom_command_prompt,
":"
);
for yaml in ["{}", "universal: {}", "universal:\n optionMenu: m"] {
let parsed: KeybindingConfig = serde_yaml::from_str(yaml).expect("valid yaml");
assert_eq!(parsed.universal.custom_command_prompt, ":");
assert_eq!(
parse_key(&parsed.universal.custom_command_prompt),
Some(KeyEvent::new(KeyCode::Char(':'), KeyModifiers::NONE))
);
}
}

#[test]
fn custom_command_prompt_can_be_remapped() {
let parsed: KeybindingConfig =
serde_yaml::from_str("universal:\n customCommandPrompt: '<c-x>'").expect("valid yaml");
assert_eq!(parsed.universal.custom_command_prompt, "<c-x>");
assert_eq!(
parse_key(&parsed.universal.custom_command_prompt),
Some(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL))
);
assert_eq!(parsed.universal.start_search, "/");
}

#[test]
fn custom_command_prompt_can_be_disabled() {
let parsed: KeybindingConfig =
serde_yaml::from_str("universal:\n customCommandPrompt: ''").expect("valid yaml");
assert!(parsed.universal.custom_command_prompt.is_empty());
assert_eq!(parse_key(&parsed.universal.custom_command_prompt), None);
assert_eq!(parsed.universal.start_search, "/");
}

#[test]
fn legacy_head_toggle_config_does_not_override_ai_generation() {
let config: KeybindingConfig =
Expand Down
Loading
Loading