Commit graph

4845 commits

Author SHA1 Message Date
Stefan Haller 348224a96e Add a global keybinding for editing the config file
The status panel already binds the universal edit key to "edit config
file", but that's only reachable while the status panel is focused. Add
a dedicated global binding (alt+shift+c) so the config file can be
opened from anywhere.
2026-06-24 22:10:52 +02:00
Stefan Haller f3ea0ab902 Extract editConfig into a shared EditConfigAction
The "edit config file" command is about to gain a second, global
keybinding alongside the existing status-panel one. Moving its body into
an action struct (the convention GlobalController already follows for
all its handlers) lets both controllers delegate to one implementation.
2026-06-24 22:03:57 +02:00
Stefan Haller a8834930a0 Remove unnecessary askForConfigFile indirection 2026-06-24 22:02:55 +02:00
Stefan Haller 9b1acce0fe Remove the "Open config file" command
OpenFile (`o`) is for opening a file as if it was double-clicked in
Finder/Explorer; this is useful for binary files like PNGs, but never
for text files. You want to edit them, and there's `e` for that.
2026-06-24 22:02:52 +02:00
Stefan Haller c6b8220772 Test per-repo side panel config and re-application on repo switch
Exercises the path the live reload relies on: a per-repo lazygit.yml
sets a different side panel layout, and switching between repos
re-applies each one's own layout (the new-repo path for the cloned repo,
the cached-repo path on switching back).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan Haller 9aaff61b79 Re-apply the side panel config on a live config reload
When the config file changes and lazygit regains focus it reloads the
config, but the side panel window assignments, default views, tab
strips, and z-order were only ever set up on repo entry, so a changed
sidePanels wouldn't take effect until restart. Re-apply it from the
reload path: reassign windows and default views and restore each panel's
default tab.

The focused panel needs care: resetting it to its default tab would
leave the focused tab hidden behind that default tab, so the panel looks
unfocused even though its tab is selected. Re-focus the current context
so its tab stays shown and highlighted; only when the new config hides
the focused panel entirely do we move focus to the default side panel.
Tab strips are already refreshed via configureViewProperties.
2026-06-23 14:15:18 +02:00
Stefan Haller 2614156b2f Add an IsActiveTab assertion for integration tests
Side panel tabs share a window, so which tab is shown is decided by view
z-order rather than the visibility flag (every tab in a window is
'visible'). Tests had no way to assert which tab is actually drawn in
front, which is distinct from which view has keyboard focus. Expose the
window's top view and add an IsActiveTab assertion built on it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan Haller 30559f1058 Let integration tests post a focus event
Lazygit reloads changed config files when its terminal window regains
focus, but the test harness had no way to simulate that focus event, so
the live config-reload path was untestable. Add a focus event to the
replayed-events queue and expose it through the GuiDriver as FocusIn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan Haller 922861bb36 Clear tab strips on views that are no longer tabs
The tab-assignment loop only ever set a view's tabs; it never cleared them.
That was fine when the groupings were fixed, but with gui.sidePanels a
config reload can turn a tab into a standalone panel, and the old tab strip
would linger on its title. Index the tab strips by view name and assign to
every view, so views that dropped out of a multi-tab panel get their tabs
cleared. No change for a given config; this only matters across a reload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan Haller 196f820af9 Add integration tests for configuring the side panels
Cover the three things gui.sidePanels enables: reordering the panels
(swapping branches and commits, checked via their jump keys), hiding a
panel (omitting stash, checked by cycling past the last panel and wrapping
to the first), and promoting a tab to its own panel (worktrees becomes a
top-level panel reachable by a jump key, and the files panel's remaining
tabs cycle straight to submodules).

