1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-05-15 22:26:40 +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"
"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"),
}
}
}
}