Skip to content
gwmgwmgwmv1.10.0

Configurable launchers (git TUI and review)

Added by #75 / #76; the embedded PTY overlay landed in #35.

Two configurable launchers, the git TUI (lazygit by default) and the review tool, are driven by user-configurable .gwm.toml sections sharing the same mini-API. Both take a command template string, substitute placeholders, split with shell-words, and exec the result with cwd = worktree.path.

Each launcher has two bindings (issue #35 / #290) that share the same resolved command but differ in how the child is hosted:

Key Action slug How it runs
l lazygit_pty git TUI in an embedded PTY overlay (~90 % of the screen)
L lazygit_fullscreen git TUI fullscreen, honours [git_tui] fullscreen
r review_pty review tool in an embedded PTY overlay (~90 % of the screen)
R review_fullscreen review tool fullscreen, honours [review] fullscreen

All four slugs are rebindable under [tui.keys].

lazygit running in the embedded PTY overlay, the worktree list still framing it

The PTY variants run the launcher inside the TUI: no alt-screen swap, no suspend. The child program is driven by a real pseudo-terminal (portable-pty) and rendered as a tui-term widget in a modal sized to roughly 90 % × 90 % of the terminal. The worktree list stays visible behind the overlay border.

  • Keystrokes are forwarded straight to the child, so lazygit / your review tool behave exactly as they do in a normal terminal.
  • Esc closes the overlay and returns to the list. Inside lazygit, its own q quits lazygit, and the overlay auto-closes when the child exits.
  • The PTY variants ignore the section’s fullscreen field. That field only governs the fullscreen bindings below. The PTY overlay is always embedded.

The L / R bindings keep the original launch model. They read the same resolved command but host the child outside the overlay:

Step What happens
1 Read the [git_tui] or [review] section from .gwm.toml (defaults below).
2 Resolve placeholders: {path}, {base}, {head}, {diff}.
3 Split the result into argv with shell-words (handles quoting like a POSIX shell).
4 Probe argv[0] against $PATH. Missing binary → status-bar error, no spawn, no flicker.
5 If fullscreen = true → suspend the TUI (raw mode off, alt-screen left), spawn Command::status(), restore on exit.
6 If fullscreen = false → keep the TUI in the alt-screen, spawn Command::output(), block until exit, drop the first line of stderr on the status bar.

The {diff} placeholder is lazy: gwm only materialises a tempfile holding git diff {base}..{head} when the template references {diff}. The tempfile’s lifetime is bound to the spawned process (its Drop impl unlinks on completion), so the reviewer always sees a consistent snapshot.

fullscreen = false is synchronous. The TUI is unresponsive until the child process exits: Command::output() waits for it. Fine for quick print-only tools (claude --print, gh pr view --web); pick fullscreen = true for anything long-running so the TUI is properly suspended and your terminal stays usable.

Default: lazygit -p {path}, fullscreen.

[git_tui]
# command template (single placeholder: {path}). Pre-v0.6 behaviour
# is the default — your existing configs see zero change.
command = "lazygit -p {path}"
# suspend the TUI for the call (recommended for TUIs that own the alt-screen)
fullscreen = true

Any tool that takes a worktree path works:

[git_tui]
command = "gitui -d {path}" # gitui
[git_tui]
command = "tig --all" # tig (run inside the worktree)
fullscreen = true
[git_tui]
command = "code -n {path}" # VS Code in a new window
fullscreen = false # IDE forks, don't suspend

No default: both r and R are inert until configured.

Two equivalent ways to configure it:

[review]
command = "claude --print 'review the diff {base}..{head}'"
fullscreen = false
default_base = "main" # optional, see "base resolution chain" below
[review]
tool = "lumen" # shorthand for the table below
Preset Expanded command fullscreen
lumen lumen diff {base}..{head} true
claude claude --print 'review the diff {base}..{head}' false
codex codex review {base}..{head} false
aider aider --message 'review {base}..{head}' true
gh gh pr view --web false

If both command and tool are set in the same block, command wins and the TUI shows a one-shot warning at startup (“your tool = X is shadowed by command”) so you notice the dead config.

Placeholder Available in Expanded to
{path} both absolute path of the selected worktree
{base} [review] only result of the base resolution chain
{head} [review] only the current branch name (branch.<name> in git config)
{diff} [review] only absolute path of a tempfile holding git diff {base}..{head} (lazy, see above)

Referencing {diff} in a [git_tui] template is a config error caught at load time (the git TUI launcher doesn’t carry the repo handle needed to materialise the diff).

The {base} placeholder follows this chain, and the first non-empty hit wins:

  1. branch.<name>.merge: the branch’s upstream tracking ref, if any.
  2. branch.<name>.gwm-base: set automatically by gwm create so the original parent stays recoverable even when the upstream is dropped.
  3. [review].default_base: the user-configured fallback in .gwm.toml.
  4. dev: gwm’s project convention (only if dev exists locally).
  5. main: universal git default (final sentinel, guaranteed non-empty).

The chain is implemented in src/launcher.rs::resolve_review_base. The fallback prefers dev only when it exists locally: returning dev blindly when the repo only has main would make the launcher’s subsequent git rev-list / git diff calls fail loudly (caught by Copilot’s review on PR #76).

gwm doctor (post-v0.6) probes the resolved [git_tui] binary (plus the [review] binary when a review launcher is configured) against $PATH as part of its external binaries on PATH check:

  • Any launcher binary missing → Warning (exit code 1), naming the binaries that aren’t on $PATH. The review launcher is only probed when you’ve actually configured one (command or tool), so an unconfigured [review] is never flagged.

See Integrations → gwm doctor for the full check breakdown.