The tests drive focus with explicit jump keys rather than ViewDriver.Focus,
which assumes the default panel layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan Haller ba0f7e8dba Show each panel's first configured tab by default
Within a window the visible tab is whichever view sits on top in the
z-order, and onRepoViewReset establishes that z-order from a fixed list
that needn't agree with the configured tab order. After ordering the
views, bring each panel's first configured tab to the top so that, for a
panel whose tabs have been reordered, the configured first tab is the one
shown before the panel is focused. No effect on the default layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan Haller e396483deb Scale the minimum window height with the panel count
In squashed mode (short terminals) the unfocused side panels each reserve
a row and the focused panel takes whatever is left, so once the unfocused
panels' rows fill the height the focused panel collapses to nothing and
panels below it render off-screen. The fixed floor of 9 was tuned for five
panels; with the panel count now configurable (and promotion allowing up
to ten), grow the floor by one per panel so we show the "not enough space"
view instead of a broken layout. Five panels still floor at 9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan Haller da8ef69133 Give the submodules and reflog views standalone titles
These two views only ever appeared as tabs (of the files and commits
panels), so unlike the other side views they had no title set; the tab
strip supplied their label. Once a tab can be promoted to its own panel
they can appear without a tab strip, so set their titles like the others.
This has no effect in the default layout, where both are always tabs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan Haller 56f3049af4 Drive the side panel layout from gui.sidePanels
Replace the hard-coded side panel order, tab groupings, and window
assignments with values resolved from the gui.sidePanels config. The panel
order (SideWindows and the layout boxes), the tab strips (viewTabMap), the
per-context window names, each window's default view, and the jump-label
groups all now come from the config rather than from five separate
hard-coded lists.

A panel's window name is the name of its first tab, and panels not listed
in the config get their own window name so their views stay hidden instead
of overlapping a visible panel. Three small lookups translate config names
into views, tab titles, and contexts; a test keeps them in sync with the
set of valid names. The lookups are split this way (rather than one
resolver) because configureViewProperties runs before the context tree
exists, so the title/view lookups must not depend on it.

The config is applied to a repo's contexts via applySidePanelConfig on
every repo entry, including the cached-repo path: a repo's per-repo config
can differ from the previously visited one's, so each repo's contexts must
be (re)assigned from its own config rather than kept from when they were
first built.

With the default config this reproduces today's layout exactly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan Haller 2f3ed7e0eb Stop requiring jumpToBlock to have exactly five entries
The number of side panels is about to become configurable, so a fixed
count of jump-to-panel keys no longer makes sense: a user who configures
six panels shouldn't be forced to also extend jumpToBlock, and one who
hides a panel shouldn't have to trim it. Drop the count check entirely
(individual keys are still validated) and assign keys to panels
positionally, for as many panels as there are keys. Surplus panels go
without a jump key but remain reachable via the next/previous-panel keys.
This also removes the log.Fatal that the count check guarded against.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan Haller 195b578fc1 Add the gui.sidePanels config option
This adds the user-facing surface for configuring the side panels: their
order, which ones are visible, and how tabs are grouped into panels. Each
entry is either a single panel name or a list of names sharing one panel
as tabs, mirroring how the Keybinding type accepts a scalar or a sequence;
the JSON schema restricts the names to the known set so editors can offer
completion and catch typos. The default reproduces today's layout exactly.

Validation rejects unknown or duplicated names, and requires the files,
branches, and commits panels to always be present: a lot of code focuses
those directly (e.g. after resolving a conflict or popping a stash), so
allowing them to be hidden would let that code focus a hidden panel.

Nothing reads the option yet; the layout still uses the hard-coded order.
Wiring follows in a later commit so the inert surface (and its generated
docs and schema) can be reviewed on its own.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan Haller b7258958b8 Assign panel jump labels by iterating panel groups
The jump-label prefixes were assigned to each side view by name, twice
(once for the on case, once for off), so the panel-to-views grouping and
the panel order were baked into 28 positional statements. Express the
grouping once as a slice of view groups and loop over it, deriving each
panel's label from its index. The label lookup is now bounds-checked, so
it no longer assumes exactly as many jump bindings as panels. Behavior is
unchanged; this prepares the grouping to come from config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan Haller 853f01eb3c Make the remote-branches view follow its parent's window
The remote-branches context is a transient guest that takes over a host
window when you drill into a remote. SubCommits and CommitFiles already
adopt their parent's window via SetWindowName when shown; RemoteBranches
relied instead on its static window name ("branches") matching the remotes
context's window. That assumption only holds while remotes lives in the
branches panel. Adopt the parent's window like the other transient guests
so remote branches render in the right place once panels are configurable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan Haller dc9445014d Drive side-panel layout from a single window list
The three branches of sidePanelChildren each spelled out the five side
windows by name, so the panel order lived in three places and the
status/stash sizing special-cases were tangled into positional literals.
Map each branch over one `windows` slice instead, and fold the
normal-height special-cases (status's fixed height, stash's
collapse-unless-focused) into a single per-window function. Behavior is
unchanged; this isolates the ordering so it can later come from config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:15:18 +02:00
Stefan Haller 13817665aa Add end-to-end test for shell command quoting on Windows
The unit tests only assert the arguments lazygit constructs; they can't
catch cmd.exe's own quote-stripping, which is where #5560 actually
manifested. This test builds a small editor executable, places it and
the file it opens at paths containing spaces, runs it through real
cmd.exe via NewShell, and checks the editor received the intended args.
It runs only on Windows.

