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
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,10 @@ Then run:
lazygitrs
```

Use `lazygitrs --commits` to start at the checked-out HEAD commit. Press
`Ctrl+G` to toggle between working-tree changes and HEAD (including from a
focused diff). If filters or pagination hide HEAD, it opens a separate HEAD
history view without clearing your filters. Remap or disable the shortcut with
`keybinding.universal.toggleWorkingTreeAndHead` (`"<c-g>"` by default; `""` disables it).
Use `lazygitrs --commits` to start at the checked-out HEAD commit. If filters
or pagination hide HEAD, it opens a separate HEAD history view without clearing
your filters. Use `2` and `4` to navigate between Files and Commits; `Ctrl+G`
in Files generates a commit message using `git.commit.generateCommand`.

### File tree navigation

Expand Down
136 changes: 132 additions & 4 deletions npm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,52 @@ Then run:
lazygitrs
```

Use `lazygitrs --commits` to start at the checked-out HEAD commit. If filters
or pagination hide HEAD, it opens a separate HEAD history view without clearing
your filters. Use `2` and `4` to navigate between Files and Commits; `Ctrl+G`
in Files generates a commit message using `git.commit.generateCommand`.

### File tree navigation

In Files, commit/stash file lists, and compare mode, press backtick (`` ` ``) to
toggle the tree view. With a tree active:

- `-` folds/unfolds the selected directory (including the root).
- `Enter` focuses the selected file or combined directory diff without changing
your normal/half/full layout; `Esc` returns to the file list.
- `,` / `.` select the parent / first visible child.
- `<` / `>` select the previous / next sibling, skipping nested descendants.
Hierarchy navigation also works while the diff is focused.

These shortcuts are configurable under `keybinding.universal` in your config:

```yaml
keybinding:
universal:
foldDirectory: "-"
treeParent: ","
treeChild: "."
treePrevSibling: "<"
treeNextSibling: ">"
```

Set a binding to `""` to disable it. Collapsed directories must be unfolded
before their children can be selected. Tree-navigation actions appear in `?`
only while a tree is active. The footer shows just the fold/unfold shortcut
(default `-`) in tree views, not hierarchy navigation or commit details.
`'` (apostrophe) toggles commit details, also shown on the panel’s top-right
border; explicitly remapping a tree action to `'` will override it while
the tree is active.

### Upgrade

Detects how you installed (brew / npm / bun / cargo / install.sh) and upgrades in place:

```sh
lazygitrs upgrade # latest
lazygitrs upgrade 0.0.32 # specific version
```

### What's different

- [x] **AI commit messages** — works with whatever agent you already use (claude, opencode, codex, or my minimal shim [modelcli](https://github.com/blankeos/modelcli)). Set `git.commit.generateCommand` (see [Configuration](#configuration)):
Expand All @@ -42,13 +88,13 @@ lazygitrs
git:
commit:
# Using claude
generateCommand: "claude -p 'Generate a conventional commit message for this diff.' --no-session-persistence"
generateCommand: "claude -p 'Generate a conventional commit message for this diff. Do not hard-wrap lines; one bullet per line; blank line between paragraphs.' --no-session-persistence"
# Using opencode
generateCommand: "opencode run 'Generate a conventional commit message for this diff.'"
generateCommand: "opencode run 'Generate a conventional commit message for this diff. Do not hard-wrap lines; one bullet per line; blank line between paragraphs.'"
# Using codex
generateCommand: "codex exec --ephemeral 'Generate a conventional commit message for this diff.'"
generateCommand: "codex exec --ephemeral 'Generate a conventional commit message for this diff. Do not hard-wrap lines; one bullet per line; blank line between paragraphs.'"
# Using modelcli
generateCommand: 'DIFF=$(git diff --cached) && modelcli "Generate a conventional commit message for this diff. Always provide a bulletpoint body. $DIFF"'
generateCommand: 'DIFF=$(git diff --cached) && modelcli "Generate a conventional commit message for this diff. Always provide a bulletpoint body. Do not hard-wrap lines; one bullet per line. $DIFF"'
```

- [x] **Side-by-side + unified diffs** with syntax highlighting by default and unified as well, no pager hacks needed
Expand All @@ -63,6 +109,31 @@ lazygitrs
- [x] `git diff/compare` (global `W`) and then asks what branch/commit A and B, has quick search.
- [x] `git rebase` (global `I`) and then asks rebase on top of what branch/commit.
- [x] 🎨 Themes + Theme-Picker!
- [x] **Grep diff contents** — `Ctrl-F` in Files / Commit Files / Compare searches hunk lines in-context, `Enter` jumps to the file in the current list.

### Image diffs

Selecting a PNG, JPEG, GIF (first frame), WebP, BMP, ICO, or TIFF file shows
**Before / After** previews in Kitty and Ghostty. Added/deleted files show the
available image full-width, labeled **Added** or **Deleted**. Staged previews use the index;
commit-file and ref comparisons use the corresponding Git blobs.

Other binaries, unsupported/corrupt images, files over 20 MiB, and unsupported
terminal environments keep the striped binary placeholder. Images are decoded
with memory/dimension limits and resized in the background. SVG and video
previews are not included yet.

Folder previews in the file tree mix text diffs with inline image sections.
Each image section is capped at 12 rows; visible sections load in the background
(two at a time) and offscreen pixels are released. This works in split/unified
and wrapped views, including commit/stash folders and ref-comparison folders.
Whole-commit overview buffers still retain binary placeholders. Inline images
require Kitty/Ghostty placeholder graphics; other terminals keep stripes.

Set `LAZYGITRS_IMAGE_PREVIEW=off` to disable image previews. tmux, screen, and
Zellij currently fall back to stripes. iTerm2/WezTerm and Sixel are experimental
(`LAZYGITRS_IMAGE_PREVIEW=experimental`); their overlay cleanup is not yet fully
verified. Nested editor launches avoid terminal capability queries.

### Configuration

Expand Down Expand Up @@ -96,6 +167,63 @@ lazygitrs ships with 30+ built-in color themes (Catppuccin, Dracula, Tokyo Night
}
```

### Editor integrations

<details>
<summary><strong>Helix</strong> — <code>Space G g</code> to open, <code>Space G f</code> for file history</summary>

Add to `~/.config/helix/config.toml` — capital `G` keeps the built-in `space g` changed-file picker intact:

```toml
[keys.normal.space.G]
g = [":insert-output lazygitrs", ":redraw"]
f = [":insert-output lazygitrs -f '%{file_path_absolute}'", ":redraw"]
```

Absolute path matters — `-f` resolves it to repo-relative (e.g. `apps/nextjs/next.config.ts` in a monorepo).

For `e` (edit back in hx) — `~/.config/lazygitrs/config.yml`:

```yaml
os:
editPreset: "helix"
```

For `o` (open), leave the default — OS opener (Finder for folders on macOS).

</details>

<details>
<summary><strong>Neovim (LazyVim / snacks.nvim)</strong> — <code>&lt;leader&gt;gg</code> to open, <code>&lt;leader&gt;gF</code> for file history</summary>

`Snacks.lazygit()` hardcodes `lazygit`, so use `Snacks.terminal` instead. In `~/.config/nvim/lua/plugins/snacks-lazygitrs.lua`:

```lua
return {
{
"folke/snacks.nvim",
opts = { lazygit = { configure = false } },
keys = {
{ "<leader>gg", function() Snacks.terminal({ "lazygitrs" }, { cwd = LazyVim.root.git(), win = { style = "lazygit" } }) end, desc = "Lazygitrs" },
{ "<leader>gF", function() Snacks.terminal({ "lazygitrs", "-f", vim.fn.expand("%:p") }, { cwd = LazyVim.root.git(), win = { style = "lazygit" } }) end, desc = "Lazygitrs file history" },
},
},
}
```

Restart nvim (or `:Lazy reload snacks.nvim`) to pick it up.

For `e` (edit back in nvim) — `~/.config/lazygitrs/config.yml`:

```yaml
os:
editPreset: "nvim"
```

For `o` (open), leave the default — it uses the OS opener (Finder for folders on macOS).

</details>

<!-- GEN_BENCHMARKS_START -->

### Benchmarks
Expand Down
13 changes: 10 additions & 3 deletions src/config/keybindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,6 @@ pub struct UniversalKeybinding {
pub reset_side_panel: String,
#[serde(rename = "toggleDiffViewLayout")]
pub toggle_diff_view_layout: String,
#[serde(rename = "toggleWorkingTreeAndHead")]
pub toggle_working_tree_and_head: String,
#[serde(rename = "foldDirectory")]
pub fold_directory: String,
#[serde(rename = "treeParent")]
Expand Down Expand Up @@ -193,7 +191,6 @@ impl Default for UniversalKeybinding {
main_panel_full: "<a-j>".into(),
reset_side_panel: "<a-r>".into(),
toggle_diff_view_layout: "\\".into(),
toggle_working_tree_and_head: "<c-g>".into(),
fold_directory: "-".into(),
tree_parent: ",".into(),
tree_child: ".".into(),
Expand Down Expand Up @@ -471,6 +468,16 @@ pub fn parse_key(s: &str) -> Option<KeyEvent> {
mod tests {
use super::*;

#[test]
fn legacy_head_toggle_config_does_not_override_ai_generation() {
let config: KeybindingConfig =
serde_yaml::from_str("universal:\n toggleWorkingTreeAndHead: '<c-g>'\n").unwrap();
assert_eq!(config.files.generate_ai_commit, "<c-g>");
assert_eq!(config.commit_message.ai_generate, "<c-g>");
let serialized = serde_yaml::to_string(&config).unwrap();
assert!(!serialized.contains("toggleWorkingTreeAndHead"));
}

#[test]
fn test_tree_keybindings_defaults_and_deserialization() {
let default_config = KeybindingConfig::default();
Expand Down
Loading
Loading