summaryrefslogtreecommitdiffstats
path: root/pkg/gui/controllers/helpers
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2023-03-23 18:55:41 +1100
committerJesse Duffield <jessedduffield@gmail.com>2023-04-30 13:19:53 +1000
commit4a33fede7bd74451db1e220eb0eb8649b03cbd0a (patch)
tree565bac400ddf2a59e92e733a34f8d618c336b341 /pkg/gui/controllers/helpers
parentdb12853bbe825d69686ea71161497d1bbb120b8e (diff)
move window arrangement helper
Diffstat (limited to 'pkg/gui/controllers/helpers')
-rw-r--r--pkg/gui/controllers/helpers/helpers.go74
-rw-r--r--pkg/gui/controllers/helpers/window_arrangement_helper.go355
2 files changed, 393 insertions, 36 deletions
diff --git a/pkg/gui/controllers/helpers/helpers.go b/pkg/gui/controllers/helpers/helpers.go
index a1da39032..7e54597e5 100644
--- a/pkg/gui/controllers/helpers/helpers.go
+++ b/pkg/gui/controllers/helpers/helpers.go
@@ -34,45 +34,47 @@ type Helpers struct {
AmendHelper *AmendHelper
Snake *SnakeHelper
// lives in context package because our contexts need it to render to main
- Diff *DiffHelper
- Repos *ReposHelper
- RecordDirectory *RecordDirectoryHelper
- Update *UpdateHelper
- Window *WindowHelper
- View *ViewHelper
- Refresh *RefreshHelper
- Confirmation *ConfirmationHelper
- Mode *ModeHelper
- AppStatus *AppStatusHelper
+ Diff *DiffHelper
+ Repos *ReposHelper
+ RecordDirectory *RecordDirectoryHelper
+ Update *UpdateHelper
+ Window *WindowHelper
+ View *ViewHelper
+ Refresh *RefreshHelper
+ Confirmation *ConfirmationHelper
+ Mode *ModeHelper
+ AppStatus *AppStatusHelper
+ WindowArrangement *WindowArrangementHelper
}
func NewStubHelpers() *Helpers {
return &Helpers{
- Refs: &RefsHelper{},
- Bisect: &BisectHelper{},
- Suggestions: &SuggestionsHelper{},
- Files: &FilesHelper{},
- WorkingTree: &WorkingTreeHelper{},
- Tags: &TagsHelper{},
- MergeAndRebase: &MergeAndRebaseHelper{},
- MergeConflicts: &MergeConflictsHelper{},
- CherryPick: &CherryPickHelper{},
- Host: &HostHelper{},
- PatchBuilding: &PatchBuildingHelper{},
- Staging: &StagingHelper{},
- GPG: &GpgHelper{},
- Upstream: &UpstreamHelper{},
- AmendHelper: &AmendHelper{},
- Snake: &SnakeHelper{},
- Diff: &DiffHelper{},
- Repos: &ReposHelper{},
- RecordDirectory: &RecordDirectoryHelper{},
- Update: &UpdateHelper{},
- Window: &WindowHelper{},
- View: &ViewHelper{},
- Refresh: &RefreshHelper{},
- Confirmation: &ConfirmationHelper{},
- Mode: &ModeHelper{},
- AppStatus: &AppStatusHelper{},
+ Refs: &RefsHelper{},
+ Bisect: &BisectHelper{},
+ Suggestions: &SuggestionsHelper{},
+ Files: &FilesHelper{},
+ WorkingTree: &WorkingTreeHelper{},
+ Tags: &TagsHelper{},
+ MergeAndRebase: &MergeAndRebaseHelper{},
+ MergeConflicts: &MergeConflictsHelper{},
+ CherryPick: &CherryPickHelper{},
+ Host: &HostHelper{},
+ PatchBuilding: &PatchBuildingHelper{},
+ Staging: &StagingHelper{},
+ GPG: &GpgHelper{},
+ Upstream: &UpstreamHelper{},
+ AmendHelper: &AmendHelper{},
+ Snake: &SnakeHelper{},
+ Diff: &DiffHelper{},
+ Repos: &ReposHelper{},
+ RecordDirectory: &RecordDirectoryHelper{},
+ Update: &UpdateHelper{},
+ Window: &WindowHelper{},
+ View: &ViewHelper{},
+ Refresh: &RefreshHelper{},
+ Confirmation: &ConfirmationHelper{},
+ Mode: &ModeHelper{},
+ AppStatus: &AppStatusHelper{},
+ WindowArrangement: &WindowArrangementHelper{},
}
}
diff --git a/pkg/gui/controllers/helpers/window_arrangement_helper.go b/pkg/gui/controllers/helpers/window_arrangement_helper.go
new file mode 100644
index 000000000..8efec0a22
--- /dev/null
+++ b/pkg/gui/controllers/helpers/window_arrangement_helper.go
@@ -0,0 +1,355 @@
+package helpers
+
+import (
+ "github.com/jesseduffield/lazycore/pkg/boxlayout"
+ "github.com/jesseduffield/lazygit/pkg/gui/constants"
+ "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().IsSearching() ||
+ 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().IsSearching() {
+ return []*boxlayout.Box{
+ {
+ Window: "searchPrefix",
+ Size: runewidth.StringWidth(constants.SEARCH_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"),
+ }
+ }
+}