mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-04-27 12:32:37 +02:00
Fix bottom line alignment (#3076)
This commit is contained in:
commit
ca4b8b25f0
@ -31,11 +31,13 @@ const (
|
|||||||
MERGE_CONFLICTS_CONTEXT_KEY types.ContextKey = "mergeConflicts"
|
MERGE_CONFLICTS_CONTEXT_KEY types.ContextKey = "mergeConflicts"
|
||||||
|
|
||||||
// these shouldn't really be needed for anything but I'm giving them unique keys nonetheless
|
// these shouldn't really be needed for anything but I'm giving them unique keys nonetheless
|
||||||
OPTIONS_CONTEXT_KEY types.ContextKey = "options"
|
OPTIONS_CONTEXT_KEY types.ContextKey = "options"
|
||||||
APP_STATUS_CONTEXT_KEY types.ContextKey = "appStatus"
|
APP_STATUS_CONTEXT_KEY types.ContextKey = "appStatus"
|
||||||
SEARCH_PREFIX_CONTEXT_KEY types.ContextKey = "searchPrefix"
|
SEARCH_PREFIX_CONTEXT_KEY types.ContextKey = "searchPrefix"
|
||||||
INFORMATION_CONTEXT_KEY types.ContextKey = "information"
|
INFORMATION_CONTEXT_KEY types.ContextKey = "information"
|
||||||
LIMIT_CONTEXT_KEY types.ContextKey = "limit"
|
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"
|
MENU_CONTEXT_KEY types.ContextKey = "menu"
|
||||||
CONFIRMATION_CONTEXT_KEY types.ContextKey = "confirmation"
|
CONFIRMATION_CONTEXT_KEY types.ContextKey = "confirmation"
|
||||||
@ -109,12 +111,14 @@ type ContextTree struct {
|
|||||||
CommandLog types.Context
|
CommandLog types.Context
|
||||||
|
|
||||||
// display contexts
|
// display contexts
|
||||||
AppStatus types.Context
|
AppStatus types.Context
|
||||||
Options types.Context
|
Options types.Context
|
||||||
SearchPrefix types.Context
|
SearchPrefix types.Context
|
||||||
Search types.Context
|
Search types.Context
|
||||||
Information types.Context
|
Information types.Context
|
||||||
Limit 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
|
// 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.Search,
|
||||||
self.Information,
|
self.Information,
|
||||||
self.Limit,
|
self.Limit,
|
||||||
|
self.StatusSpacer1,
|
||||||
|
self.StatusSpacer2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,10 +138,12 @@ func NewContextTree(c *ContextCommon) *ContextTree {
|
|||||||
Focusable: true,
|
Focusable: true,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Options: NewDisplayContext(OPTIONS_CONTEXT_KEY, c.Views().Options, "options"),
|
Options: NewDisplayContext(OPTIONS_CONTEXT_KEY, c.Views().Options, "options"),
|
||||||
AppStatus: NewDisplayContext(APP_STATUS_CONTEXT_KEY, c.Views().AppStatus, "appStatus"),
|
AppStatus: NewDisplayContext(APP_STATUS_CONTEXT_KEY, c.Views().AppStatus, "appStatus"),
|
||||||
SearchPrefix: NewDisplayContext(SEARCH_PREFIX_CONTEXT_KEY, c.Views().SearchPrefix, "searchPrefix"),
|
SearchPrefix: NewDisplayContext(SEARCH_PREFIX_CONTEXT_KEY, c.Views().SearchPrefix, "searchPrefix"),
|
||||||
Information: NewDisplayContext(INFORMATION_CONTEXT_KEY, c.Views().Information, "information"),
|
Information: NewDisplayContext(INFORMATION_CONTEXT_KEY, c.Views().Information, "information"),
|
||||||
Limit: NewDisplayContext(LIMIT_CONTEXT_KEY, c.Views().Limit, "limit"),
|
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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
package helpers
|
package helpers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"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"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// In this file we use the boxlayout package, along with knowledge about the app's state,
|
// In this file we use the boxlayout package, along with knowledge about the app's state,
|
||||||
@ -32,42 +36,99 @@ func NewWindowArrangementHelper(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const INFO_SECTION_PADDING = " "
|
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) shouldUsePortraitMode(width, height int) bool {
|
func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
|
||||||
switch self.c.UserConfig.Gui.PortraitMode {
|
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
|
showInfoSection := args.UserConfig.Gui.ShowBottomLine ||
|
||||||
if self.splitMainPanelSideBySide() {
|
args.InSearchPrompt ||
|
||||||
mainPanelsDirection = boxlayout.COLUMN
|
args.IsAnyModeActive ||
|
||||||
}
|
args.AppStatus != ""
|
||||||
|
|
||||||
extrasWindowSize := self.getExtrasWindowSize(height)
|
|
||||||
|
|
||||||
self.c.Modes().Filtering.Active()
|
|
||||||
|
|
||||||
showInfoSection := self.c.UserConfig.Gui.ShowBottomLine ||
|
|
||||||
self.c.State().GetRepoState().InSearchPrompt() ||
|
|
||||||
self.modeHelper.IsAnyModeActive() ||
|
|
||||||
self.appStatusHelper.HasStatus()
|
|
||||||
infoSectionSize := 0
|
infoSectionSize := 0
|
||||||
if showInfoSection {
|
if showInfoSection {
|
||||||
infoSectionSize = 1
|
infoSectionSize = 1
|
||||||
@ -83,39 +144,51 @@ 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,
|
||||||
Weight: mainSectionWeight,
|
Weight: mainSectionWeight,
|
||||||
Children: []*boxlayout.Box{
|
Children: mainPanelChildren(args),
|
||||||
{
|
|
||||||
Direction: mainPanelsDirection,
|
|
||||||
Children: self.mainSectionChildren(),
|
|
||||||
Weight: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Window: "extras",
|
|
||||||
Size: extrasWindowSize,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mainPanelChildren(args WindowArrangementArgs) []*boxlayout.Box {
|
||||||
|
mainPanelsDirection := boxlayout.ROW
|
||||||
|
if splitMainPanelSideBySide(args) {
|
||||||
|
mainPanelsDirection = boxlayout.COLUMN
|
||||||
|
}
|
||||||
|
|
||||||
|
result := []*boxlayout.Box{
|
||||||
|
{
|
||||||
|
Direction: mainPanelsDirection,
|
||||||
|
Children: mainSectionChildren(args),
|
||||||
|
Weight: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if args.ShowExtrasWindow {
|
||||||
|
result = append(result, &boxlayout.Box{
|
||||||
|
Window: "extras",
|
||||||
|
Size: getExtrasWindowSize(args),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V {
|
func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V {
|
||||||
result := map[K]V{}
|
result := map[K]V{}
|
||||||
for _, currMap := range maps {
|
for _, currMap := range maps {
|
||||||
@ -127,12 +200,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",
|
||||||
@ -153,29 +224,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,18 +250,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",
|
||||||
@ -203,51 +264,109 @@ func (self *WindowArrangementHelper) infoSectionChildren(informationStr string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appStatusBox := &boxlayout.Box{Window: "appStatus"}
|
statusSpacerPrefix := "statusSpacer"
|
||||||
optionsBox := &boxlayout.Box{Window: "options"}
|
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 {
|
if spacerBoxIndex > maxSpacerBoxIndex {
|
||||||
optionsBox.Weight = 0
|
panic("Too many spacer boxes")
|
||||||
appStatusBox.Weight = 1
|
}
|
||||||
} else {
|
|
||||||
optionsBox.Weight = 1
|
return &boxlayout.Box{Window: fmt.Sprintf("%s%d", statusSpacerPrefix, spacerBoxIndex), Size: 1}
|
||||||
if self.c.InDemo() {
|
}
|
||||||
// app status appears very briefly in demos and dislodges the caption,
|
|
||||||
// so better not to show it at all
|
// Returns a box with weight 1 to be used as flexible padding between views
|
||||||
appStatusBox.Size = 0
|
flexibleSpacerBox := func() *boxlayout.Box {
|
||||||
} else {
|
spacerBoxIndex++
|
||||||
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), 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 !args.InDemo {
|
||||||
|
// app status appears very briefly in demos and dislodges the caption,
|
||||||
|
// so better not to show it at all
|
||||||
|
if args.AppStatus != "" {
|
||||||
|
result = append(result, &boxlayout.Box{Window: "appStatus", Size: runewidth.StringWidth(args.AppStatus)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result := []*boxlayout.Box{appStatusBox, optionsBox}
|
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{
|
result = append(result,
|
||||||
Window: "information",
|
&boxlayout.Box{
|
||||||
// unlike appStatus, informationStr has various colors so we need to decolorise before taking the length
|
Window: "information",
|
||||||
Size: runewidth.StringWidth(INFO_SECTION_PADDING) + runewidth.StringWidth(utils.Decolorise(informationStr)),
|
// unlike appStatus, informationStr has various colors so we need to decolorise before taking the length
|
||||||
})
|
Size: runewidth.StringWidth(utils.Decolorise(args.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
|
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
|
||||||
@ -255,18 +374,15 @@ func (self *WindowArrangementHelper) splitMainPanelSideBySide() bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *WindowArrangementHelper) getExtrasWindowSize(screenHeight int) int {
|
func getExtrasWindowSize(args WindowArrangementArgs) int {
|
||||||
if !self.c.State().GetShowExtrasWindow() {
|
|
||||||
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
|
||||||
@ -277,17 +393,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
|
||||||
@ -296,81 +405,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"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
408
pkg/gui/controllers/helpers/window_arrangement_helper_test.go
Normal file
408
pkg/gui/controllers/helpers/window_arrangement_helper_test.go
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazycore/pkg/boxlayout"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/config"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The best way to add test cases here is to set your args and then get the
|
||||||
|
// test to fail and copy+paste the output into the test case's expected string.
|
||||||
|
// TODO: add more test cases
|
||||||
|
func TestGetWindowDimensions(t *testing.T) {
|
||||||
|
getDefaultArgs := func() WindowArrangementArgs {
|
||||||
|
return WindowArrangementArgs{
|
||||||
|
Width: 75,
|
||||||
|
Height: 30,
|
||||||
|
UserConfig: config.GetDefaultConfig(),
|
||||||
|
CurrentWindow: "files",
|
||||||
|
CurrentSideWindow: "files",
|
||||||
|
CurrentStaticWindow: "files",
|
||||||
|
SplitMainPanel: false,
|
||||||
|
ScreenMode: types.SCREEN_NORMAL,
|
||||||
|
AppStatus: "",
|
||||||
|
InformationStr: "information",
|
||||||
|
ShowExtrasWindow: false,
|
||||||
|
InDemo: false,
|
||||||
|
IsAnyModeActive: false,
|
||||||
|
InSearchPrompt: false,
|
||||||
|
SearchPrefix: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Test struct {
|
||||||
|
name string
|
||||||
|
mutateArgs func(*WindowArrangementArgs)
|
||||||
|
expected string
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []Test{
|
||||||
|
{
|
||||||
|
name: "default",
|
||||||
|
mutateArgs: func(args *WindowArrangementArgs) {},
|
||||||
|
expected: `
|
||||||
|
╭status─────────────────╮╭main────────────────────────────────────────────╮
|
||||||
|
│ ││ │
|
||||||
|
╰───────────────────────╯│ │
|
||||||
|
╭files──────────────────╮│ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
╰───────────────────────╯│ │
|
||||||
|
╭branches───────────────╮│ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
╰───────────────────────╯│ │
|
||||||
|
╭commits────────────────╮│ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
╰───────────────────────╯│ │
|
||||||
|
╭stash──────────────────╮│ │
|
||||||
|
│ ││ │
|
||||||
|
╰───────────────────────╯╰────────────────────────────────────────────────╯
|
||||||
|
<options──────────────────────────────────────────────────────>A<B────────>
|
||||||
|
A: statusSpacer1
|
||||||
|
B: information
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stash focused",
|
||||||
|
mutateArgs: func(args *WindowArrangementArgs) {
|
||||||
|
args.CurrentSideWindow = "stash"
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
╭status─────────────────╮╭main────────────────────────────────────────────╮
|
||||||
|
│ ││ │
|
||||||
|
╰───────────────────────╯│ │
|
||||||
|
╭files──────────────────╮│ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
╰───────────────────────╯│ │
|
||||||
|
╭branches───────────────╮│ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
╰───────────────────────╯│ │
|
||||||
|
╭commits────────────────╮│ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
╰───────────────────────╯│ │
|
||||||
|
╭stash──────────────────╮│ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
│ ││ │
|
||||||
|
╰───────────────────────╯╰────────────────────────────────────────────────╯
|
||||||
|
<options──────────────────────────────────────────────────────>A<B────────>
|
||||||
|
A: statusSpacer1
|
||||||
|
B: information
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "search mode",
|
||||||
|
mutateArgs: func(args *WindowArrangementArgs) {
|
||||||
|
args.InSearchPrompt = true
|
||||||
|
args.SearchPrefix = "Search: "
|
||||||
|
args.Height = 6 // small height cos we only care about the bottom line
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
<status─────────────────>╭main────────────────────────────────────────────╮
|
||||||
|
<files──────────────────>│ │
|
||||||
|
<branches───────────────>│ │
|
||||||
|
<commits────────────────>│ │
|
||||||
|
<stash──────────────────>╰────────────────────────────────────────────────╯
|
||||||
|
<A─────><search───────────────────────────────────────────────────────────>
|
||||||
|
A: searchPrefix
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "app status present",
|
||||||
|
mutateArgs: func(args *WindowArrangementArgs) {
|
||||||
|
args.AppStatus = "Rebasing /"
|
||||||
|
args.Height = 6 // small height cos we only care about the bottom line
|
||||||
|
},
|
||||||
|
// We expect single-character spacers between the windows of the bottom line
|
||||||
|
expected: `
|
||||||
|
<status─────────────────>╭main────────────────────────────────────────────╮
|
||||||
|
<files──────────────────>│ │
|
||||||
|
<branches───────────────>│ │
|
||||||
|
<commits────────────────>│ │
|
||||||
|
<stash──────────────────>╰────────────────────────────────────────────────╯
|
||||||
|
<A───────>B<options───────────────────────────────────────────>C<D────────>
|
||||||
|
A: appStatus
|
||||||
|
B: statusSpacer2
|
||||||
|
C: statusSpacer1
|
||||||
|
D: information
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "information present without options",
|
||||||
|
mutateArgs: func(args *WindowArrangementArgs) {
|
||||||
|
args.Height = 6 // small height cos we only care about the bottom line
|
||||||
|
args.UserConfig.Gui.ShowBottomLine = false // this hides the options window
|
||||||
|
args.IsAnyModeActive = true // this means we show the bottom line despite the user config
|
||||||
|
},
|
||||||
|
// We expect a spacer on the left of the bottom line so that the information
|
||||||
|
// window is right-aligned
|
||||||
|
expected: `
|
||||||
|
<status─────────────────>╭main────────────────────────────────────────────╮
|
||||||
|
<files──────────────────>│ │
|
||||||
|
<branches───────────────>│ │
|
||||||
|
<commits────────────────>│ │
|
||||||
|
<stash──────────────────>╰────────────────────────────────────────────────╯
|
||||||
|
<statusSpacer1────────────────────────────────────────────────>A<B────────>
|
||||||
|
A: statusSpacer2
|
||||||
|
B: information
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "app status present without information or options",
|
||||||
|
mutateArgs: func(args *WindowArrangementArgs) {
|
||||||
|
args.Height = 6 // small height cos we only care about the bottom line
|
||||||
|
args.UserConfig.Gui.ShowBottomLine = false // this hides the options window
|
||||||
|
args.IsAnyModeActive = false
|
||||||
|
args.AppStatus = "Rebasing /"
|
||||||
|
},
|
||||||
|
// We expect the app status window to take up all the available space
|
||||||
|
expected: `
|
||||||
|
<status─────────────────>╭main────────────────────────────────────────────╮
|
||||||
|
<files──────────────────>│ │
|
||||||
|
<branches───────────────>│ │
|
||||||
|
<commits────────────────>│ │
|
||||||
|
<stash──────────────────>╰────────────────────────────────────────────────╯
|
||||||
|
<appStatus────────────────────────────────────────────────────────────────>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "app status present with information but without options",
|
||||||
|
mutateArgs: func(args *WindowArrangementArgs) {
|
||||||
|
args.Height = 6 // small height cos we only care about the bottom line
|
||||||
|
args.UserConfig.Gui.ShowBottomLine = false // this hides the options window
|
||||||
|
args.IsAnyModeActive = true
|
||||||
|
args.AppStatus = "Rebasing /"
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
<status─────────────────>╭main────────────────────────────────────────────╮
|
||||||
|
<files──────────────────>│ │
|
||||||
|
<branches───────────────>│ │
|
||||||
|
<commits────────────────>│ │
|
||||||
|
<stash──────────────────>╰────────────────────────────────────────────────╯
|
||||||
|
<A───────><statusSpacer1──────────────────────────────────────>B<C────────>
|
||||||
|
A: appStatus
|
||||||
|
B: statusSpacer2
|
||||||
|
C: information
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "app status present with very long information but without options",
|
||||||
|
mutateArgs: func(args *WindowArrangementArgs) {
|
||||||
|
args.Height = 6 // small height cos we only care about the bottom line
|
||||||
|
args.Width = 55 // smaller width so that not all bottom line views fit
|
||||||
|
args.UserConfig.Gui.ShowBottomLine = false // this hides the options window
|
||||||
|
args.IsAnyModeActive = true
|
||||||
|
args.AppStatus = "Rebasing /"
|
||||||
|
args.InformationStr = "Showing output for: git diff deadbeef fa1afe1 -- (Reset)"
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
<status───────────>╭main──────────────────────────────╮
|
||||||
|
<files────────────>│ │
|
||||||
|
<branches─────────>│ │
|
||||||
|
<commits──────────>│ │
|
||||||
|
<stash────────────>╰──────────────────────────────────╯
|
||||||
|
<A───────>B<information──────────────────────────────────────────>
|
||||||
|
A: appStatus
|
||||||
|
B: statusSpacer2
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
args := getDefaultArgs()
|
||||||
|
test.mutateArgs(&args)
|
||||||
|
windows := GetWindowDimensions(args)
|
||||||
|
output := renderLayout(windows)
|
||||||
|
// removing tabs so that it's easier to paste the expected output
|
||||||
|
expected := strings.ReplaceAll(test.expected, "\t", "")
|
||||||
|
expected = strings.TrimSpace(expected)
|
||||||
|
if output != expected {
|
||||||
|
fmt.Println(output)
|
||||||
|
t.Errorf("Expected:\n%s\n\nGot:\n%s", expected, output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderLayout(windows map[string]boxlayout.Dimensions) string {
|
||||||
|
// Each window will be represented by a letter.
|
||||||
|
windowMarkers := map[string]string{}
|
||||||
|
shortLabels := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}
|
||||||
|
currentShortLabelIdx := 0
|
||||||
|
windowNames := lo.Keys(windows)
|
||||||
|
// Sort first by name, then by position. This means our short labels will
|
||||||
|
// increment in the order that the windows appear on the screen.
|
||||||
|
slices.Sort(windowNames)
|
||||||
|
slices.SortStableFunc(windowNames, func(a, b string) bool {
|
||||||
|
dimensionsA := windows[a]
|
||||||
|
dimensionsB := windows[b]
|
||||||
|
if dimensionsA.Y0 < dimensionsB.Y0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if dimensionsA.Y0 > dimensionsB.Y0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return dimensionsA.X0 < dimensionsB.X0
|
||||||
|
})
|
||||||
|
|
||||||
|
// Uniquefy windows by dimensions (so perfectly overlapping windows are de-duped). This prevents getting 'fileshes' as a label where the files and branches windows overlap.
|
||||||
|
// branches windows overlap.
|
||||||
|
windowNames = lo.UniqBy(windowNames, func(windowName string) boxlayout.Dimensions {
|
||||||
|
return windows[windowName]
|
||||||
|
})
|
||||||
|
|
||||||
|
// excluding the limit window because it overlaps with everything. In future
|
||||||
|
// we should have a concept of layers and then our test can assert against
|
||||||
|
// each layer.
|
||||||
|
windowNames = lo.Without(windowNames, "limit")
|
||||||
|
|
||||||
|
// get width/height by getting the max values of the dimensions
|
||||||
|
width := 0
|
||||||
|
height := 0
|
||||||
|
for _, dimensions := range windows {
|
||||||
|
if dimensions.X1+1 > width {
|
||||||
|
width = dimensions.X1 + 1
|
||||||
|
}
|
||||||
|
if dimensions.Y1+1 > height {
|
||||||
|
height = dimensions.Y1 + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
screen := make([][]string, height)
|
||||||
|
for i := range screen {
|
||||||
|
screen[i] = make([]string, width)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw each window
|
||||||
|
for _, windowName := range windowNames {
|
||||||
|
dimensions := windows[windowName]
|
||||||
|
|
||||||
|
zeroWidth := dimensions.X0 == dimensions.X1+1
|
||||||
|
if zeroWidth {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
singleRow := dimensions.Y0 == dimensions.Y1
|
||||||
|
oneOrTwoColumns := dimensions.X0 == dimensions.X1 || dimensions.X0+1 == dimensions.X1
|
||||||
|
|
||||||
|
assignShortLabel := func(windowName string) string {
|
||||||
|
windowMarkers[windowName] = shortLabels[currentShortLabelIdx]
|
||||||
|
currentShortLabelIdx++
|
||||||
|
return windowMarkers[windowName]
|
||||||
|
}
|
||||||
|
|
||||||
|
if singleRow {
|
||||||
|
y := dimensions.Y0
|
||||||
|
// If our window only occupies one (or two) columns we'll just use the short
|
||||||
|
// label once (or twice) i.e. 'A' or 'AA'.
|
||||||
|
if oneOrTwoColumns {
|
||||||
|
shortLabel := assignShortLabel(windowName)
|
||||||
|
|
||||||
|
for x := dimensions.X0; x <= dimensions.X1; x++ {
|
||||||
|
screen[y][x] = shortLabel
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
screen[y][dimensions.X0] = "<"
|
||||||
|
screen[y][dimensions.X1] = ">"
|
||||||
|
for x := dimensions.X0 + 1; x < dimensions.X1; x++ {
|
||||||
|
screen[y][x] = "─"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now add the label
|
||||||
|
label := windowName
|
||||||
|
// If we can't fit the label we'll use a one-character short label
|
||||||
|
if len(label) > dimensions.X1-dimensions.X0-1 {
|
||||||
|
label = assignShortLabel(windowName)
|
||||||
|
}
|
||||||
|
for i, char := range label {
|
||||||
|
screen[y][dimensions.X0+1+i] = string(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Draw box border
|
||||||
|
for y := dimensions.Y0; y <= dimensions.Y1; y++ {
|
||||||
|
for x := dimensions.X0; x <= dimensions.X1; x++ {
|
||||||
|
if x == dimensions.X0 && y == dimensions.Y0 {
|
||||||
|
screen[y][x] = "╭"
|
||||||
|
} else if x == dimensions.X1 && y == dimensions.Y0 {
|
||||||
|
screen[y][x] = "╮"
|
||||||
|
} else if x == dimensions.X0 && y == dimensions.Y1 {
|
||||||
|
screen[y][x] = "╰"
|
||||||
|
} else if x == dimensions.X1 && y == dimensions.Y1 {
|
||||||
|
screen[y][x] = "╯"
|
||||||
|
} else if y == dimensions.Y0 || y == dimensions.Y1 {
|
||||||
|
screen[y][x] = "─"
|
||||||
|
} else if x == dimensions.X0 || x == dimensions.X1 {
|
||||||
|
screen[y][x] = "│"
|
||||||
|
} else {
|
||||||
|
screen[y][x] = " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the label
|
||||||
|
label := windowName
|
||||||
|
// If we can't fit the label we'll use a one-character short label
|
||||||
|
if len(label) > dimensions.X1-dimensions.X0-1 {
|
||||||
|
label = assignShortLabel(windowName)
|
||||||
|
}
|
||||||
|
for i, char := range label {
|
||||||
|
screen[dimensions.Y0][dimensions.X0+1+i] = string(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the screen
|
||||||
|
output := ""
|
||||||
|
for _, row := range screen {
|
||||||
|
for _, marker := range row {
|
||||||
|
output += marker
|
||||||
|
}
|
||||||
|
output += "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a legend
|
||||||
|
for _, windowName := range windowNames {
|
||||||
|
if !lo.Contains(lo.Keys(windowMarkers), windowName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
marker := windowMarkers[windowName]
|
||||||
|
output += fmt.Sprintf("%s: %s\n", marker, windowName)
|
||||||
|
}
|
||||||
|
|
||||||
|
output = strings.TrimSpace(output)
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
@ -34,6 +34,8 @@ type Views struct {
|
|||||||
AppStatus *gocui.View
|
AppStatus *gocui.View
|
||||||
Search *gocui.View
|
Search *gocui.View
|
||||||
SearchPrefix *gocui.View
|
SearchPrefix *gocui.View
|
||||||
|
StatusSpacer1 *gocui.View
|
||||||
|
StatusSpacer2 *gocui.View
|
||||||
Limit *gocui.View
|
Limit *gocui.View
|
||||||
Suggestions *gocui.View
|
Suggestions *gocui.View
|
||||||
Tooltip *gocui.View
|
Tooltip *gocui.View
|
||||||
|
@ -53,8 +53,11 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping {
|
|||||||
{viewPtr: &gui.Views.AppStatus, name: "appStatus"},
|
{viewPtr: &gui.Views.AppStatus, name: "appStatus"},
|
||||||
{viewPtr: &gui.Views.Information, name: "information"},
|
{viewPtr: &gui.Views.Information, name: "information"},
|
||||||
{viewPtr: &gui.Views.Search, name: "search"},
|
{viewPtr: &gui.Views.Search, name: "search"},
|
||||||
// this view takes up one character. Its only purpose is to show the slash when searching
|
// this view shows either the "Search:" prompt when searching, or the "Filter:" prompt when filtering
|
||||||
{viewPtr: &gui.Views.SearchPrefix, name: "searchPrefix"},
|
{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.
|
// popups.
|
||||||
{viewPtr: &gui.Views.CommitMessage, name: "commitMessage"},
|
{viewPtr: &gui.Views.CommitMessage, name: "commitMessage"},
|
||||||
@ -98,6 +101,9 @@ func (gui *Gui) createAllViews() error {
|
|||||||
gui.Views.SearchPrefix.Frame = false
|
gui.Views.SearchPrefix.Frame = false
|
||||||
gui.c.SetViewContent(gui.Views.SearchPrefix, gui.Tr.SearchPrefix)
|
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.BgColor = gocui.ColorDefault
|
||||||
gui.Views.Search.FgColor = gocui.ColorCyan
|
gui.Views.Search.FgColor = gocui.ColorCyan
|
||||||
gui.Views.Search.Editable = true
|
gui.Views.Search.Editable = true
|
||||||
|
Loading…
x
Reference in New Issue
Block a user