1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-01 00:54:58 +02:00

Refactor window arrangement helper to use pure function

This will make it easier to test the file
This commit is contained in:
Jesse Duffield
2023-12-04 17:56:18 +11:00
committed by Stefan Haller
parent b96befa250
commit 8a08abcd35

View File

@ -5,7 +5,7 @@ import (
"strings" "strings"
"github.com/jesseduffield/lazycore/pkg/boxlayout" "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/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
@ -36,40 +36,106 @@ func NewWindowArrangementHelper(
} }
} }
func (self *WindowArrangementHelper) shouldUsePortraitMode(width, height int) bool { type WindowArrangementArgs struct {
switch self.c.UserConfig.Gui.PortraitMode { // 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": case "never":
return false return false
case "always": case "always":
return true return true
default: // "auto" or any garbage values in PortraitMode value 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 { func GetWindowDimensions(args WindowArrangementArgs) map[string]boxlayout.Dimensions {
width, height := self.c.GocuiGui().Size() sideSectionWeight, mainSectionWeight := getMidSectionWeights(args)
sideSectionWeight, mainSectionWeight := self.getMidSectionWeights()
sidePanelsDirection := boxlayout.COLUMN sidePanelsDirection := boxlayout.COLUMN
if self.shouldUsePortraitMode(width, height) { if shouldUsePortraitMode(args) {
sidePanelsDirection = boxlayout.ROW sidePanelsDirection = boxlayout.ROW
} }
mainPanelsDirection := boxlayout.ROW mainPanelsDirection := boxlayout.ROW
if self.splitMainPanelSideBySide() { if splitMainPanelSideBySide(args) {
mainPanelsDirection = boxlayout.COLUMN mainPanelsDirection = boxlayout.COLUMN
} }
extrasWindowSize := self.getExtrasWindowSize(height) extrasWindowSize := getExtrasWindowSize(args)
self.c.Modes().Filtering.Active() showInfoSection := args.UserConfig.Gui.ShowBottomLine ||
args.InSearchPrompt ||
showInfoSection := self.c.UserConfig.Gui.ShowBottomLine || args.IsAnyModeActive ||
self.c.State().GetRepoState().InSearchPrompt() || args.AppStatus != ""
self.modeHelper.IsAnyModeActive() ||
self.appStatusHelper.HasStatus()
infoSectionSize := 0 infoSectionSize := 0
if showInfoSection { if showInfoSection {
infoSectionSize = 1 infoSectionSize = 1
@ -85,7 +151,7 @@ func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string,
{ {
Direction: boxlayout.ROW, Direction: boxlayout.ROW,
Weight: sideSectionWeight, Weight: sideSectionWeight,
ConditionalChildren: self.sidePanelChildren, ConditionalChildren: sidePanelChildren(args),
}, },
{ {
Direction: boxlayout.ROW, Direction: boxlayout.ROW,
@ -93,7 +159,7 @@ func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string,
Children: []*boxlayout.Box{ Children: []*boxlayout.Box{
{ {
Direction: mainPanelsDirection, Direction: mainPanelsDirection,
Children: self.mainSectionChildren(), Children: mainSectionChildren(args),
Weight: 1, Weight: 1,
}, },
{ {
@ -107,13 +173,13 @@ func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string,
{ {
Direction: boxlayout.COLUMN, Direction: boxlayout.COLUMN,
Size: infoSectionSize, Size: infoSectionSize,
Children: self.infoSectionChildren(informationStr, appStatus), Children: infoSectionChildren(args),
}, },
}, },
} }
layerOneWindows := boxlayout.ArrangeWindows(root, 0, 0, width, height) layerOneWindows := boxlayout.ArrangeWindows(root, 0, 0, args.Width, args.Height)
limitWindows := boxlayout.ArrangeWindows(&boxlayout.Box{Window: "limit"}, 0, 0, width, height) limitWindows := boxlayout.ArrangeWindows(&boxlayout.Box{Window: "limit"}, 0, 0, args.Width, args.Height)
return MergeMaps(layerOneWindows, limitWindows) return MergeMaps(layerOneWindows, limitWindows)
} }
@ -129,12 +195,10 @@ func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V {
return result return result
} }
func (self *WindowArrangementHelper) mainSectionChildren() []*boxlayout.Box { func mainSectionChildren(args WindowArrangementArgs) []*boxlayout.Box {
currentWindow := self.windowHelper.CurrentWindow()
// if we're not in split mode we can just show the one main panel. Likewise if // 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 // 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{ return []*boxlayout.Box{
{ {
Window: "main", Window: "main",
@ -155,29 +219,25 @@ func (self *WindowArrangementHelper) mainSectionChildren() []*boxlayout.Box {
} }
} }
func (self *WindowArrangementHelper) getMidSectionWeights() (int, int) { func getMidSectionWeights(args WindowArrangementArgs) (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 // 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 // we could make this better by creating ratios like 2:3 rather than always 1:something
mainSectionWeight := int(1/sidePanelWidthRatio) - 1 mainSectionWeight := int(1/sidePanelWidthRatio) - 1
sideSectionWeight := 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 mainSectionWeight = 5 // need to shrink side panel to make way for main panels if side-by-side
} }
screenMode := self.c.State().GetRepoState().GetScreenMode() if args.CurrentWindow == "main" {
if args.ScreenMode == types.SCREEN_HALF || args.ScreenMode == types.SCREEN_FULL {
if currentWindow == "main" {
if screenMode == types.SCREEN_HALF || screenMode == types.SCREEN_FULL {
sideSectionWeight = 0 sideSectionWeight = 0
} }
} else { } else {
if screenMode == types.SCREEN_HALF { if args.ScreenMode == types.SCREEN_HALF {
mainSectionWeight = 1 mainSectionWeight = 1
} else if screenMode == types.SCREEN_FULL { } else if args.ScreenMode == types.SCREEN_FULL {
mainSectionWeight = 0 mainSectionWeight = 0
} }
} }
@ -185,18 +245,12 @@ func (self *WindowArrangementHelper) getMidSectionWeights() (int, int) {
return sideSectionWeight, mainSectionWeight return sideSectionWeight, mainSectionWeight
} }
func (self *WindowArrangementHelper) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box { func infoSectionChildren(args WindowArrangementArgs) []*boxlayout.Box {
if self.c.State().GetRepoState().InSearchPrompt() { if args.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{ return []*boxlayout.Box{
{ {
Window: "searchPrefix", Window: "searchPrefix",
Size: runewidth.StringWidth(prefix), Size: runewidth.StringWidth(args.SearchPrefix),
}, },
{ {
Window: "search", Window: "search",
@ -245,24 +299,24 @@ func (self *WindowArrangementHelper) infoSectionChildren(informationStr string,
// between at the end // between at the end
var result []*boxlayout.Box var result []*boxlayout.Box
if !self.c.InDemo() { if !args.InDemo {
// app status appears very briefly in demos and dislodges the caption, // app status appears very briefly in demos and dislodges the caption,
// so better not to show it at all // so better not to show it at all
if appStatus != "" { if args.AppStatus != "" {
result = append(result, &boxlayout.Box{Window: "appStatus", Size: runewidth.StringWidth(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}) 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, result = append(result,
&boxlayout.Box{ &boxlayout.Box{
Window: "information", Window: "information",
// unlike appStatus, informationStr has various colors so we need to decolorise before taking the length // 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 return result
} }
func (self *WindowArrangementHelper) splitMainPanelSideBySide() bool { func splitMainPanelSideBySide(args WindowArrangementArgs) bool {
if !self.c.State().GetRepoState().GetSplitMainPanel() { if !args.SplitMainPanel {
return false return false
} }
mainPanelSplitMode := self.c.UserConfig.Gui.MainPanelSplitMode mainPanelSplitMode := args.UserConfig.Gui.MainPanelSplitMode
width, height := self.c.GocuiGui().Size()
switch mainPanelSplitMode { switch mainPanelSplitMode {
case "vertical": case "vertical":
return false return false
case "horizontal": case "horizontal":
return true return true
default: 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 return false
} else { } else {
return true return true
@ -317,18 +369,19 @@ func (self *WindowArrangementHelper) splitMainPanelSideBySide() bool {
} }
} }
func (self *WindowArrangementHelper) getExtrasWindowSize(screenHeight int) int { func getExtrasWindowSize(args WindowArrangementArgs) int {
if !self.c.State().GetShowExtrasWindow() { if !args.ShowExtrasWindow {
return 0 return 0
} }
var baseSize int 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' baseSize = 1000 // my way of saying 'fill the available space'
} else if screenHeight < 40 { } else if args.Height < 40 {
baseSize = 1 baseSize = 1
} else { } else {
baseSize = self.c.UserConfig.Gui.CommandLogSize baseSize = args.UserConfig.Gui.CommandLogSize
} }
frameSize := 2 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 // 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 // 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. // then when it's accessed it will have weight 2, not 1.
func (self *WindowArrangementHelper) getDefaultStashWindowBox() *boxlayout.Box { func getDefaultStashWindowBox(args WindowArrangementArgs) *boxlayout.Box {
stashWindowAccessed := false
self.c.Context().ForEach(func(context types.Context) {
if context.GetWindowName() == "stash" {
stashWindowAccessed = true
}
})
box := &boxlayout.Box{Window: "stash"} box := &boxlayout.Box{Window: "stash"}
// if the stash window is anywhere in our stack we should enlargen it // if the stash window is anywhere in our stack we should enlargen it
if stashWindowAccessed { if args.CurrentSideWindow == "stash" {
box.Weight = 1 box.Weight = 1
} else { } else {
box.Size = 3 box.Size = 3
@ -358,81 +404,80 @@ func (self *WindowArrangementHelper) getDefaultStashWindowBox() *boxlayout.Box {
return box return box
} }
func (self *WindowArrangementHelper) sidePanelChildren(width int, height int) []*boxlayout.Box { func sidePanelChildren(args WindowArrangementArgs) func(width int, height int) []*boxlayout.Box {
currentWindow := self.c.CurrentSideContext().GetWindowName() return func(width int, height int) []*boxlayout.Box {
if args.ScreenMode == types.SCREEN_FULL || args.ScreenMode == types.SCREEN_HALF {
screenMode := self.c.State().GetRepoState().GetScreenMode() fullHeightBox := func(window string) *boxlayout.Box {
if screenMode == types.SCREEN_FULL || screenMode == types.SCREEN_HALF { if window == args.CurrentSideWindow {
fullHeightBox := func(window string) *boxlayout.Box { return &boxlayout.Box{
if window == currentWindow { Window: window,
return &boxlayout.Box{ Weight: 1,
Window: window, }
Weight: 1, } else {
} return &boxlayout.Box{
} else { Window: window,
return &boxlayout.Box{ Size: 0,
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{
} fullHeightBox("status"),
fullHeightBox("files"),
return []*boxlayout.Box{ fullHeightBox("branches"),
{ fullHeightBox("commits"),
Window: "status", fullHeightBox("stash"),
Size: 3, }
}, } else if height >= 28 {
accordionBox(&boxlayout.Box{Window: "files", Weight: 1}), accordionMode := args.UserConfig.Gui.ExpandFocusedSidePanel
accordionBox(&boxlayout.Box{Window: "branches", Weight: 1}), accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
accordionBox(&boxlayout.Box{Window: "commits", Weight: 1}), if accordionMode && defaultBox.Window == args.CurrentSideWindow {
accordionBox(self.getDefaultStashWindowBox()), return &boxlayout.Box{
} Window: defaultBox.Window,
} else { Weight: 2,
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{ return defaultBox
Window: window, }
Size: squashedHeight,
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{ return []*boxlayout.Box{
squashedSidePanelBox("status"), squashedSidePanelBox("status"),
squashedSidePanelBox("files"), squashedSidePanelBox("files"),
squashedSidePanelBox("branches"), squashedSidePanelBox("branches"),
squashedSidePanelBox("commits"), squashedSidePanelBox("commits"),
squashedSidePanelBox("stash"), squashedSidePanelBox("stash"),
}
} }
} }
} }