Skip to content
7 changes: 7 additions & 0 deletions docs-master/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ gui:
# a branch that is checked out in that worktree
skipSwitchWorktreeOnCheckoutWarning: false

# If true, show worktrees as its own side panel group instead of a tab in the
# files group
worktreesInSeparateGroup: false

# If true, hide the status panel from the side panel
hideStatusPanel: false

# Fraction of the total screen width to use for the left side section. You may
# want to pick a small number (e.g. 0.2) if you're using a narrow screen, so
# that you can see more of the main section.
Expand Down
7 changes: 7 additions & 0 deletions docs/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ gui:
# a branch that is checked out in that worktree
skipSwitchWorktreeOnCheckoutWarning: false

# If true, show worktrees as its own side panel group instead of a tab in the
# files group
worktreesInSeparateGroup: false

# If true, hide the status panel from the side panel
hideStatusPanel: false

# Fraction of the total screen width to use for the left side section. You may
# want to pick a small number (e.g. 0.2) if you're using a narrow screen, so
# that you can see more of the main section.
Expand Down
4 changes: 4 additions & 0 deletions pkg/config/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ type GuiConfig struct {
SkipRewordInEditorWarning bool `yaml:"skipRewordInEditorWarning"`
// If true, switch to a different worktree without confirmation when checking out a branch that is checked out in that worktree
SkipSwitchWorktreeOnCheckoutWarning bool `yaml:"skipSwitchWorktreeOnCheckoutWarning"`
// If true, show worktrees as its own side panel group instead of a tab in the files group
WorktreesInSeparateGroup bool `yaml:"worktreesInSeparateGroup"`
// If true, hide the status panel from the side panel
HideStatusPanel bool `yaml:"hideStatusPanel"`
// Fraction of the total screen width to use for the left side section. You may want to pick a small number (e.g. 0.2) if you're using a narrow screen, so that you can see more of the main section.
// Number from 0 to 1.0.
SidePanelWidth float64 `yaml:"sidePanelWidth" jsonschema:"maximum=1,minimum=0"`
Expand Down
7 changes: 6 additions & 1 deletion pkg/gui/context/worktrees_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,17 @@ func NewWorktreesContext(c *ContextCommon) *WorktreesContext {
)
}

windowName := "files"
if c.UserConfig().Gui.WorktreesInSeparateGroup {
windowName = "worktrees"
}