Co-Authored-By: Antoine Gaudreau Simard <a.simard@multidev.net>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:11:00 +02:00
Stefan Haller 6b311ccb62 Fix quoting of shell commands on Windows
lazygit builds a shell command by interpolating Quote'd arguments into a
template and running the result via `cmd /c`. Several things were wrong
on Windows:

- Quote emitted bash-style `\"…\"` quoting, which cmd.exe doesn't
  understand. Making it usable at all previously required a fragile
  round-trip through str.ToArgv and re-escaping.
- The assembled command line was handed to `cmd /c` without `/s`, so
  cmd's default rules stripped the wrong quotes once the line contained
  more than two of them (e.g. a quoted editor path at a location with
  spaces, plus a quoted filename that also contains spaces).
- Shell metacharacters were escaped with `^` (`&` → `^&`, etc.), which
  neutralised command chaining, pipes, redirection and `%VAR%` expansion
  in custom commands.

Quote now emits the standard Windows convention directly, and NewShell
hands cmd.exe the fully-assembled line verbatim via SysProcAttr.CmdLine,
wrapped as `cmd /s /c "<command>"`. The /s flag strips exactly the outer
quote pair we add, leaving each argument's own quoting intact. With the
`^` escaping gone, metacharacters in a custom command reach cmd as the
author intended; this also removes the spurious `^` reported in #3092.

