diff --git a/docs-master/Config.md b/docs-master/Config.md index 1d101be1863..15e92ec6b15 100644 --- a/docs-master/Config.md +++ b/docs-master/Config.md @@ -130,6 +130,14 @@ gui: - [commits, reflog] - [stash] + # The side panel that is focused when lazygit starts, or when you switch to + # another repository. + # Must be one of the names listed in `sidePanels`; you can't focus a panel you + # have hidden. + # Starting lazygit with a path filter or with a git subcommand (e.g. `lazygit + # log`) takes precedence over this setting. + initialSidePanel: files + # Sometimes the main window is split in two (e.g. when the selected file has # both staged and unstaged changes). This setting controls how the two sections # are split. diff --git a/pkg/config/side_panel.go b/pkg/config/side_panel.go index 307ed0c1d86..fd97a826964 100644 --- a/pkg/config/side_panel.go +++ b/pkg/config/side_panel.go @@ -27,6 +27,19 @@ var ValidSidePanelTabs = []string{ "stash", } +// SidePanelName is the name of a single side panel, i.e. one of +// ValidSidePanelTabs. +type SidePanelName string + +// JSONSchema restricts a side panel name to the known names. +func (SidePanelName) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{Type: "string", Enum: validSidePanelTabsAsAny()} +} + +func validSidePanelTabsAsAny() []any { + return lo.Map(ValidSidePanelTabs, func(name string, _ int) any { return name }) +} + func (p SidePanel) MarshalYAML() (any, error) { // Render in flow style (`[a, b]`) rather than the default block style, which // is more compact and reads better in the generated docs. @@ -46,9 +59,8 @@ func (p SidePanel) MarshalYAML() (any, error) { // JSONSchema describes a side panel as a list of tab names, restricted to the // known names. func (SidePanel) JSONSchema() *jsonschema.Schema { - names := lo.Map(ValidSidePanelTabs, func(name string, _ int) any { return name }) return &jsonschema.Schema{ Type: "array", - Items: &jsonschema.Schema{Type: "string", Enum: names}, + Items: &jsonschema.Schema{Type: "string", Enum: validSidePanelTabsAsAny()}, } } diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 30ce0377d5a..91b22aca1d3 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -118,6 +118,10 @@ type GuiConfig struct { // Omit a name to hide it; give a name its own one-element list to promote a tab to a top-level panel. // Valid names are: 'status', 'files', 'worktrees', 'submodules', 'branches', 'remotes', 'tags', 'commits', 'reflog', 'stash'. 'files', 'branches', and 'commits' must always be included; they can't be hidden. SidePanels []SidePanel `yaml:"sidePanels"` + // The side panel that is focused when lazygit starts, or when you switch to another repository. + // Must be one of the names listed in `sidePanels`; you can't focus a panel you have hidden. + // Starting lazygit with a path filter or with a git subcommand (e.g. `lazygit log`) takes precedence over this setting. + InitialSidePanel SidePanelName `yaml:"initialSidePanel"` // Sometimes the main window is split in two (e.g. when the selected file has both staged and unstaged changes). This setting controls how the two sections are split. // Options are: // - 'horizontal': split the window horizontally @@ -869,6 +873,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig { {"commits", "reflog"}, {"stash"}, }, + InitialSidePanel: "files", MainPanelSplitMode: "flexible", EnlargedSideViewLocation: "left", WrapLinesInStagingView: true, diff --git a/pkg/config/user_config_validation.go b/pkg/config/user_config_validation.go index 9550e916064..f9e655748ae 100644 --- a/pkg/config/user_config_validation.go +++ b/pkg/config/user_config_validation.go @@ -61,9 +61,26 @@ func (config *UserConfig) Validate() error { if err := validateSidePanels(config.Gui.SidePanels); err != nil { return err } + if err := validateInitialSidePanel(config.Gui.SidePanels, config.Gui.InitialSidePanel); err != nil { + return err + } return nil } +func validateInitialSidePanel(panels []SidePanel, initial SidePanelName) error { + name := string(initial) + if !slices.Contains(ValidSidePanelTabs, name) { + return fmt.Errorf("gui.initialSidePanel: unknown side panel '%s'. Allowed values: %s", + name, strings.Join(ValidSidePanelTabs, ", ")) + } + for _, panel := range panels { + if slices.Contains(panel, name) { + return nil + } + } + return fmt.Errorf("gui.initialSidePanel: '%s' is not listed in gui.sidePanels; a hidden side panel can't be focused.", name) +} + func validateSidePanels(panels []SidePanel) error { seen := map[string]bool{} total := 0 diff --git a/pkg/config/user_config_validation_test.go b/pkg/config/user_config_validation_test.go index a0c17636dbd..c10399e2645 100644 --- a/pkg/config/user_config_validation_test.go +++ b/pkg/config/user_config_validation_test.go @@ -361,6 +361,38 @@ func TestUserConfigValidate_sidePanels(t *testing.T) { } } +func TestUserConfigValidate_initialSidePanel(t *testing.T) { + scenarios := []struct { + name string + panels []SidePanel + initial SidePanelName + valid bool + }{ + {name: "default", panels: []SidePanel{{"files"}, {"branches"}, {"commits"}}, initial: "files", valid: true}, + {name: "non-default panel", panels: []SidePanel{{"files"}, {"branches"}, {"commits"}}, initial: "commits", valid: true}, + {name: "non-first tab of a panel", panels: []SidePanel{{"files"}, {"branches"}, {"reflog", "commits"}}, initial: "commits", valid: true}, + {name: "status panel", panels: []SidePanel{{"status"}, {"files"}, {"branches"}, {"commits"}}, initial: "status", valid: true}, + {name: "empty", panels: []SidePanel{{"files"}, {"branches"}, {"commits"}}, initial: "", valid: false}, + {name: "unknown name", panels: []SidePanel{{"files"}, {"branches"}, {"commits"}}, initial: "bogus", valid: false}, + {name: "hidden panel", panels: []SidePanel{{"files"}, {"branches"}, {"commits"}}, initial: "stash", valid: false}, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + config := GetDefaultConfig() + config.Gui.SidePanels = s.panels + config.Gui.InitialSidePanel = s.initial + err := config.Validate() + + if s.valid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} + func TestUserConfigValidate_pagers(t *testing.T) { scenarios := []struct { name string diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 912d46567b2..79e17ca404b 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -661,7 +661,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context { gui.applySidePanelConfig() - return initialContext(contextTree, startArgs) + return initialContext(contextTree, startArgs, gui.c.UserConfig().Gui.InitialSidePanel) } func (gui *Gui) loadCachedPullRequests() []*models.GithubPullRequest { @@ -745,8 +745,12 @@ func parseScreenModeArg(screenModeArg string) types.ScreenMode { } } -func initialContext(contextTree *context.ContextTree, startArgs appTypes.StartArgs) types.IListContext { - var initialContext types.IListContext = contextTree.Files +func initialContext( + contextTree *context.ContextTree, + startArgs appTypes.StartArgs, + initialSidePanel config.SidePanelName, +) types.Context { + initialContext := sidePanelContexts(contextTree)[string(initialSidePanel)] if startArgs.FilterPath != "" { initialContext = contextTree.LocalCommits diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 77e54e26509..c068ae9810c 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -493,6 +493,8 @@ var tests = []*components.IntegrationTest{ ui.DisableSwitchTabWithPanelJumpKeys, ui.EmptyMenu, ui.HideSidePanel, + ui.InitialSidePanel, + ui.InitialSidePanelNotFirstTab, ui.KeybindingSuggestionsDontCrashOnDisabledBindings, ui.KeybindingSuggestionsWhenSwitchingRepos, ui.ModeSpecificKeybindingSuggestions, diff --git a/pkg/integration/tests/ui/initial_side_panel.go b/pkg/integration/tests/ui/initial_side_panel.go new file mode 100644 index 00000000000..d21c8266f5f --- /dev/null +++ b/pkg/integration/tests/ui/initial_side_panel.go @@ -0,0 +1,25 @@ +package ui + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var InitialSidePanel = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "The side panel named by gui.initialSidePanel is focused at startup", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().Gui.InitialSidePanel = "commits" + }, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("one") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + IsFocused(). + Lines( + Contains("one"), + ) + }, +}) diff --git a/pkg/integration/tests/ui/initial_side_panel_not_first_tab.go b/pkg/integration/tests/ui/initial_side_panel_not_first_tab.go new file mode 100644 index 00000000000..831bc9fe37c --- /dev/null +++ b/pkg/integration/tests/ui/initial_side_panel_not_first_tab.go @@ -0,0 +1,35 @@ +package ui + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var InitialSidePanelNotFirstTab = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "gui.initialSidePanel names a tab that isn't the first one of its panel, so it must be brought to the front", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().Gui.SidePanels = []config.SidePanel{ + {"status"}, + {"files"}, + {"branches"}, + {"reflog", "commits"}, + {"stash"}, + } + cfg.GetUserConfig().Gui.InitialSidePanel = "commits" + }, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("one") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + // Commits is shown in front of the reflog tab it shares a panel with, + // rather than being focused behind it. + t.Views().Commits(). + IsActiveTab(). + IsFocused(). + Lines( + Contains("one"), + ) + }, +}) diff --git a/schema-master/config.json b/schema-master/config.json index 82dbebb0b7f..c6ab03a466d 100644 --- a/schema-master/config.json +++ b/schema-master/config.json @@ -624,6 +624,10 @@ ] ] }, + "initialSidePanel": { + "$ref": "#/$defs/SidePanelName", + "description": "The side panel that is focused when lazygit starts, or when you switch to another repository.\nMust be one of the names listed in `sidePanels`; you can't focus a panel you have hidden.\nStarting lazygit with a path filter or with a git subcommand (e.g. `lazygit log`) takes precedence over this setting." + }, "mainPanelSplitMode": { "type": "string", "enum": [ @@ -3622,6 +3626,23 @@ }, "type": "array" }, + "SidePanelName": { + "type": "string", + "enum": [ + "status", + "files", + "worktrees", + "submodules", + "branches", + "remotes", + "tags", + "commits", + "reflog", + "stash" + ], + "description": "The side panel that is focused when lazygit starts, or when you switch to another repository.\nMust be one of the names listed in `sidePanels`; you can't focus a panel you have hidden.\nStarting lazygit with a path filter or with a git subcommand (e.g. `lazygit log`) takes precedence over this setting.", + "default": "files" + }, "SpinnerConfig": { "properties": { "frames": {