return &WorktreesContext{
FilteredListViewModel: viewModel,
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: c.Views().Worktrees,
WindowName: "files",
WindowName: windowName,
Key: WORKTREES_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT,
Focusable: true,
Expand Down
44 changes: 34 additions & 10 deletions pkg/gui/controllers/helpers/window_arrangement_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,9 @@ func getDefaultStashWindowBox(args WindowArrangementArgs) *boxlayout.Box {

func sidePanelChildren(args WindowArrangementArgs) func(width int, height int) []*boxlayout.Box {
return func(width int, height int) []*boxlayout.Box {
hideStatus := args.UserConfig.Gui.HideStatusPanel
separateWorktrees := args.UserConfig.Gui.WorktreesInSeparateGroup

if args.ScreenMode == types.SCREEN_FULL || args.ScreenMode == types.SCREEN_HALF {
fullHeightBox := func(window string) *boxlayout.Box {
if window == args.CurrentSideWindow {
Expand All @@ -436,13 +439,20 @@ func sidePanelChildren(args WindowArrangementArgs) func(width int, height int) [
}
}

return []*boxlayout.Box{
fullHeightBox("status"),
boxes := []*boxlayout.Box{}
if !hideStatus {
boxes = append(boxes, fullHeightBox("status"))
}
if separateWorktrees {
boxes = append(boxes, fullHeightBox("worktrees"))
}
boxes = append(boxes,
fullHeightBox("files"),
fullHeightBox("branches"),
fullHeightBox("commits"),
fullHeightBox("stash"),
}
)
return boxes
} else if height >= 28 {
accordionMode := args.UserConfig.Gui.ExpandFocusedSidePanel
accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
Expand All @@ -456,16 +466,23 @@ func sidePanelChildren(args WindowArrangementArgs) func(width int, height int) [
return defaultBox
}

return []*boxlayout.Box{
{
boxes := []*boxlayout.Box{}
if !hideStatus {
boxes = append(boxes, &boxlayout.Box{
Window: "status",
Size: 3,
},
})
}
if separateWorktrees {
boxes = append(boxes, accordionBox(&boxlayout.Box{Window: "worktrees", Weight: 1}))
}
boxes = append(boxes,
accordionBox(&boxlayout.Box{Window: "files", Weight: 1}),
accordionBox(&boxlayout.Box{Window: "branches", Weight: 1}),
accordionBox(&boxlayout.Box{Window: "commits", Weight: 1}),
accordionBox(getDefaultStashWindowBox(args)),
}
)
return boxes
}

squashedHeight := 1
Expand All @@ -487,12 +504,19 @@ func sidePanelChildren(args WindowArrangementArgs) func(width int, height int) [
}
}

return []*boxlayout.Box{
squashedSidePanelBox("status"),
boxes := []*boxlayout.Box{}
if !hideStatus {
boxes = append(boxes, squashedSidePanelBox("status"))
}
if separateWorktrees {
boxes = append(boxes, squashedSidePanelBox("worktrees"))
}
boxes = append(boxes,
squashedSidePanelBox("files"),
squashedSidePanelBox("branches"),
squashedSidePanelBox("commits"),
squashedSidePanelBox("stash"),
}
)
return boxes
}
}
10 changes: 9 additions & 1 deletion pkg/gui/controllers/helpers/window_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,13 @@ func (self *WindowHelper) WindowForView(viewName string) string {
}

func (self *WindowHelper) SideWindows() []string {
return []string{"status", "files", "branches", "commits", "stash"}
windows := []string{}
if !self.c.UserConfig().Gui.HideStatusPanel {
windows = append(windows, "status")
}
if self.c.UserConfig().Gui.WorktreesInSeparateGroup {
windows = append(windows, "worktrees")
}
windows = append(windows, "files", "branches", "commits", "stash")
return windows
}
11 changes: 7 additions & 4 deletions pkg/gui/controllers/jump_to_side_window_controller.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package controllers

import (
"log"
"fmt"

"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
Expand Down Expand Up @@ -30,16 +31,18 @@ func (self *JumpToSideWindowController) Context() types.Context {

func (self *JumpToSideWindowController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
windows := self.c.Helpers().Window.SideWindows()
jumpBindings := opts.Config.Universal.JumpToBlock

if len(opts.Config.Universal.JumpToBlock) != len(windows) {
log.Fatal("Jump to block keybindings cannot be set. Exactly 5 keybindings must be supplied.")
// Auto-extend jump bindings if there are more windows than bindings
for len(jumpBindings) < len(windows) {
jumpBindings = append(jumpBindings, config.Keybinding{fmt.Sprintf("%d", len(jumpBindings)+1)})
}

return lo.Map(windows, func(window string, index int) *types.Binding {
return &types.Binding{
ViewName: "",
// by default the keys are 1, 2, 3, etc
Keys: opts.GetKeys(opts.Config.Universal.JumpToBlock[index]),
Keys: opts.GetKeys(jumpBindings[index]),
Handler: opts.Guards.NoPopupPanel(self.goToSideWindow(window)),
}
})
Expand Down
37 changes: 25 additions & 12 deletions pkg/gui/gui.go
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,14 @@ func (gui *Gui) onUserConfigLoaded() error {
gui.setColorScheme()
gui.configureViewProperties()

if gui.State != nil && gui.State.Contexts != nil {
if userConfig.Gui.WorktreesInSeparateGroup {
gui.State.Contexts.Worktrees.SetWindowName("worktrees")
} else {
gui.State.Contexts.Worktrees.SetWindowName("files")
}
}

gui.g.SearchEscapeKeys = config.GetValidatedKeyBindingKeys(userConfig.Keybinding.Universal.Return)
gui.g.NextSearchMatchKeys = config.GetValidatedKeyBindingKeys(userConfig.Keybinding.Universal.NextMatch)
gui.g.PrevSearchMatchKeys = config.GetValidatedKeyBindingKeys(userConfig.Keybinding.Universal.PrevMatch)
Expand Down Expand Up @@ -859,20 +867,25 @@ func (gui *Gui) viewTabMap() map[string][]context.TabView {
ViewName: "reflogCommits",
},
},
"files": {
{
Tab: gui.c.Tr.FilesTitle,
ViewName: "files",
},
context.TabView{
Tab: gui.c.Tr.WorktreesTitle,
ViewName: "worktrees",
},
{
"files": func() []context.TabView {
tabs := []context.TabView{
{
Tab: gui.c.Tr.FilesTitle,
ViewName: "files",
},
}
if !gui.c.UserConfig().Gui.WorktreesInSeparateGroup {
tabs = append(tabs, context.TabView{
Tab: gui.c.Tr.WorktreesTitle,
ViewName: "worktrees",
})
}
tabs = append(tabs, context.TabView{
Tab: gui.c.Tr.SubmodulesTitle,
ViewName: "submodules",
},
},
})
return tabs
}(),
}

return result
Expand Down
60 changes: 50 additions & 10 deletions pkg/gui/views.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,24 +218,49 @@ func (gui *Gui) configureViewProperties() {
return fmt.Sprintf("[%s]", binding[0])
}
jumpBindings := gui.c.UserConfig().Keybinding.Universal.JumpToBlock
sideWindows := gui.sideWindows()

// Auto-extend jump bindings if there are more windows than bindings
for len(jumpBindings) < len(sideWindows) {
jumpBindings = append(jumpBindings, config.Keybinding{fmt.Sprintf("%d", len(jumpBindings)+1)})
}

jumpLabels := lo.Map(jumpBindings, func(binding config.Keybinding, _ int) string {
return keyToTitlePrefix(binding)
})

gui.Views.Status.TitlePrefix = jumpLabels[0]
// Build a map from window name to its jump label
windowLabel := func(window string) string {
for i, w := range sideWindows {
if w == window {
return jumpLabels[i]
}
}
return ""
}

gui.Views.Status.TitlePrefix = windowLabel("status")

gui.Views.Files.TitlePrefix = jumpLabels[1]
gui.Views.Worktrees.TitlePrefix = jumpLabels[1]
gui.Views.Submodules.TitlePrefix = jumpLabels[1]
filesLabel := windowLabel("files")
gui.Views.Files.TitlePrefix = filesLabel
gui.Views.Submodules.TitlePrefix = filesLabel

gui.Views.Branches.TitlePrefix = jumpLabels[2]
gui.Views.Remotes.TitlePrefix = jumpLabels[2]
gui.Views.Tags.TitlePrefix = jumpLabels[2]
if gui.c.UserConfig().Gui.WorktreesInSeparateGroup {
gui.Views.Worktrees.TitlePrefix = windowLabel("worktrees")
} else {
gui.Views.Worktrees.TitlePrefix = filesLabel
}

branchesLabel := windowLabel("branches")
gui.Views.Branches.TitlePrefix = branchesLabel
gui.Views.Remotes.TitlePrefix = branchesLabel
gui.Views.Tags.TitlePrefix = branchesLabel

gui.Views.Commits.TitlePrefix = jumpLabels[3]
gui.Views.ReflogCommits.TitlePrefix = jumpLabels[3]
commitsLabel := windowLabel("commits")
gui.Views.Commits.TitlePrefix = commitsLabel
gui.Views.ReflogCommits.TitlePrefix = commitsLabel

gui.Views.Stash.TitlePrefix = jumpLabels[4]
gui.Views.Stash.TitlePrefix = windowLabel("stash")

gui.Views.Main.TitlePrefix = keyToTitlePrefix(gui.c.UserConfig().Keybinding.Universal.FocusMainView)
} else {
Expand Down Expand Up @@ -273,3 +298,18 @@ func (gui *Gui) configureViewProperties() {
}
}
}

// sideWindows returns the list of side windows based on config.
// This duplicates the logic from WindowHelper.SideWindows() so it can be used
// before helpers are initialized.
func (gui *Gui) sideWindows() []string {
windows := []string{}
if !gui.c.UserConfig().Gui.HideStatusPanel {
windows = append(windows, "status")
}
if gui.c.UserConfig().Gui.WorktreesInSeparateGroup {
windows = append(windows, "worktrees")
}
windows = append(windows, "files", "branches", "commits", "stash")
return windows
}
2 changes: 2 additions & 0 deletions pkg/integration/tests/test_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,13 +473,15 @@ var tests = []*components.IntegrationTest{
ui.Accordion,
ui.DisableSwitchTabWithPanelJumpKeys,
ui.EmptyMenu,
ui.HideStatusPanel,
ui.KeybindingSuggestionsDontCrashOnDisabledBindings,
ui.KeybindingSuggestionsWhenSwitchingRepos,
ui.ModeSpecificKeybindingSuggestions,
ui.OpenLinkFailure,
ui.RangeSelect,
ui.SwitchTabFromMenu,
ui.SwitchTabWithPanelJumpKeys,
ui.WorktreesInSeparateGroup,
undo.UndoCheckoutAndDrop,
undo.UndoCommit,
undo.UndoDrop,
Expand Down
41 changes: 41 additions & 0 deletions pkg/integration/tests/ui/hide_status_panel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package ui

import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)

var HideStatusPanel = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Verify status panel is hidden when hideStatusPanel is enabled",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Gui.HideStatusPanel = true
},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("initial commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
// Default focus is files
t.Views().Files().IsFocused()

// Next block should go to branches (skipping status)
t.Views().Files().Press(keys.Universal.NextBlock)
t.Views().Branches().IsFocused()

// Continue forward: commits, stash
t.Views().Branches().Press(keys.Universal.NextBlock)
t.Views().Commits().IsFocused()

t.Views().Commits().Press(keys.Universal.NextBlock)
t.Views().Stash().IsFocused()

// Wrapping forward from stash should go to files (skipping status)
t.Views().Stash().Press(keys.Universal.NextBlock)
t.Views().Files().IsFocused()

// Cycling backwards from files should wrap to stash (skipping status)
t.Views().Files().Press(keys.Universal.PrevBlock)
t.Views().Stash().IsFocused()
},
})
Loading