From 600b9aa34076a1431ba3f2e949c7ebc0c9b03389 Mon Sep 17 00:00:00 2001 From: yzxcj797 Date: Sat, 22 Aug 2026 06:36:10 +0800 Subject: [PATCH] Add gui.startupPanel config for the initially focused panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/Config.md | 4 +++ pkg/config/user_config.go | 6 ++++ pkg/gui/gui.go | 38 ++++++++++++++++++++-- pkg/gui/initial_context_test.go | 57 +++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 pkg/gui/initial_context_test.go diff --git a/docs/Config.md b/docs/Config.md index 857a4e359..39c08d930 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -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 diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 9738186d9..ebbc986d7 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -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. diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index bde383caf..58a633eaf 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -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 } diff --git a/pkg/gui/initial_context_test.go b/pkg/gui/initial_context_test.go new file mode 100644 index 000000000..77a164150 --- /dev/null +++ b/pkg/gui/initial_context_test.go @@ -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]) + } +}