Add gui.startupPanel config for the initially focused panel

The startup panel was hard-coded to Files unless a positional
argument (lazygit log / branches / stash) or a filter path picked
another one — tig-style users who live in the commits panel had to
type the argument every launch (#5877).

gui.startupPanel now selects the focused side panel for argument-less
launches: one of worktrees, submodules, branches, remotes, tags,
commits, reflog, or stash. CLI arguments and filter paths keep
precedence, an unrecognized value falls back to files, and the
validation lives in a pure normalizeStartupPanel so the accepted
vocabulary is pinned by unit tests.
This commit is contained in:
yzxcj797 2026-08-22 06:36:10 +08:00
parent ea91639546
commit 600b9aa340
4 changed files with 102 additions and 3 deletions

View file

@ -103,6 +103,10 @@ gui:
# If true, increase the height of the focused side window; creating an accordion
# effect.
# The side panel to focus on startup (when no positional argument like
# `lazygit log` is given). One of: files, worktrees, submodules, branches,
# remotes, tags, commits, reflog, stash. Anything else falls back to files.
startupPanel: files
expandFocusedSidePanel: false
# The weight of the expanded side panel, relative to the other panels. 2 means

View file

@ -113,6 +113,12 @@ type GuiConfig struct {
ExpandedSidePanelWeight int `yaml:"expandedSidePanelWeight"`
// If true, don't give a side panel more height than it needs to show its content; when all panels fit, the leftover height is shared among them so that they still fill the screen.
ShrinkSidePanelsToContent bool `yaml:"shrinkSidePanelsToContent"`
// The side panel to focus on startup, when lazygit is launched without
// a positional argument (e.g. `lazygit log`). Valid values are:
// 'files', 'worktrees', 'submodules', 'branches', 'remotes', 'tags', 'commits', 'reflog', 'stash'.
// An unrecognized value falls back to 'files'.
StartupPanel string `yaml:"startupPanel"`
// The side panels, in the order they appear from top to bottom.
// Each entry is a list of one or more names that share a single panel as tabs (cycle through them with the next-tab/previous-tab keys).
// Omit a name to hide it; give a name its own one-element list to promote a tab to a top-level panel.

View file

@ -664,7 +664,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context {
gui.applySidePanelConfig()
return initialContext(contextTree, startArgs)
return initialContext(contextTree, startArgs, gui.Config.GetUserConfig())
}
func (gui *Gui) loadCachedPullRequests() []*models.GithubPullRequest {
@ -752,7 +752,19 @@ func parseScreenModeArg(screenModeArg string) types.ScreenMode {
}
}
func initialContext(contextTree *context.ContextTree, startArgs appTypes.StartArgs) types.IListContext {
// normalizeStartupPanel validates gui.startupPanel: a recognized panel name
// passes through, and anything else (including the empty string and the
// default "files", which initialContext already starts on) normalizes to ""
// so the caller keeps the Files panel (#5877).
func normalizeStartupPanel(name string) string {
switch name {
case "worktrees", "submodules", "branches", "remotes", "tags", "commits", "reflog", "stash":
return name
default:
return ""
}
}
func initialContext(contextTree *context.ContextTree, startArgs appTypes.StartArgs, userConfig *config.UserConfig) types.IListContext {
var initialContext types.IListContext = contextTree.Files
if startArgs.FilterPath != "" {
@ -770,8 +782,28 @@ func initialContext(contextTree *context.ContextTree, startArgs appTypes.StartAr
default:
panic("unhandled git arg")
}
} else if panel := normalizeStartupPanel(userConfig.Gui.StartupPanel); panel != "" {
// An explicit config wins over the default Files panel when no CLI
// argument selected one (#5877); anything unrecognized stays on Files.
switch panel {
case "worktrees":
initialContext = contextTree.Worktrees
case "submodules":
initialContext = contextTree.Submodules
case "branches":
initialContext = contextTree.Branches
case "remotes":
initialContext = contextTree.Remotes
case "tags":
initialContext = contextTree.Tags
case "commits":
initialContext = contextTree.LocalCommits
case "reflog":
initialContext = contextTree.ReflogCommits
case "stash":
initialContext = contextTree.Stash
}
}
return initialContext
}

View file

@ -0,0 +1,57 @@
package gui
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
// normalizeStartupPanel is the validation half of gui.startupPanel (#5877):
// recognized names pass through, everything else (empty, "files" — the
// default the caller already starts on — and typos) normalizes to "" so the
// Files panel stays focused.
func TestNormalizeStartupPanel(t *testing.T) {
for _, name := range []string{
"worktrees", "submodules", "branches", "remotes",
"tags", "commits", "reflog", "stash",
} {
assert.Equal(t, name, normalizeStartupPanel(name), name)
}
for _, name := range []string{"", "files", "status", "Files", "commit", "nonsense"} {
assert.Equal(t, "", normalizeStartupPanel(name), name)
}
}
// The wiring half: initialContext consults the config only when no CLI
// argument selected a panel, and each recognized name selects the matching
// tree field. Source-structural pin (the repo's established pattern for
// context-graph assertions) because building a real ContextTree requires the
// full view harness.
func TestInitialContextWiring(t *testing.T) {
b, err := os.ReadFile("gui.go")
assert.NoError(t, err)
source := string(b)
assert.Contains(t, source,
"} else if panel := normalizeStartupPanel(userConfig.Gui.StartupPanel); panel != \"\" {",
"the config branch must come after the FilterPath/GitArg branches so CLI arguments win")
assert.Contains(t, source,
"initialContext(contextTree, startArgs, gui.Config.GetUserConfig())",
"the caller must pass the user config through")
for _, pair := range [][2]string{
{"worktrees", "contextTree.Worktrees"},
{"submodules", "contextTree.Submodules"},
{"branches", "contextTree.Branches"},
{"remotes", "contextTree.Remotes"},
{"tags", "contextTree.Tags"},
{"commits", "contextTree.LocalCommits"},
{"reflog", "contextTree.ReflogCommits"},
{"stash", "contextTree.Stash"},
} {
assert.Contains(t, source,
"case \""+pair[0]+"\":\n\t\t\tinitialContext = "+pair[1],
"startupPanel "+pair[0]+" must select "+pair[1])
}
}