Skip to content

Commit

Permalink
Layout the bottom line view using spacer views
Browse files Browse the repository at this point in the history
We are also removing the single-character padding on the left/right edges of the bottom
line because it's unnecessary

Unfortunately we need to create views for each spacer: it's not enough to just
layout the existing views with padding inbetween because gocui only renders
views meaning if there is no view in a given position, that position will just
render whatever was there previously (at least that's what I recall from talking
this through with Stefan: I could be way off).

Co-authored-by: Stefan Haller <[email protected]>
  • Loading branch information
jesseduffield and stefanhaller committed Dec 3, 2023
1 parent 00e140f commit 849524f
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 37 deletions.
34 changes: 23 additions & 11 deletions pkg/gui/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,15 @@ 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"
STATUS_SPACER3_CONTEXT_KEY types.ContextKey = "statusSpacer3"
STATUS_SPACER4_CONTEXT_KEY types.ContextKey = "statusSpacer4"

MENU_CONTEXT_KEY types.ContextKey = "menu"
CONFIRMATION_CONTEXT_KEY types.ContextKey = "confirmation"
Expand Down Expand Up @@ -109,12 +113,16 @@ 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
StatusSpacer3 types.Context
StatusSpacer4 types.Context
}

// the order of this decides which context is initially at the top of its window
Expand Down Expand Up @@ -156,6 +164,10 @@ func (self *ContextTree) Flatten() []types.Context {
self.Search,
self.Information,
self.Limit,
self.StatusSpacer1,
self.StatusSpacer2,
self.StatusSpacer3,
self.StatusSpacer4,
}
}

Expand Down
14 changes: 9 additions & 5 deletions pkg/gui/context/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,14 @@ 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"),
StatusSpacer3: NewDisplayContext(STATUS_SPACER3_CONTEXT_KEY, c.Views().StatusSpacer3, "statusSpacer3"),
StatusSpacer4: NewDisplayContext(STATUS_SPACER4_CONTEXT_KEY, c.Views().StatusSpacer4, "statusSpacer4"),
}
}
102 changes: 81 additions & 21 deletions pkg/gui/controllers/helpers/window_arrangement_helper.go
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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":
Expand Down Expand Up @@ -203,31 +205,89 @@ func (self *WindowArrangementHelper) infoSectionChildren(informationStr string,
}
}

appStatusBox := &boxlayout.Box{Window: "appStatus"}
optionsBox := &boxlayout.Box{Window: "options"}
statusSpacerPrefix := "statusSpacer"
spacerBoxIndex := 0
maxSpacerBoxIndex := 4 // See pkg/gui/types/views.go
// Returns a box with size 1 to be used as padding before, between, or after 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}
}

result := []*boxlayout.Box{appStatusBox, optionsBox}
// Returns a box with weight 1 to be used as flexible padding before, between, or after 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)})
}
}

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 after the 1-width spacer between the two, so that information
// is right-aligned

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
Expand Down
4 changes: 4 additions & 0 deletions pkg/gui/types/views.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ type Views struct {
AppStatus *gocui.View
Search *gocui.View
SearchPrefix *gocui.View
StatusSpacer1 *gocui.View
StatusSpacer2 *gocui.View
StatusSpacer3 *gocui.View
StatusSpacer4 *gocui.View
Limit *gocui.View
Suggestions *gocui.View
Tooltip *gocui.View
Expand Down
10 changes: 10 additions & 0 deletions pkg/gui/views.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ 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 all contain one space, and are used as spacers before, between, and after the various views in the bottom line
{viewPtr: &gui.Views.StatusSpacer1, name: "statusSpacer1"},
{viewPtr: &gui.Views.StatusSpacer2, name: "statusSpacer2"},
{viewPtr: &gui.Views.StatusSpacer3, name: "statusSpacer3"},
{viewPtr: &gui.Views.StatusSpacer4, name: "statusSpacer4"},

// popups.
{viewPtr: &gui.Views.CommitMessage, name: "commitMessage"},
Expand Down Expand Up @@ -98,6 +103,11 @@ 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.StatusSpacer3.Frame = false
gui.Views.StatusSpacer4.Frame = false

gui.Views.Search.BgColor = gocui.ColorDefault
gui.Views.Search.FgColor = gocui.ColorCyan
gui.Views.Search.Editable = true
Expand Down

0 comments on commit 849524f

Please sign in to comment.