package helpers import ( "github.com/jesseduffield/lazycore/pkg/boxlayout" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/mattn/go-runewidth" ) // In this file we use the boxlayout package, along with knowledge about the app's state, // to arrange the windows (i.e. panels) on the screen. type WindowArrangementHelper struct { c *HelperCommon windowHelper *WindowHelper modeHelper *ModeHelper appStatusHelper *AppStatusHelper } func NewWindowArrangementHelper( c *HelperCommon, windowHelper *WindowHelper, modeHelper *ModeHelper, appStatusHelper *AppStatusHelper, ) *WindowArrangementHelper { return &WindowArrangementHelper{ c: c, windowHelper: windowHelper, modeHelper: modeHelper, appStatusHelper: appStatusHelper, } } const INFO_SECTION_PADDING = " " func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions { width, height := self.c.GocuiGui().Size() sideSectionWeight, mainSectionWeight := self.getMidSectionWeights() sidePanelsDirection := boxlayout.COLUMN portraitMode := width <= 84 && height > 45 if portraitMode { sidePanelsDirection = boxlayout.ROW } mainPanelsDirection := boxlayout.ROW if self.splitMainPanelSideBySide() { mainPanelsDirection = boxlayout.COLUMN } extrasWindowSize := self.getExtrasWindowSize(height) self.c.Modes().Filtering.Active() showInfoSection := self.c.UserConfig.Gui.ShowBottomLine || self.c.State().GetRepoState().InSearchPrompt() || self.modeHelper.IsAnyModeActive() || self.appStatusHelper.HasStatus() infoSectionSize := 0 if showInfoSection { infoSectionSize = 1 } root := &boxlayout.Box{ Direction: boxlayout.ROW, Children: []*boxlayout.Box{ { Direction: sidePanelsDirection, Weight: 1, Children: []*boxlayout.Box{ { Direction: boxlayout.ROW, Weight: sideSectionWeight, ConditionalChildren: self.sidePanelChildren, }, { Direction: boxlayout.ROW, Weight: mainSectionWeight, Children: []*boxlayout.Box{ { Direction: mainPanelsDirection, Children: self.mainSectionChildren(), Weight: 1, }, { Window: "extras", Size: extrasWindowSize, }, }, }, }, }, { Direction: boxlayout.COLUMN, Size: infoSectionSize, Children: self.infoSectionChildren(informationStr, appStatus), }, }, } layerOneWindows := boxlayout.ArrangeWindows(root, 0, 0, width, height) limitWindows := boxlayout.ArrangeWindows(&boxlayout.Box{Window: "limit"}, 0, 0, width, height) return MergeMaps(layerOneWindows, limitWindows) } func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V { result := map[K]V{} for _, currMap := range maps { for key, value := range currMap { result[key] = value } } return result } func (self *WindowArrangementHelper) mainSectionChildren() []*boxlayout.Box { currentWindow := self.windowHelper.CurrentWindow() // if we're not in split mode we can just show the one main panel. Likewise if // the main panel is focused and we're in full-screen mode if !self.c.State().GetRepoState().GetSplitMainPanel() || (self.c.State().GetRepoState().GetScreenMode() == types.SCREEN_FULL && currentWindow == "main") { return []*boxlayout.Box{ { Window: "main", Weight: 1, }, } } return []*boxlayout.Box{ { Window: "main", Weight: 1, }, { Window: "secondary", Weight: 1, }, } } func (self *WindowArrangementHelper) getMidSectionWeights() (int, int) { currentWindow := self.windowHelper.CurrentWindow() // we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4 sidePanelWidthRatio := self.c.UserConfig.Gui.SidePanelWidth // we could make this better by creating ratios like 2:3 rather than always 1:something mainSectionWeight := int(1/sidePanelWidthRatio) - 1 sideSectionWeight := 1 if self.splitMainPanelSideBySide() { mainSectionWeight = 5 // need to shrink side panel to make way for main panels if side-by-side } screenMode := self.c.State().GetRepoState().GetScreenMode() if currentWindow == "main" { if screenMode == types.SCREEN_HALF || screenMode == types.SCREEN_FULL { sideSectionWeight = 0 } } else { if screenMode == types.SCREEN_HALF { mainSectionWeight = 1 } else if screenMode == types.SCREEN_FULL { mainSectionWeight = 0 } } return sideSectionWeight, mainSectionWeight } func (self *WindowArrangementHelper) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box { if self.c.State().GetRepoState().InSearchPrompt() { var prefix string if self.c.State().GetRepoState().GetSearchState().SearchType() == types.SearchTypeSearch { prefix = self.c.Tr.SearchPrefix } else { prefix = self.c.Tr.FilterPrefix } return []*boxlayout.Box{ { Window: "searchPrefix", Size: runewidth.StringWidth(prefix), }, { Window: "search", Weight: 1, }, } } appStatusBox := &boxlayout.Box{Window: "appStatus"} optionsBox := &boxlayout.Box{Window: "options"} if !self.c.UserConfig.Gui.ShowBottomLine { optionsBox.Weight = 0 appStatusBox.Weight = 1 } else { optionsBox.Weight = 1 appStatusBox.Size = runewidth.StringWidth(INFO_SECTION_PADDING) + runewidth.StringWidth(appStatus) } result := []*boxlayout.Box{appStatusBox, optionsBox} if self.c.UserConfig.Gui.ShowBottomLine || self.modeHelper.IsAnyModeActive() { result = append(result, &boxlayout.Box{ Window: "information", // unlike appStatus, informationStr has various colors so we need to decolorise before taking the length Size: runewidth.StringWidth(INFO_SECTION_PADDING) + runewidth.StringWidth(utils.Decolorise(informationStr)), }) } return result } func (self *WindowArrangementHelper) splitMainPanelSideBySide() bool { if !self.c.State().GetRepoState().GetSplitMainPanel() { return false } mainPanelSplitMode := self.c.UserConfig.Gui.MainPanelSplitMode width, height := self.c.GocuiGui().Size() switch mainPanelSplitMode { case "vertical": return false case "horizontal": return true default: if width < 200 && height > 30 { // 2 80 character width panels + 40 width for side panel return false } else { return true } } } func (self *WindowArrangementHelper) getExtrasWindowSize(screenHeight int) int { if !self.c.State().GetShowExtrasWindow() { return 0 } var baseSize int if self.c.CurrentStaticContext().GetKey() == context.COMMAND_LOG_CONTEXT_KEY { baseSize = 1000 // my way of saying 'fill the available space' } else if screenHeight < 40 { baseSize = 1 } else { baseSize = self.c.UserConfig.Gui.CommandLogSize } frameSize := 2 return baseSize + frameSize } // The stash window by default only contains one line so that it's not hogging // too much space, but if you access it it should take up some space. This is // the default behaviour when accordion mode is NOT in effect. If it is in effect // then when it's accessed it will have weight 2, not 1. func (self *WindowArrangementHelper) getDefaultStashWindowBox() *boxlayout.Box { stashWindowAccessed := false self.c.Context().ForEach(func(context types.Context) { if context.GetWindowName() == "stash" { stashWindowAccessed = true } }) box := &boxlayout.Box{Window: "stash"} // if the stash window is anywhere in our stack we should enlargen it if stashWindowAccessed { box.Weight = 1 } else { box.Size = 3 } return box } func (self *WindowArrangementHelper) sidePanelChildren(width int, height int) []*boxlayout.Box { currentWindow := self.c.CurrentSideContext().GetWindowName() screenMode := self.c.State().GetRepoState().GetScreenMode() if screenMode == types.SCREEN_FULL || screenMode == types.SCREEN_HALF { fullHeightBox := func(window string) *boxlayout.Box { if window == currentWindow { return &boxlayout.Box{ Window: window, Weight: 1, } } else { return &boxlayout.Box{ Window: window, Size: 0, } } } return []*boxlayout.Box{ fullHeightBox("status"), fullHeightBox("files"), fullHeightBox("branches"), fullHeightBox("commits"), fullHeightBox("stash"), } } else if height >= 28 { accordionMode := self.c.UserConfig.Gui.ExpandFocusedSidePanel accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box { if accordionMode && defaultBox.Window == currentWindow { return &boxlayout.Box{ Window: defaultBox.Window, Weight: 2, } } return defaultBox } return []*boxlayout.Box{ { Window: "status", Size: 3, }, accordionBox(&boxlayout.Box{Window: "files", Weight: 1}), accordionBox(&boxlayout.Box{Window: "branches", Weight: 1}), accordionBox(&boxlayout.Box{Window: "commits", Weight: 1}), accordionBox(self.getDefaultStashWindowBox()), } } else { squashedHeight := 1 if height >= 21 { squashedHeight = 3 } squashedSidePanelBox := func(window string) *boxlayout.Box { if window == currentWindow { return &boxlayout.Box{ Window: window, Weight: 1, } } else { return &boxlayout.Box{ Window: window, Size: squashedHeight, } } } return []*boxlayout.Box{ squashedSidePanelBox("status"), squashedSidePanelBox("files"), squashedSidePanelBox("branches"), squashedSidePanelBox("commits"), squashedSidePanelBox("stash"), } } }