diff --git a/pkg/gui/context/context.go b/pkg/gui/context/context.go index 7008f7d14..e3584a8af 100644 --- a/pkg/gui/context/context.go +++ b/pkg/gui/context/context.go @@ -31,11 +31,13 @@ const ( MERGE_CONFLICTS_CONTEXT_KEY types.ContextKey = "mergeConflicts" // these shouldn't really be needed for anything but I'm giving them unique keys nonetheless - OPTIONS_CONTEXT_KEY types.ContextKey = "options" - APP_STATUS_CONTEXT_KEY types.ContextKey = "appStatus" - SEARCH_PREFIX_CONTEXT_KEY types.ContextKey = "searchPrefix" - INFORMATION_CONTEXT_KEY types.ContextKey = "information" - LIMIT_CONTEXT_KEY types.ContextKey = "limit" + OPTIONS_CONTEXT_KEY types.ContextKey = "options" + APP_STATUS_CONTEXT_KEY types.ContextKey = "appStatus" + SEARCH_PREFIX_CONTEXT_KEY types.ContextKey = "searchPrefix" + INFORMATION_CONTEXT_KEY types.ContextKey = "information" + LIMIT_CONTEXT_KEY types.ContextKey = "limit" + STATUS_SPACER1_CONTEXT_KEY types.ContextKey = "statusSpacer1" + STATUS_SPACER2_CONTEXT_KEY types.ContextKey = "statusSpacer2" MENU_CONTEXT_KEY types.ContextKey = "menu" CONFIRMATION_CONTEXT_KEY types.ContextKey = "confirmation" @@ -109,12 +111,14 @@ type ContextTree struct { CommandLog types.Context // display contexts - AppStatus types.Context - Options types.Context - SearchPrefix types.Context - Search types.Context - Information types.Context - Limit types.Context + AppStatus types.Context + Options types.Context + SearchPrefix types.Context + Search types.Context + Information types.Context + Limit types.Context + StatusSpacer1 types.Context + StatusSpacer2 types.Context } // the order of this decides which context is initially at the top of its window @@ -156,6 +160,8 @@ func (self *ContextTree) Flatten() []types.Context { self.Search, self.Information, self.Limit, + self.StatusSpacer1, + self.StatusSpacer2, } } diff --git a/pkg/gui/context/setup.go b/pkg/gui/context/setup.go index ecb47d3f8..ec14a5028 100644 --- a/pkg/gui/context/setup.go +++ b/pkg/gui/context/setup.go @@ -138,10 +138,12 @@ func NewContextTree(c *ContextCommon) *ContextTree { Focusable: true, }), ), - Options: NewDisplayContext(OPTIONS_CONTEXT_KEY, c.Views().Options, "options"), - AppStatus: NewDisplayContext(APP_STATUS_CONTEXT_KEY, c.Views().AppStatus, "appStatus"), - SearchPrefix: NewDisplayContext(SEARCH_PREFIX_CONTEXT_KEY, c.Views().SearchPrefix, "searchPrefix"), - Information: NewDisplayContext(INFORMATION_CONTEXT_KEY, c.Views().Information, "information"), - Limit: NewDisplayContext(LIMIT_CONTEXT_KEY, c.Views().Limit, "limit"), + Options: NewDisplayContext(OPTIONS_CONTEXT_KEY, c.Views().Options, "options"), + AppStatus: NewDisplayContext(APP_STATUS_CONTEXT_KEY, c.Views().AppStatus, "appStatus"), + SearchPrefix: NewDisplayContext(SEARCH_PREFIX_CONTEXT_KEY, c.Views().SearchPrefix, "searchPrefix"), + Information: NewDisplayContext(INFORMATION_CONTEXT_KEY, c.Views().Information, "information"), + Limit: NewDisplayContext(LIMIT_CONTEXT_KEY, c.Views().Limit, "limit"), + StatusSpacer1: NewDisplayContext(STATUS_SPACER1_CONTEXT_KEY, c.Views().StatusSpacer1, "statusSpacer1"), + StatusSpacer2: NewDisplayContext(STATUS_SPACER2_CONTEXT_KEY, c.Views().StatusSpacer2, "statusSpacer2"), } } diff --git a/pkg/gui/controllers/helpers/window_arrangement_helper.go b/pkg/gui/controllers/helpers/window_arrangement_helper.go index 7f4261d8b..c459b80c7 100644 --- a/pkg/gui/controllers/helpers/window_arrangement_helper.go +++ b/pkg/gui/controllers/helpers/window_arrangement_helper.go @@ -1,11 +1,15 @@ package helpers import ( + "fmt" + "strings" + "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" + "golang.org/x/exp/slices" ) // In this file we use the boxlayout package, along with knowledge about the app's state, @@ -32,8 +36,6 @@ func NewWindowArrangementHelper( } } -const INFO_SECTION_PADDING = " " - func (self *WindowArrangementHelper) shouldUsePortraitMode(width, height int) bool { switch self.c.UserConfig.Gui.PortraitMode { case "never": @@ -203,31 +205,91 @@ func (self *WindowArrangementHelper) infoSectionChildren(informationStr string, } } - appStatusBox := &boxlayout.Box{Window: "appStatus"} - optionsBox := &boxlayout.Box{Window: "options"} + statusSpacerPrefix := "statusSpacer" + spacerBoxIndex := 0 + maxSpacerBoxIndex := 2 // See pkg/gui/types/views.go + // Returns a box with size 1 to be used as padding between views + spacerBox := func() *boxlayout.Box { + spacerBoxIndex++ - if !self.c.UserConfig.Gui.ShowBottomLine { - optionsBox.Weight = 0 - appStatusBox.Weight = 1 - } else { - optionsBox.Weight = 1 - if self.c.InDemo() { - // app status appears very briefly in demos and dislodges the caption, - // so better not to show it at all - appStatusBox.Size = 0 - } else { - appStatusBox.Size = runewidth.StringWidth(INFO_SECTION_PADDING) + runewidth.StringWidth(appStatus) + if spacerBoxIndex > maxSpacerBoxIndex { + panic("Too many spacer boxes") + } + + return &boxlayout.Box{Window: fmt.Sprintf("%s%d", statusSpacerPrefix, spacerBoxIndex), Size: 1} + } + + // Returns a box with weight 1 to be used as flexible padding between views + flexibleSpacerBox := func() *boxlayout.Box { + spacerBoxIndex++ + + if spacerBoxIndex > maxSpacerBoxIndex { + panic("Too many spacer boxes") + } + + return &boxlayout.Box{Window: fmt.Sprintf("%s%d", statusSpacerPrefix, spacerBoxIndex), Weight: 1} + } + + // Adds spacer boxes inbetween given boxes + insertSpacerBoxes := func(boxes []*boxlayout.Box) []*boxlayout.Box { + for i := len(boxes) - 1; i >= 1; i-- { + // ignore existing spacer boxes + if !strings.HasPrefix(boxes[i].Window, statusSpacerPrefix) { + boxes = slices.Insert(boxes, i, spacerBox()) + } + } + return boxes + } + + // First collect the real views that we want to show, we'll add spacers in + // between at the end + var result []*boxlayout.Box + + if !self.c.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)}) } } - result := []*boxlayout.Box{appStatusBox, optionsBox} + if self.c.UserConfig.Gui.ShowBottomLine { + result = append(result, &boxlayout.Box{Window: "options", Weight: 1}) + } if (!self.c.InDemo() && 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)), - }) + 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)), + }) + } + + if len(result) == 2 && result[0].Window == "appStatus" { + // Only status and information are showing; need to insert a flexible + // spacer between the two, so that information is right-aligned. Note + // that the call to insertSpacerBoxes below will still insert a 1-char + // spacer in addition (right after the flexible one); this is needed for + // the case that there's not enough room, to ensure there's always at + // least one space. + result = slices.Insert(result, 1, flexibleSpacerBox()) + } else if len(result) == 1 { + if result[0].Window == "information" { + // Only information is showing; need to add a flexible spacer so + // that information is right-aligned + result = slices.Insert(result, 0, flexibleSpacerBox()) + } else { + // Only status is showing; need to make it flexible so that it + // extends over the whole width + result[0].Size = 0 + result[0].Weight = 1 + } + } + + if len(result) > 0 { + // If we have at least one view, insert 1-char wide spacer boxes between them. + result = insertSpacerBoxes(result) } return result diff --git a/pkg/gui/types/views.go b/pkg/gui/types/views.go index 69a79f8a0..867dff92e 100644 --- a/pkg/gui/types/views.go +++ b/pkg/gui/types/views.go @@ -34,6 +34,8 @@ type Views struct { AppStatus *gocui.View Search *gocui.View SearchPrefix *gocui.View + StatusSpacer1 *gocui.View + StatusSpacer2 *gocui.View Limit *gocui.View Suggestions *gocui.View Tooltip *gocui.View diff --git a/pkg/gui/views.go b/pkg/gui/views.go index 6ffde7d41..1c9748486 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -55,6 +55,9 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping { {viewPtr: &gui.Views.Search, name: "search"}, // this view shows either the "Search:" prompt when searching, or the "Filter:" prompt when filtering {viewPtr: &gui.Views.SearchPrefix, name: "searchPrefix"}, + // these views contain one space, and are used as spacers between the various views in the bottom line + {viewPtr: &gui.Views.StatusSpacer1, name: "statusSpacer1"}, + {viewPtr: &gui.Views.StatusSpacer2, name: "statusSpacer2"}, // popups. {viewPtr: &gui.Views.CommitMessage, name: "commitMessage"}, @@ -98,6 +101,9 @@ func (gui *Gui) createAllViews() error { gui.Views.SearchPrefix.Frame = false gui.c.SetViewContent(gui.Views.SearchPrefix, gui.Tr.SearchPrefix) + gui.Views.StatusSpacer1.Frame = false + gui.Views.StatusSpacer2.Frame = false + gui.Views.Search.BgColor = gocui.ColorDefault gui.Views.Search.FgColor = gocui.ColorCyan gui.Views.Search.Editable = true