Fixes #5560
Fixes #2427
Fixes #4147

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:11:00 +02:00
Stefan Haller e0fcdf1c3f Demonstrate that shell metacharacters are mangled on Windows
On Windows, NewShell escapes shell metacharacters (`&`, `|`, `<`, `>`,
`%`) with `^` and splits the command into separate arguments. The
operators in a custom command therefore never reach cmd as operators,
so command chaining (`&&`), pipes, redirection and `%VAR%` expansion all
silently break (#2427, #4147, #5113; the stray `^` is also what #3092
reports).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:11:00 +02:00
Stefan Haller 41efc9a37a Demonstrate that Quote produces invalid Windows quoting
On Windows, Quote wraps arguments in bash-style `\"…\"` and rewrites
embedded double quotes as `"'"'"`. Neither convention is understood by
cmd.exe or CommandLineToArgvW, so commands built from quoted arguments
are mis-parsed once they contain quotes or spaces (#5560).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:11:00 +02:00
Stefan Haller e33b6f9397 Document running the integration tests via the just recipes
The integration README still described the raw `go run cmd/integration_test`
and `go test` invocations, which are easy to get wrong (the headless go-test
command in particular) and don't match how we actually run the tests. Rewrite
the running/debugging/sandbox instructions around the justfile's e2e recipes
instead, and point at the optional zsh completion script. Also switch the
test-list regeneration hint to `just generate`, matching the rest of our docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 13:31:03 +02:00
Stefan Haller 7f96c8ff4f Remove bespoke commit selection restoration when starting a rebase
Starting an interactive rebase (the `edit` command and quick-start) used
to capture the selected commit range by hash before starting the rebase
and restore it afterwards, because new update-ref lines for stacked
branches can shift the commits' positions in the list. The generic
keep-selection-by-hash mechanism now does exactly this for every refresh,
including these, so the bespoke code is redundant.

This relies on the previous commit, which taught the generic matcher to
handle the case where the selected commit turns into a rebase TODO entry
while it's being edited - something the bespoke code handled implicitly
by matching on hash alone.
2026-06-23 09:20:05 +02:00
Stefan Haller b6063cff5b Restore commit selection even when the commit's TODO status changed
When restoring the commit selection after a refresh we match by hash and
TODO status. The TODO status is part of the match so that a commit being
reverted or cherry-picked is matched to the real commit rather than to
the rebase TODO entry that shares its hash.

But a selected commit can also change its TODO status across a refresh:
when starting an interactive rebase that stops to edit it, the real
commit becomes a TODO entry. Fall back to matching by hash alone when
there is no exact match, so the selection is still restored in that case.

The next commit relies on this to remove bespoke selection-restoration
code in the local commits controller that matched by hash alone, which
the generic mechanism otherwise wouldn't fully replace.
2026-06-23 09:20:05 +02:00
Stefan Haller c15ab5db5d Keep selected commits stable across refreshes
With the recently added external change detection, it happens more often
now that we refresh the commits list because an agent made a commit in
the background. In this case, if we keep the selection index the same,
it now points at a different commit, making the main view show a
different commit too, which is confusing and annoying. To fix this,
track the selected commit and range anchor by hash before reloading,
then restore those rows if both hashes still exist. This also allows us
to get rid of some bespoke code that did this for the specific cases of
reverting a commit or cherry-picking commits, because those are now
handled by the generic mechanism.
2026-06-23 09:20:05 +02:00
Stefan Haller 10d2f9f715 Allow GpgHelper to refresh differently on success and failure
Preparation for the next commit, which selects the newly created commit
after a commit succeeds, while leaving the selection alone on failure.
For now success and failure use the same refresh options, so behavior is
unchanged.
2026-06-23 08:43:51 +02:00
Stefan Haller 5d5aa0a865 Cleanup: wrap long parameter lists
This makes the following diff a little easier to read.
2026-06-23 08:43:51 +02:00
Stefan Haller 3f8dc527b5 Cleanup: remove unnecessary if statement 2026-06-23 08:43:51 +02:00
Stefan Haller cfb46f440c Add HeadCommitIdx helper function
Not used yet, we'll need it in the next commit.
2026-06-22 09:16:57 +02:00
Stefan Haller d673a0f3b9 Add tests for IsHeadCommit 2026-06-22 09:03:14 +02:00
Stefan Haller 02be5e74ed Tighten a test expectation
This guards against regressions from the changes that follow. We're
about to add a mechanism that keeps the selection anchored by commit
hash, but we need to make sure that it doesn't take effect here; after a
merge we want to select the newly added merge commit. In the current
state of the code this happens to work because we keep the selection
index the same, which happened to be 0 here; later we will change this
to explicitly select the head commit after the merge.
2026-06-22 09:00:15 +02:00
Stefan Haller eb988395e6 Remove the now-redundant gitCmdObjBuilder wrapper
The wrapper existed to add a git-specific env var to every command. Now
that that's gone, its New/NewShell/Quote methods just delegated to the
inner builder. The only remaining git-specific behavior — the command
runner — is attached in the constructor via CloneWithNewRunner, which
already returns a complete builder, so we can return that directly and
drop the wrapper struct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 18:14:24 +02:00
Stefan Haller d94f2f05ac Only pass --no-optional-locks for background status refreshes
We set GIT_OPTIONAL_LOCKS=0 for every git command we run. That env var
only affects `git status`: it tells git not to take the optional lock it
would otherwise use to write the index back after refreshing the cached
stat information. The intent was to avoid contending for index.lock with
git commands the user runs in a terminal.

The downside is that our `git status` never persists the refreshed
stat-cache. So whenever the working tree's cached stat info goes stale
(e.g. editing files and discarding the changes, or a checkout), every
subsequent status re-hashes the affected files to confirm they're clean,
and stays slow until something else writes the index (such as the user
running `git status` in a terminal).

Fix this by only suppressing optional locks for refreshes that run
unattended in the background; foreground refreshes triggered by a user
action now run a plain `git status` that writes the refreshed index back,
just like the command line does. Background refreshes keep passing
--no-optional-locks so they still can't cause lock contention.

RefreshOptions gains a Background flag that the background routines set,
threaded down to the status command.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 18:14:24 +02:00
Stefan Haller 94db69f64b Add GlobalArg/GlobalArgIf to GitCommandBuilder
This can be used to add a git argument that goes before the git
subcommand.
2026-06-19 18:14:24 +02:00
Stefan Haller 3050303ed2 Detect external ref changes via background polling
Add a 2-second background poll that calls Status.RefsSnapshot and
compares against the snapshot stored at the end of the last refs-
touching refresh. On a diff, trigger a full refresh — same scope as the
focus-in handler, because once we know something changed externally
we can't be sure what (an agent might have created a worktree or
stashed something alongside the commit we detected).

