mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-12 08:36:25 -04:00
Accordion mode expands the focused side panel, but when that panel has little content (an empty Files panel, a Branches panel with only master) it just fills the extra height with blank space. The same waste happens for any panel that gets more height than it has content to show. When this option is enabled, each side panel is sized to its own content (plus a blank line, so it's clear there's nothing more below) rather than to an equal share of the height. The height a small panel gives up flows to the panels that have more content than fits; those grow up to their content and then scroll, weighted toward the focused panel in accordion mode so the two features compose. Only when every panel fits with room to spare is the leftover shared out equally, regardless of focus: enlarging the focused panel there would reveal no more content and would only make the panels jump around as the focus moves. The option is independent of expandFocusedSidePanel and off by default. The status panel, and the stash panel when unfocused, keep their fixed one-line height as before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1069 lines
72 KiB
Go
1069 lines
72 KiB
Go
package helpers
|
|
|
|
import (
|
|
"cmp"
|
|
"fmt"
|
|
"slices"
|
|
"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"
|
|
)
|
|
|
|
// contentHeights builds a ContentHeightForWindow function from a map of window
|
|
// name to content height; windows not in the map report a height of 0.
|
|
func contentHeights(heights map[string]int) func(window string) int {
|
|
return func(window string) int {
|
|
return heights[window]
|
|
}
|
|
}
|
|
|
|
// 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",
|
|
// Each panel shows its first tab by default; for the special-cased
|
|
// panels (status, stash) the view name matches the window name.
|
|
ActiveViewForWindow: func(window string) string { return window },
|
|
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: "worktrees promoted to its own side panel",
|
|
mutateArgs: func(args *WindowArrangementArgs) {
|
|
args.UserConfig.Gui.SidePanels = []config.SidePanel{
|
|
{"status"},
|
|
{"files", "submodules"},
|
|
{"worktrees"},
|
|
{"branches", "remotes", "tags"},
|
|
{"commits", "reflog"},
|
|
{"stash"},
|
|
}
|
|
},
|
|
expected: `
|
|
╭status─────────────────╮╭main────────────────────────────────────────────╮
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭files──────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭worktrees──────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭branches───────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭commits────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭stash──────────────────╮│ │
|
|
│ ││ │
|
|
╰───────────────────────╯╰────────────────────────────────────────────────╯
|
|
<options──────────────────────────────────────────────────────>A<B────────>
|
|
A: statusSpacer1
|
|
B: information
|
|
`,
|
|
},
|
|
{
|
|
name: "stash side panel hidden",
|
|
mutateArgs: func(args *WindowArrangementArgs) {
|
|
args.UserConfig.Gui.SidePanels = []config.SidePanel{
|
|
{"status"},
|
|
{"files", "worktrees", "submodules"},
|
|
{"branches", "remotes", "tags"},
|
|
{"commits", "reflog"},
|
|
}
|
|
},
|
|
expected: `
|
|
╭status─────────────────╮╭main────────────────────────────────────────────╮
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭files──────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭branches───────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭commits────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯╰────────────────────────────────────────────────╯
|
|
<options──────────────────────────────────────────────────────>A<B────────>
|
|
A: statusSpacer1
|
|
B: information
|
|
`,
|
|
},
|
|
{
|
|
name: "stash leading a grouped panel doesn't squash its other tabs",
|
|
mutateArgs: func(args *WindowArrangementArgs) {
|
|
args.UserConfig.Gui.SidePanels = []config.SidePanel{
|
|
{"status"},
|
|
{"files", "worktrees", "submodules"},
|
|
{"stash", "branches", "remotes", "tags"},
|
|
{"commits", "reflog"},
|
|
}
|
|
// The third panel is named after its first tab, stash, but is
|
|
// currently showing the branches tab, which must get full height
|
|
// rather than stash's compact height.
|
|
args.ActiveViewForWindow = func(window string) string {
|
|
if window == "stash" {
|
|
return "branches"
|
|
}
|
|
return window
|
|
}
|
|
},
|
|
expected: `
|
|
╭status─────────────────╮╭main────────────────────────────────────────────╮
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭files──────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭stash──────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭commits────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯╰────────────────────────────────────────────────╯
|
|
<options──────────────────────────────────────────────────────>A<B────────>
|
|
A: statusSpacer1
|
|
B: information
|
|
`,
|
|
},
|
|
{
|
|
name: "expandFocusedSidePanel",
|
|
mutateArgs: func(args *WindowArrangementArgs) {
|
|
args.UserConfig.Gui.ExpandFocusedSidePanel = true
|
|
},
|
|
expected: `
|
|
╭status─────────────────╮╭main────────────────────────────────────────────╮
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭files──────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭branches───────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭commits────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭stash──────────────────╮│ │
|
|
│ ││ │
|
|
╰───────────────────────╯╰────────────────────────────────────────────────╯
|
|
<options──────────────────────────────────────────────────────>A<B────────>
|
|
A: statusSpacer1
|
|
B: information
|
|
`,
|
|
},
|
|
{
|
|
name: "expandSidePanelWeight",
|
|
mutateArgs: func(args *WindowArrangementArgs) {
|
|
args.UserConfig.Gui.ExpandFocusedSidePanel = true
|
|
args.UserConfig.Gui.ExpandedSidePanelWeight = 4
|
|
},
|
|
expected: `
|
|
╭status─────────────────╮╭main────────────────────────────────────────────╮
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭files──────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭branches───────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭commits────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭stash──────────────────╮│ │
|
|
│ ││ │
|
|
╰───────────────────────╯╰────────────────────────────────────────────────╯
|
|
<options──────────────────────────────────────────────────────>A<B────────>
|
|
A: statusSpacer1
|
|
B: information
|
|
`,
|
|
},
|
|
{
|
|
name: "0.5 SidePanelWidth",
|
|
mutateArgs: func(args *WindowArrangementArgs) {
|
|
args.UserConfig.Gui.SidePanelWidth = 0.5
|
|
},
|
|
expected: `
|
|
╭status──────────────────────────────╮╭main───────────────────────────────╮
|
|
│ ││ │
|
|
╰────────────────────────────────────╯│ │
|
|
╭files───────────────────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰────────────────────────────────────╯│ │
|
|
╭branches────────────────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰────────────────────────────────────╯│ │
|
|
╭commits─────────────────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰────────────────────────────────────╯│ │
|
|
╭stash───────────────────────────────╮│ │
|
|
│ ││ │
|
|
╰────────────────────────────────────╯╰───────────────────────────────────╯
|
|
<options──────────────────────────────────────────────────────>A<B────────>
|
|
A: statusSpacer1
|
|
B: information
|
|
`,
|
|
},
|
|
{
|
|
name: "0.8 SidePanelWidth",
|
|
mutateArgs: func(args *WindowArrangementArgs) {
|
|
args.UserConfig.Gui.SidePanelWidth = 0.8
|
|
},
|
|
expected: `
|
|
╭status────────────────────────────────────────────────────╮╭main─────────╮
|
|
│ ││ │
|
|
╰──────────────────────────────────────────────────────────╯│ │
|
|
╭files─────────────────────────────────────────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰──────────────────────────────────────────────────────────╯│ │
|
|
╭branches──────────────────────────────────────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰──────────────────────────────────────────────────────────╯│ │
|
|
╭commits───────────────────────────────────────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰──────────────────────────────────────────────────────────╯│ │
|
|
╭stash─────────────────────────────────────────────────────╮│ │
|
|
│ ││ │
|
|
╰──────────────────────────────────────────────────────────╯╰─────────────╯
|
|
<options──────────────────────────────────────────────────────>A<B────────>
|
|
A: statusSpacer1
|
|
B: information
|
|
`,
|
|
},
|
|
{
|
|
name: "half screen mode, enlargedSideViewLocation left",
|
|
mutateArgs: func(args *WindowArrangementArgs) {
|
|
args.Height = 20 // smaller height because we don't need more here
|
|
args.ScreenMode = types.SCREEN_HALF
|
|
args.UserConfig.Gui.EnlargedSideViewLocation = "left"
|
|
},
|
|
expected: `
|
|
╭status──────────────────────────────╮╭main───────────────────────────────╮
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰────────────────────────────────────╯╰───────────────────────────────────╯
|
|
<options──────────────────────────────────────────────────────>A<B────────>
|
|
A: statusSpacer1
|
|
B: information
|
|
`,
|
|
},
|
|
{
|
|
name: "half screen mode, enlargedSideViewLocation top",
|
|
mutateArgs: func(args *WindowArrangementArgs) {
|
|
args.Height = 20 // smaller height because we don't need more here
|
|
args.ScreenMode = types.SCREEN_HALF
|
|
args.UserConfig.Gui.EnlargedSideViewLocation = "top"
|
|
},
|
|
expected: `
|
|
╭status───────────────────────────────────────────────────────────────────╮
|
|
│ │
|
|
│ │
|
|
│ │
|
|
│ │
|
|
│ │
|
|
╰─────────────────────────────────────────────────────────────────────────╯
|
|
╭main─────────────────────────────────────────────────────────────────────╮
|
|
│ │
|
|
│ │
|
|
│ │
|
|
│ │
|
|
│ │
|
|
│ │
|
|
│ │
|
|
│ │
|
|
│ │
|
|
│ │
|
|
╰─────────────────────────────────────────────────────────────────────────╯
|
|
<options──────────────────────────────────────────────────────>A<B────────>
|
|
A: statusSpacer1
|
|
B: information
|
|
`,
|
|
},
|
|
{
|
|
name: "portrait auto mode, enabled",
|
|
mutateArgs: func(args *WindowArrangementArgs) {
|
|
args.Width = 50
|
|
args.Height = 20
|
|
args.UserConfig.Gui.PortraitModeAutoMaxWidth = 50
|
|
args.UserConfig.Gui.PortraitModeAutoMinHeight = 20
|
|
},
|
|
expected: `
|
|
<status──────────────────────────────────────────>
|
|
╭files───────────────────────────────────────────╮
|
|
│ │
|
|
╰────────────────────────────────────────────────╯
|
|
<branches────────────────────────────────────────>
|
|
<commits─────────────────────────────────────────>
|
|
<stash───────────────────────────────────────────>
|
|
╭main────────────────────────────────────────────╮
|
|
│ │
|
|
│ │
|
|
│ │
|
|
│ │
|
|
│ │
|
|
│ │
|
|
│ │
|
|
│ │
|
|
│ │
|
|
│ │
|
|
╰────────────────────────────────────────────────╯
|
|
<options─────────────────────────────>A<B────────>
|
|
A: statusSpacer1
|
|
B: information
|
|
`,
|
|
},
|
|
{
|
|
name: "portrait auto mode, disabled because width is too large",
|
|
mutateArgs: func(args *WindowArrangementArgs) {
|
|
args.Width = 50
|
|
args.Height = 20
|
|
args.UserConfig.Gui.PortraitModeAutoMaxWidth = 49
|
|
args.UserConfig.Gui.PortraitModeAutoMinHeight = 20
|
|
},
|
|
expected: `
|
|
<status─────────>╭main───────────────────────────╮
|
|
╭files──────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────╯│ │
|
|
<branches───────>│ │
|
|
<commits────────>│ │
|
|
<stash──────────>╰───────────────────────────────╯
|
|
<options─────────────────────────────>A<B────────>
|
|
A: statusSpacer1
|
|
B: information
|
|
`,
|
|
},
|
|
{
|
|
name: "portrait auto mode, disabled because height is too small",
|
|
mutateArgs: func(args *WindowArrangementArgs) {
|
|
args.Width = 50
|
|
args.Height = 20
|
|
args.UserConfig.Gui.PortraitModeAutoMaxWidth = 50
|
|
args.UserConfig.Gui.PortraitModeAutoMinHeight = 21
|
|
},
|
|
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
|
|
`,
|
|
},
|
|
{
|
|
name: "shrink to content, one panel overflows",
|
|
mutateArgs: func(args *WindowArrangementArgs) {
|
|
args.UserConfig.Gui.ShrinkSidePanelsToContent = true
|
|
args.ContentHeightForWindow = contentHeights(map[string]int{
|
|
"files": 2,
|
|
"branches": 1,
|
|
"commits": 100,
|
|
})
|
|
},
|
|
expected: `
|
|
╭status─────────────────╮╭main────────────────────────────────────────────╮
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭files──────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭branches───────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭commits────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭stash──────────────────╮│ │
|
|
│ ││ │
|
|
╰───────────────────────╯╰────────────────────────────────────────────────╯
|
|
<options──────────────────────────────────────────────────────>A<B────────>
|
|
A: statusSpacer1
|
|
B: information
|
|
`,
|
|
},
|
|
{
|
|
name: "shrink to content, everything fits with room to spare",
|
|
mutateArgs: func(args *WindowArrangementArgs) {
|
|
args.UserConfig.Gui.ShrinkSidePanelsToContent = true
|
|
args.ContentHeightForWindow = contentHeights(map[string]int{
|
|
"files": 2,
|
|
"branches": 1,
|
|
"commits": 3,
|
|
})
|
|
},
|
|
expected: `
|
|
╭status─────────────────╮╭main────────────────────────────────────────────╮
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭files──────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭branches───────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭commits────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭stash──────────────────╮│ │
|
|
│ ││ │
|
|
╰───────────────────────╯╰────────────────────────────────────────────────╯
|
|
<options──────────────────────────────────────────────────────>A<B────────>
|
|
A: statusSpacer1
|
|
B: information
|
|
`,
|
|
},
|
|
{
|
|
name: "shrink to content, accordion doesn't resize panels when everything fits",
|
|
mutateArgs: func(args *WindowArrangementArgs) {
|
|
args.UserConfig.Gui.ShrinkSidePanelsToContent = true
|
|
args.UserConfig.Gui.ExpandFocusedSidePanel = true
|
|
args.CurrentSideWindow = "branches"
|
|
args.ContentHeightForWindow = contentHeights(map[string]int{
|
|
"files": 2,
|
|
"branches": 1,
|
|
"commits": 3,
|
|
})
|
|
},
|
|
expected: `
|
|
╭status─────────────────╮╭main────────────────────────────────────────────╮
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭files──────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭branches───────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭commits────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭stash──────────────────╮│ │
|
|
│ ││ │
|
|
╰───────────────────────╯╰────────────────────────────────────────────────╯
|
|
<options──────────────────────────────────────────────────────>A<B────────>
|
|
A: statusSpacer1
|
|
B: information
|
|
`,
|
|
},
|
|
{
|
|
name: "shrink to content, empty panel keeps two rows rather than one",
|
|
mutateArgs: func(args *WindowArrangementArgs) {
|
|
args.UserConfig.Gui.ShrinkSidePanelsToContent = true
|
|
args.ContentHeightForWindow = contentHeights(map[string]int{
|
|
"files": 0,
|
|
"branches": 1,
|
|
"commits": 100,
|
|
})
|
|
},
|
|
expected: `
|
|
╭status─────────────────╮╭main────────────────────────────────────────────╮
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭files──────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭branches───────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭commits────────────────╮│ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
│ ││ │
|
|
╰───────────────────────╯│ │
|
|
╭stash──────────────────╮│ │
|
|
│ ││ │
|
|
╰───────────────────────╯╰────────────────────────────────────────────────╯
|
|
<options──────────────────────────────────────────────────────>A<B────────>
|
|
A: statusSpacer1
|
|
B: information
|
|
`,
|
|
},
|
|
}
|
|
|
|
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) int {
|
|
dimensionsA := windows[a]
|
|
dimensionsB := windows[b]
|
|
if dimensionsA.Y0 != dimensionsB.Y0 {
|
|
return cmp.Compare(dimensionsA.Y0, dimensionsB.Y0)
|
|
}
|
|
return cmp.Compare(dimensionsA.X0, dimensionsB.X0)
|
|
})
|
|
|
|
// Uniquify 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
|
|
}
|