From 8a08abcd35c83d6ee13d5959655a156d059d4ff6 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 4 Dec 2023 17:56:18 +1100 Subject: [PATCH] Refactor window arrangement helper to use pure function This will make it easier to test the file --- .../helpers/window_arrangement_helper.go | 325 ++++++++++-------- 1 file changed, 185 insertions(+), 140 deletions(-) diff --git a/pkg/gui/controllers/helpers/window_arrangement_helper.go b/pkg/gui/controllers/helpers/window_arrangement_helper.go index c459b80c7..2923841e8 100644 --- a/pkg/gui/controllers/helpers/window_arrangement_helper.go +++ b/pkg/gui/controllers/helpers/window_arrangement_helper.go @@ -5,7 +5,7 @@ import ( "strings" "github.com/jesseduffield/lazycore/pkg/boxlayout" - "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/mattn/go-runewidth" @@ -36,40 +36,106 @@ func NewWindowArrangementHelper( } } -func (self *WindowArrangementHelper) shouldUsePortraitMode(width, height int) bool { - switch self.c.UserConfig.Gui.PortraitMode { +type WindowArrangementArgs struct { + // Width of the screen (in characters) + Width int + // Height of the screen (in characters) + Height int + // User config + UserConfig *config.UserConfig + // Name of the currently focused window + CurrentWindow string + // Name of the current static window (meaning popups are ignored) + CurrentStaticWindow string + // Name of the current side window (i.e. the current window in the left + // section of the UI) + CurrentSideWindow string + // Whether the main panel is split (as is the case e.g. when a file has both + // staged and unstaged changes) + SplitMainPanel bool + // The current screen mode (normal, half, full) + ScreenMode types.WindowMaximisation + // The content shown on the bottom left of the screen when showing a loader + // or toast e.g. 'Rebasing /' + AppStatus string + // The content shown on the bottom right of the screen (e.g. the 'donate', + // 'ask question' links or a message about the current mode e.g. rebase mode) + InformationStr string + // Whether to show the extras window which contains the command log context + ShowExtrasWindow bool + // Whether we are in a demo (which is used for generating demo gifs for the + // repo's readme) + InDemo bool + // Whether any mode is active (e.g. rebasing, cherry picking, etc) + IsAnyModeActive bool + // Whether the search prompt is shown in the bottom left + InSearchPrompt bool + // One of '' (not searching), 'Search: ', and 'Filter: ' + SearchPrefix string +} + +func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions { + width, height := self.c.GocuiGui().Size() + repoState := self.c.State().GetRepoState() + + var searchPrefix string + if repoState.GetSearchState().SearchType() == types.SearchTypeSearch { + searchPrefix = self.c.Tr.SearchPrefix + } else { + searchPrefix = self.c.Tr.FilterPrefix + } + + args := WindowArrangementArgs{ + Width: width, + Height: height, + UserConfig: self.c.UserConfig, + CurrentWindow: self.windowHelper.CurrentWindow(), + CurrentSideWindow: self.c.CurrentSideContext().GetWindowName(), + CurrentStaticWindow: self.c.CurrentStaticContext().GetWindowName(), + SplitMainPanel: repoState.GetSplitMainPanel(), + ScreenMode: repoState.GetScreenMode(), + AppStatus: appStatus, + InformationStr: informationStr, + ShowExtrasWindow: self.c.State().GetShowExtrasWindow(), + InDemo: self.c.InDemo(), + IsAnyModeActive: self.modeHelper.IsAnyModeActive(), + InSearchPrompt: repoState.InSearchPrompt(), + SearchPrefix: searchPrefix, + } + + return GetWindowDimensions(args) +} + +func shouldUsePortraitMode(args WindowArrangementArgs) bool { + switch args.UserConfig.Gui.PortraitMode { case "never": return false case "always": return true default: // "auto" or any garbage values in PortraitMode value - return width <= 84 && height > 45 + return args.Width <= 84 && args.Height > 45 } } -func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions { - width, height := self.c.GocuiGui().Size() - - sideSectionWeight, mainSectionWeight := self.getMidSectionWeights() +func GetWindowDimensions(args WindowArrangementArgs) map[string]boxlayout.Dimensions { + sideSectionWeight, mainSectionWeight := getMidSectionWeights(args) sidePanelsDirection := boxlayout.COLUMN - if self.shouldUsePortraitMode(width, height) { + if shouldUsePortraitMode(args) { sidePanelsDirection = boxlayout.ROW } mainPanelsDirection := boxlayout.ROW - if self.splitMainPanelSideBySide() { + if splitMainPanelSideBySide(args) { mainPanelsDirection = boxlayout.COLUMN } - extrasWindowSize := self.getExtrasWindowSize(height) + extrasWindowSize := getExtrasWindowSize(args) - self.c.Modes().Filtering.Active() - - showInfoSection := self.c.UserConfig.Gui.ShowBottomLine || - self.c.State().GetRepoState().InSearchPrompt() || - self.modeHelper.IsAnyModeActive() || - self.appStatusHelper.HasStatus() + showInfoSection := args.UserConfig.Gui.ShowBottomLine || + args.InSearchPrompt || + args.IsAnyModeActive || + args.AppStatus != "" infoSectionSize := 0 if showInfoSection { infoSectionSize = 1 @@ -85,7 +151,7 @@ func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string, { Direction: boxlayout.ROW, Weight: sideSectionWeight, - ConditionalChildren: self.sidePanelChildren, + ConditionalChildren: sidePanelChildren(args), }, { Direction: boxlayout.ROW, @@ -93,7 +159,7 @@ func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string, Children: []*boxlayout.Box{ { Direction: mainPanelsDirection, - Children: self.mainSectionChildren(), + Children: mainSectionChildren(args), Weight: 1, }, { @@ -107,13 +173,13 @@ func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string, { Direction: boxlayout.COLUMN, Size: infoSectionSize, - Children: self.infoSectionChildren(informationStr, appStatus), + Children: infoSectionChildren(args), }, }, } - layerOneWindows := boxlayout.ArrangeWindows(root, 0, 0, width, height) - limitWindows := boxlayout.ArrangeWindows(&boxlayout.Box{Window: "limit"}, 0, 0, width, height) + layerOneWindows := boxlayout.ArrangeWindows(root, 0, 0, args.Width, args.Height) + limitWindows := boxlayout.ArrangeWindows(&boxlayout.Box{Window: "limit"}, 0, 0, args.Width, args.Height) return MergeMaps(layerOneWindows, limitWindows) } @@ -129,12 +195,10 @@ func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V { return result } -func (self *WindowArrangementHelper) mainSectionChildren() []*boxlayout.Box { - currentWindow := self.windowHelper.CurrentWindow() - +func mainSectionChildren(args WindowArrangementArgs) []*boxlayout.Box { // 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") { + if !args.SplitMainPanel || (args.ScreenMode == types.SCREEN_FULL && args.CurrentWindow == "main") { return []*boxlayout.Box{ { Window: "main", @@ -155,29 +219,25 @@ func (self *WindowArrangementHelper) mainSectionChildren() []*boxlayout.Box { } } -func (self *WindowArrangementHelper) getMidSectionWeights() (int, int) { - currentWindow := self.windowHelper.CurrentWindow() - +func getMidSectionWeights(args WindowArrangementArgs) (int, int) { // 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 + sidePanelWidthRatio := args.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() { + if splitMainPanelSideBySide(args) { 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 { + if args.CurrentWindow == "main" { + if args.ScreenMode == types.SCREEN_HALF || args.ScreenMode == types.SCREEN_FULL { sideSectionWeight = 0 } } else { - if screenMode == types.SCREEN_HALF { + if args.ScreenMode == types.SCREEN_HALF { mainSectionWeight = 1 - } else if screenMode == types.SCREEN_FULL { + } else if args.ScreenMode == types.SCREEN_FULL { mainSectionWeight = 0 } } @@ -185,18 +245,12 @@ func (self *WindowArrangementHelper) getMidSectionWeights() (int, int) { 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 - } +func infoSectionChildren(args WindowArrangementArgs) []*boxlayout.Box { + if args.InSearchPrompt { return []*boxlayout.Box{ { Window: "searchPrefix", - Size: runewidth.StringWidth(prefix), + Size: runewidth.StringWidth(args.SearchPrefix), }, { Window: "search", @@ -245,24 +299,24 @@ func (self *WindowArrangementHelper) infoSectionChildren(informationStr string, // between at the end var result []*boxlayout.Box - if !self.c.InDemo() { + if !args.InDemo { // app status appears very briefly in demos and dislodges the caption, // so better not to show it at all - if appStatus != "" { - result = append(result, &boxlayout.Box{Window: "appStatus", Size: runewidth.StringWidth(appStatus)}) + if args.AppStatus != "" { + result = append(result, &boxlayout.Box{Window: "appStatus", Size: runewidth.StringWidth(args.AppStatus)}) } } - if self.c.UserConfig.Gui.ShowBottomLine { + if args.UserConfig.Gui.ShowBottomLine { result = append(result, &boxlayout.Box{Window: "options", Weight: 1}) } - if (!self.c.InDemo() && self.c.UserConfig.Gui.ShowBottomLine) || self.modeHelper.IsAnyModeActive() { + if (!args.InDemo && args.UserConfig.Gui.ShowBottomLine) || args.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(utils.Decolorise(informationStr)), + Size: runewidth.StringWidth(utils.Decolorise(args.InformationStr)), }) } @@ -295,21 +349,19 @@ func (self *WindowArrangementHelper) infoSectionChildren(informationStr string, return result } -func (self *WindowArrangementHelper) splitMainPanelSideBySide() bool { - if !self.c.State().GetRepoState().GetSplitMainPanel() { +func splitMainPanelSideBySide(args WindowArrangementArgs) bool { + if !args.SplitMainPanel { return false } - mainPanelSplitMode := self.c.UserConfig.Gui.MainPanelSplitMode - width, height := self.c.GocuiGui().Size() - + mainPanelSplitMode := args.UserConfig.Gui.MainPanelSplitMode 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 + if args.Width < 200 && args.Height > 30 { // 2 80 character width panels + 40 width for side panel return false } else { return true @@ -317,18 +369,19 @@ func (self *WindowArrangementHelper) splitMainPanelSideBySide() bool { } } -func (self *WindowArrangementHelper) getExtrasWindowSize(screenHeight int) int { - if !self.c.State().GetShowExtrasWindow() { +func getExtrasWindowSize(args WindowArrangementArgs) int { + if !args.ShowExtrasWindow { return 0 } var baseSize int - if self.c.CurrentStaticContext().GetKey() == context.COMMAND_LOG_CONTEXT_KEY { + // The 'extras' window contains the command log context + if args.CurrentStaticWindow == "extras" { baseSize = 1000 // my way of saying 'fill the available space' - } else if screenHeight < 40 { + } else if args.Height < 40 { baseSize = 1 } else { - baseSize = self.c.UserConfig.Gui.CommandLogSize + baseSize = args.UserConfig.Gui.CommandLogSize } frameSize := 2 @@ -339,17 +392,10 @@ func (self *WindowArrangementHelper) getExtrasWindowSize(screenHeight int) int { // 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 - } - }) - +func getDefaultStashWindowBox(args WindowArrangementArgs) *boxlayout.Box { box := &boxlayout.Box{Window: "stash"} // if the stash window is anywhere in our stack we should enlargen it - if stashWindowAccessed { + if args.CurrentSideWindow == "stash" { box.Weight = 1 } else { box.Size = 3 @@ -358,81 +404,80 @@ func (self *WindowArrangementHelper) getDefaultStashWindowBox() *boxlayout.Box { 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, +func sidePanelChildren(args WindowArrangementArgs) func(width int, height int) []*boxlayout.Box { + return func(width int, height int) []*boxlayout.Box { + if args.ScreenMode == types.SCREEN_FULL || args.ScreenMode == types.SCREEN_HALF { + fullHeightBox := func(window string) *boxlayout.Box { + if window == args.CurrentSideWindow { + return &boxlayout.Box{ + Window: window, + Weight: 1, + } + } else { + return &boxlayout.Box{ + Window: window, + Size: 0, + } } } - 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, + return []*boxlayout.Box{ + fullHeightBox("status"), + fullHeightBox("files"), + fullHeightBox("branches"), + fullHeightBox("commits"), + fullHeightBox("stash"), + } + } else if height >= 28 { + accordionMode := args.UserConfig.Gui.ExpandFocusedSidePanel + accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box { + if accordionMode && defaultBox.Window == args.CurrentSideWindow { + return &boxlayout.Box{ + Window: defaultBox.Window, + Weight: 2, + } } - } else { - return &boxlayout.Box{ - Window: window, - Size: squashedHeight, + + 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(getDefaultStashWindowBox(args)), + } + } else { + squashedHeight := 1 + if height >= 21 { + squashedHeight = 3 + } + + squashedSidePanelBox := func(window string) *boxlayout.Box { + if window == args.CurrentSideWindow { + 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"), + return []*boxlayout.Box{ + squashedSidePanelBox("status"), + squashedSidePanelBox("files"), + squashedSidePanelBox("branches"), + squashedSidePanelBox("commits"), + squashedSidePanelBox("stash"), + } } } }