Refresh runs in SYNC mode because goEvery already serializes iterations
via <-done: a slow refresh delays the next tick naturally instead of
letting work stack. The post-refresh hook from the previous commit
updates the snapshot, so in-app commands don't cause the next poll to
spuriously re-fire.

Disabled in the integration test config, like autoRefresh and autoFetch,
because demo replays make repo changes throughout the run; at 2-second
cadence the resulting full refreshes compete with the demo's own
choreography and push some demos past their 40-second timeout.

Also list the two new config keys in checkForChangedConfigsThatDontAutoReload
so a config edit warns the user that lazygit needs a restart.
2026-06-19 18:07:48 +02:00
Stefan Haller c1eeacdfe8 Snapshot refs state before refs-touching refreshes
Add the storage and snapshot-update half of the external-change-detection
mechanism. RefreshHelper now keeps a mutex-protected snapshot string and exposes
accessors for it; Refresh captures a fresh snapshot at the start of any refresh
whose scope set includes COMMITS or BRANCHES.

We capture before reading the git state, not after. Capturing after would let an
external change that lands between the git state read and the snapshot (say, the
next step of a rebase running in another terminal) leave the stored snapshot
newer than what we actually rendered; the poller would then see no difference
and never refresh again, stranding the UI on the intermediate state. Capturing
first keeps the snapshot from running ahead of the render, so if disk moves
during the refresh the next poll catches it.

No reader of the snapshot exists yet — the polling goroutine that consumes it
comes in a later commit. Keeping the snapshot hook in its own commit isolates
the invariant that the snapshot stays in sync with what the UI has observed,
which is what makes the poller's change-detection predicate work across in-app
commands and focus-in refreshes.
2026-06-19 18:07:48 +02:00
Stefan Haller 661df80fe8 Add config options for external change detection
Two settings to control the upcoming background polling mechanism:

- git.autoDetectExternalChanges (default true) is the on/off switch, parallel to
  autoFetch/autoRefresh
- refresher.externalChangeCheckInterval (default 2 seconds) is the poll cadence

Disabling is the bool's job, not a magic 0 interval, matching the existing
convention.

Not yet referenced by any code.
2026-06-19 18:07:48 +02:00
Stefan Haller cb12cb2f6b Add Status.RefsSnapshot
A cheap fingerprint of local branches and HEAD that future code can poll to
detect when refs have moved externally.

Branches come from a porcelain for-each-ref. HEAD is read directly from
.git/HEAD: that avoids spawning a child process and captures the symref-or-hash
distinction we need to tell "detached at X" apart from "on a branch pointing at
X" — they share a commit hash, which is exactly the situation at the end of a
rebase when HEAD reattaches to the branch. The reftable backend doesn't keep a
real .git/HEAD (it writes a fixed stub), so when we see that stub or the file is
unreadable we fall back to porcelain commands, which are backend-agnostic.

Uses DontLog so a future polling caller won't spam the command log. Not yet
wired up to any caller.
2026-06-19 18:07:48 +02:00
Stefan Haller 93bd26b9a9 Centralize scope expansion in Refresh
Several downstream conditions in Refresh() relied on multi-scope predicates to
express "if X is in scope, Y also needs refreshing". This makes it hard to add
new code that needs to ask "does this refresh re-read refs?", because the answer
involves mirroring one of those predicates and keeping them in sync forever.

Expand the co-refreshing relationships once, up front, right after the scope set
is built. The downstream conditions then collapse to single-scope checks against
the (now-expanded) set. Behavior is preserved.

Two of the scattered multi-scope conditions are intentionally left as-is because
they express subsumption rather than co-refresh (one branch already does the
work of another internally — expanding would cause double-refresh), and one
expresses mid-function coupling on a flag set inside the COMMITS/BRANCHES block.
2026-06-19 18:07:48 +02:00
Stefan Haller d81b6d9e1d Log CPU time of external commands in addition to wall-clock time
For certain kinds of performance investigations it is useful to see this, and
doesn't terribly pollute the log, so just do this always.
2026-06-19 18:07:48 +02:00
Stefan Haller 04d62e072b Fix schema minimum for refresh and fetch intervals
The schema annotated refreshInterval and fetchInterval with minimum=0,
but the background routines reject a value of 0 (they require
interval > 0 and otherwise log it as invalid and disable the feature).
So 0 is not actually a valid value; switch to exclusiveMinimum=0 so the
schema matches what the code accepts.
2026-06-19 18:07:48 +02:00
Stefan Haller 3cf890b7d7 Pause background refreshes while driving a git operation
Several commands (rewording or amending an earlier commit, custom patch
operations, etc.) are implemented by starting an interactive rebase that stops
at a commit, amending it, and continuing. When no conflict occurs, the user
isn't meant to notice a rebase happened at all.

But a background file refresh can fire while the rebase is mid-flight and render
a dirty working copy of whatever the behind-the-scenes rebase is doing (e.g.
applying a custom patch).

To fix this, we pause the background routines for the duration of any
waiting-status operation — exactly the window in which lazygit is driving the
git operation itself and will refresh once at the end. The boundary is also
right for the conflict case: when a rebase stops on a conflict the operation
returns, the pause releases, and background refreshes resume for the interactive
resolution that follows.
2026-06-19 17:56:48 +02:00
Stefan Haller f9c81b655d Make background-refresh pausing reentrant
Replace the pauseBackgroundRefreshes bool with a count. The single existing
caller (subprocess suspend/resume) is unaffected, but we're about to add a
second, independent reason to pause — lazygit driving a git operation that the
background routines would otherwise catch mid-flight — and the two scopes can
overlap. A bool can't represent "two things both want refreshes paused"; a count
can.
2026-06-19 17:56:48 +02:00
Stefan Haller 51b409383c Extend the fill background to wrapped tail segments
Tools like delta paint each diff line's background with '\x1b[K' so
the color reaches the right edge. Up to now the '\x1b[K' handler
appended (InnerWidth - cx) explicit padding cells with the fill bg
so rendering picked up the color. That worked for short lines but
silently degraded once content exceeded InnerWidth: the repeat
count went non-positive, no cells were added, and after wrapping
the partial tail segment was left without any cells carrying the
fill color, so draw() fell back to the view's default bg.

Record the fill colors on the source line as optional
trailingFillAttributes. In the '\x1b[K' handler set them (and drop
the padding-cell loop — the metadata covers both the wrap and the
non-wrap cases). In draw(), once per source line, pick the trailing
cell's fg/bg from the metadata if present and otherwise from the
view defaults; then the inner-loop fills past-content cells with
that.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-18 17:22:31 +02:00
Stefan Haller 9c8a02f901 Remove the '\n' sentinel cell
The sentinel was appended to every \n-terminated line solely so that
draw()'s prevFgColor tracking would reset to default for the trailing
area; without it, an AttrReverse-styled last cell would carry its
rendered bg past the end of the line.

The same prevFgColor mechanism propagated AttrReverse past content on
*unterminated* lines too — which doesn't match real terminal behavior
(try `print '\x1b[7m\x1b[31mfoo'` in a shell: the reverse stops at
the last character) and isn't relied on by anything in lazygit, since
all our writers terminate lines with \n.

Drop the sentinel cell, drop prevFgColor, and just have draw() paint
trailing cells with the view's default fg/bg. The
TestUnterminatedReverseLineExtendsToEdge regression test inverts to
document the new (terminal-matching) behavior, renamed accordingly.
TestWriteString expectations also drop the trailing "" that came
from the sentinel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-18 17:22:31 +02:00
Stefan Haller ad8335aaa8 Wrap v.lines cells in a line struct
The cells of a source line will soon need to carry metadata about how
the line was terminated (newline vs filled to edge via \x1b[K). Move
to a struct so there's somewhere to put it; this commit only renames
[][]cell to []line{cells: ...} with no behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-18 17:22:31 +02:00
Stefan Haller 1788eaa91f Rename lineType to cells
This is to free up the name lineType for something else in the next commit.
2026-06-18 17:22:31 +02:00
Stefan Haller d234d8b358 Remove dead \x00 filtering from string conversion helpers
The four ReplaceAll(str, "\x00", "") calls (and the equivalent
rune-by-rune skip in linesToString) are leftover from when cell.chr
was a rune and \x00 was used as an internal sentinel. With chr now
being a string and no code path writing \x00, the filtering never
strips anything.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-18 17:22:31 +02:00