From 3477cbc81f7011de0eff6b37b646c850cfc3ffb3 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 17 Apr 2022 09:27:17 +1000 Subject: [PATCH] better weight distribution in window arrangement --- go.mod | 4 +- go.sum | 8 +- pkg/gui/boxlayout/boxlayout.go | 134 +++++--- pkg/gui/boxlayout/boxlayout_test.go | 295 ++++++++++++++---- vendor/github.com/gdamore/tcell/v2/tscreen.go | 1 + vendor/github.com/jesseduffield/gocui/gui.go | 2 +- vendor/modules.txt | 4 +- 7 files changed, 339 insertions(+), 109 deletions(-) diff --git a/go.mod b/go.mod index 73124a6d1..29671a030 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/integrii/flaggy v1.4.0 github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4 - github.com/jesseduffield/gocui v0.3.1-0.20220416053910-5b19e175bc67 + github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6 github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e github.com/jesseduffield/yaml v2.1.0+incompatible github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 @@ -40,7 +40,7 @@ require ( github.com/emirpasic/gods v1.12.0 // indirect github.com/fatih/color v1.9.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect - github.com/gdamore/tcell/v2 v2.5.0 // indirect + github.com/gdamore/tcell/v2 v2.5.1 // indirect github.com/go-git/gcfg v1.5.0 // indirect github.com/go-git/go-billy/v5 v5.0.0 // indirect github.com/go-logfmt/logfmt v0.5.0 // indirect diff --git a/go.sum b/go.sum index 0a5b19b6c..fc123630f 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= -github.com/gdamore/tcell/v2 v2.5.0 h1:/LA5f/wqTP5mWT79czngibKVVx5wOgdFTIXPQ68fMO8= -github.com/gdamore/tcell/v2 v2.5.0/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo= +github.com/gdamore/tcell/v2 v2.5.1 h1:zc3LPdpK184lBW7syF2a5C6MV827KmErk9jGVnmsl/I= +github.com/gdamore/tcell/v2 v2.5.1/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo= github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= @@ -70,8 +70,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk= github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4 h1:GOQrmaE8i+KEdB8NzAegKYd4tPn/inM0I1uo0NXFerg= github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o= -github.com/jesseduffield/gocui v0.3.1-0.20220416053910-5b19e175bc67 h1:6NIOoR4LMuNEkP6e9P6GVZkzgYZ7rqpfM+LieKqubnA= -github.com/jesseduffield/gocui v0.3.1-0.20220416053910-5b19e175bc67/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU= +github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6 h1:Fmay0Lz21taUpXiIbFkjjIIcn0E5GKwp5UFRuXaOiGQ= +github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU= github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e h1:uw/oo+kg7t/oeMs6sqlAwr85ND/9cpO3up3VxphxY0U= github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e/go.mod h1:u60qdFGXRd36jyEXxetz0vQceQIxzI13lIo3EFUDf4I= github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE= diff --git a/pkg/gui/boxlayout/boxlayout.go b/pkg/gui/boxlayout/boxlayout.go index 36af2b2ab..4eb6f15e6 100644 --- a/pkg/gui/boxlayout/boxlayout.go +++ b/pkg/gui/boxlayout/boxlayout.go @@ -1,6 +1,10 @@ package boxlayout -import "math" +import ( + "github.com/jesseduffield/generics/slices" + "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/samber/lo" +) type Dimensions struct { X0 int @@ -69,45 +73,12 @@ func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions availableSize = height } - // work out size taken up by children - reservedSize := 0 - totalWeight := 0 - for _, child := range children { - // assuming either size or weight are non-zero - reservedSize += child.Size - totalWeight += child.Weight - } - - remainingSize := availableSize - reservedSize - if remainingSize < 0 { - remainingSize = 0 - } - - unitSize := 0 - extraSize := 0 - if totalWeight > 0 { - unitSize = remainingSize / totalWeight - extraSize = remainingSize % totalWeight - } + sizes := calcSizes(children, availableSize) result := map[string]Dimensions{} offset := 0 - for _, child := range children { - var boxSize int - if child.isStatic() { - boxSize = child.Size - // assuming that only one static child can have a size greater than the - // available space. In that case we just crop the size to what's available - if boxSize > availableSize { - boxSize = availableSize - } - } else { - // TODO: consider more evenly distributing the remainder - boxSize = unitSize * child.Weight - boxExtraSize := int(math.Min(float64(extraSize), float64(child.Weight))) - boxSize += boxExtraSize - extraSize -= boxExtraSize - } + for i, child := range children { + boxSize := sizes[i] var resultForChild map[string]Dimensions if direction == COLUMN { @@ -123,6 +94,95 @@ func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions return result } +func calcSizes(boxes []*Box, availableSpace int) []int { + normalizedWeights := normalizeWeights(slices.Map(boxes, func(box *Box) int { return box.Weight })) + + totalWeight := 0 + reservedSpace := 0 + for i, box := range boxes { + if box.isStatic() { + reservedSpace += box.Size + } else { + totalWeight += normalizedWeights[i] + } + } + + dynamicSpace := utils.Max(0, availableSpace-reservedSpace) + + unitSize := 0 + extraSpace := 0 + if totalWeight > 0 { + unitSize = dynamicSpace / totalWeight + extraSpace = dynamicSpace % totalWeight + } + + result := make([]int, len(boxes)) + for i, box := range boxes { + if box.isStatic() { + // assuming that only one static child can have a size greater than the + // available space. In that case we just crop the size to what's available + result[i] = utils.Min(availableSpace, box.Size) + } else { + result[i] = unitSize * normalizedWeights[i] + } + } + + // distribute the remainder across dynamic boxes. + for extraSpace > 0 { + for i, weight := range normalizedWeights { + if weight > 0 { + result[i]++ + extraSpace-- + normalizedWeights[i]-- + + if extraSpace == 0 { + break + } + } + } + } + + return result +} + +// removes common multiple from weights e.g. if we get 2, 4, 4 we return 1, 2, 2. +func normalizeWeights(weights []int) []int { + if len(weights) == 0 { + return []int{} + } + + // to spare us some computation we'll exit early if any of our weights is 1 + if slices.Some(weights, func(weight int) bool { return weight == 1 }) { + return weights + } + + // map weights to factorSlices and find the lowest common factor + positiveWeights := slices.Filter(weights, func(weight int) bool { return weight > 0 }) + factorSlices := slices.Map(positiveWeights, func(weight int) []int { return calcFactors(weight) }) + commonFactors := factorSlices[0] + for _, factors := range factorSlices { + commonFactors = lo.Intersect(commonFactors, factors) + } + + if len(commonFactors) == 0 { + return weights + } + + newWeights := slices.Map(weights, func(weight int) int { return weight / commonFactors[0] }) + + return normalizeWeights(newWeights) +} + +func calcFactors(n int) []int { + factors := []int{} + for i := 2; i <= n; i++ { + if n%i == 0 { + factors = append(factors, i) + } + } + return factors +} + func (b *Box) isStatic() bool { return b.Size > 0 } diff --git a/pkg/gui/boxlayout/boxlayout_test.go b/pkg/gui/boxlayout/boxlayout_test.go index 65e6101f7..c2c0bc9e4 100644 --- a/pkg/gui/boxlayout/boxlayout_test.go +++ b/pkg/gui/boxlayout/boxlayout_test.go @@ -19,24 +19,24 @@ func TestArrangeWindows(t *testing.T) { scenarios := []scenario{ { - "Empty box", - &Box{}, - 0, - 0, - 10, - 10, - func(result map[string]Dimensions) { + testName: "Empty box", + root: &Box{}, + x0: 0, + y0: 0, + width: 10, + height: 10, + test: func(result map[string]Dimensions) { assert.EqualValues(t, result, map[string]Dimensions{}) }, }, { - "Box with static and dynamic panel", - &Box{Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic"}}}, - 0, - 0, - 10, - 10, - func(result map[string]Dimensions) { + testName: "Box with static and dynamic panel", + root: &Box{Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic"}}}, + x0: 0, + y0: 0, + width: 10, + height: 10, + test: func(result map[string]Dimensions) { assert.EqualValues( t, result, @@ -48,13 +48,13 @@ func TestArrangeWindows(t *testing.T) { }, }, { - "Box with static and two dynamic panels", - &Box{Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}}, - 0, - 0, - 10, - 10, - func(result map[string]Dimensions) { + testName: "Box with static and two dynamic panels", + root: &Box{Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}}, + x0: 0, + y0: 0, + width: 10, + height: 10, + test: func(result map[string]Dimensions) { assert.EqualValues( t, result, @@ -67,13 +67,13 @@ func TestArrangeWindows(t *testing.T) { }, }, { - "Box with COLUMN direction", - &Box{Direction: COLUMN, Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}}, - 0, - 0, - 10, - 10, - func(result map[string]Dimensions) { + testName: "Box with COLUMN direction", + root: &Box{Direction: COLUMN, Children: []*Box{{Size: 1, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}}, + x0: 0, + y0: 0, + width: 10, + height: 10, + test: func(result map[string]Dimensions) { assert.EqualValues( t, result, @@ -86,19 +86,19 @@ func TestArrangeWindows(t *testing.T) { }, }, { - "Box with COLUMN direction only on wide boxes with narrow box", - &Box{ConditionalDirection: func(width int, height int) Direction { + testName: "Box with COLUMN direction only on wide boxes with narrow box", + root: &Box{ConditionalDirection: func(width int, height int) Direction { if width > 4 { return COLUMN } else { return ROW } }, Children: []*Box{{Weight: 1, Window: "dynamic1"}, {Weight: 1, Window: "dynamic2"}}}, - 0, - 0, - 4, - 4, - func(result map[string]Dimensions) { + x0: 0, + y0: 0, + width: 4, + height: 4, + test: func(result map[string]Dimensions) { assert.EqualValues( t, result, @@ -110,19 +110,20 @@ func TestArrangeWindows(t *testing.T) { }, }, { - "Box with COLUMN direction only on wide boxes with wide box", - &Box{ConditionalDirection: func(width int, height int) Direction { + testName: "Box with COLUMN direction only on wide boxes with wide box", + root: &Box{ConditionalDirection: func(width int, height int) Direction { if width > 4 { return COLUMN } else { return ROW } }, Children: []*Box{{Weight: 1, Window: "dynamic1"}, {Weight: 1, Window: "dynamic2"}}}, - 0, - 0, - 5, - 5, - func(result map[string]Dimensions) { + // 5 / 2 = 2 remainder 1. That remainder goes to the first box. + x0: 0, + y0: 0, + width: 5, + height: 5, + test: func(result map[string]Dimensions) { assert.EqualValues( t, result, @@ -134,19 +135,19 @@ func TestArrangeWindows(t *testing.T) { }, }, { - "Box with conditional children where box is wide", - &Box{ConditionalChildren: func(width int, height int) []*Box { + testName: "Box with conditional children where box is wide", + root: &Box{ConditionalChildren: func(width int, height int) []*Box { if width > 4 { return []*Box{{Window: "wide", Weight: 1}} } else { return []*Box{{Window: "narrow", Weight: 1}} } }}, - 0, - 0, - 5, - 5, - func(result map[string]Dimensions) { + x0: 0, + y0: 0, + width: 5, + height: 5, + test: func(result map[string]Dimensions) { assert.EqualValues( t, result, @@ -157,19 +158,19 @@ func TestArrangeWindows(t *testing.T) { }, }, { - "Box with conditional children where box is narrow", - &Box{ConditionalChildren: func(width int, height int) []*Box { + testName: "Box with conditional children where box is narrow", + root: &Box{ConditionalChildren: func(width int, height int) []*Box { if width > 4 { return []*Box{{Window: "wide", Weight: 1}} } else { return []*Box{{Window: "narrow", Weight: 1}} } }}, - 0, - 0, - 4, - 4, - func(result map[string]Dimensions) { + x0: 0, + y0: 0, + width: 4, + height: 4, + test: func(result map[string]Dimensions) { assert.EqualValues( t, result, @@ -180,13 +181,13 @@ func TestArrangeWindows(t *testing.T) { }, }, { - "Box with static child with size too large", - &Box{Direction: COLUMN, Children: []*Box{{Size: 11, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}}, - 0, - 0, - 10, - 10, - func(result map[string]Dimensions) { + testName: "Box with static child with size too large", + root: &Box{Direction: COLUMN, Children: []*Box{{Size: 11, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}}, + x0: 0, + y0: 0, + width: 10, + height: 10, + test: func(result map[string]Dimensions) { assert.EqualValues( t, result, @@ -200,6 +201,118 @@ func TestArrangeWindows(t *testing.T) { ) }, }, + { + // 10 total space minus 2 from the status box leaves us with 8. + // Total weight is 3, 8 / 3 = 2 with 2 remainder. + // We want to end up with 2, 3, 5 (one unit from remainder to each dynamic box) + testName: "Distributing remainder across weighted boxes", + root: &Box{Direction: COLUMN, Children: []*Box{{Size: 2, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}}, + x0: 0, + y0: 0, + width: 10, + height: 10, + test: func(result map[string]Dimensions) { + assert.EqualValues( + t, + result, + map[string]Dimensions{ + "static": {X0: 0, X1: 1, Y0: 0, Y1: 9}, // 2 + "dynamic1": {X0: 2, X1: 4, Y0: 0, Y1: 9}, // 3 + "dynamic2": {X0: 5, X1: 9, Y0: 0, Y1: 9}, // 5 + }, + ) + }, + }, + { + // 9 total space. + // total weight is 5, 9 / 5 = 1 with 4 remainder + // we want to give 2 of that remainder to the first, 1 to the second, and 1 to the last. + // Reason being that we just give units to each box evenly and consider weight in subsequent passes. + testName: "Distributing remainder across weighted boxes 2", + root: &Box{Direction: COLUMN, Children: []*Box{{Weight: 2, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}, {Weight: 1, Window: "dynamic3"}}}, + x0: 0, + y0: 0, + width: 9, + height: 10, + test: func(result map[string]Dimensions) { + assert.EqualValues( + t, + result, + map[string]Dimensions{ + "dynamic1": {X0: 0, X1: 3, Y0: 0, Y1: 9}, // 4 + "dynamic2": {X0: 4, X1: 6, Y0: 0, Y1: 9}, // 3 + "dynamic3": {X0: 7, X1: 8, Y0: 0, Y1: 9}, // 2 + }, + ) + }, + }, + { + // 9 total space. + // total weight is 5, 9 / 5 = 1 with 4 remainder + // we want to give 2 of that remainder to the first, 1 to the second, and 1 to the last. + // Reason being that we just give units to each box evenly and consider weight in subsequent passes. + testName: "Distributing remainder across weighted boxes with unnormalized weights", + root: &Box{Direction: COLUMN, Children: []*Box{{Weight: 4, Window: "dynamic1"}, {Weight: 4, Window: "dynamic2"}, {Weight: 2, Window: "dynamic3"}}}, + x0: 0, + y0: 0, + width: 9, + height: 10, + test: func(result map[string]Dimensions) { + assert.EqualValues( + t, + result, + map[string]Dimensions{ + "dynamic1": {X0: 0, X1: 3, Y0: 0, Y1: 9}, // 4 + "dynamic2": {X0: 4, X1: 6, Y0: 0, Y1: 9}, // 3 + "dynamic3": {X0: 7, X1: 8, Y0: 0, Y1: 9}, // 2 + }, + ) + }, + }, + { + testName: "Another distribution test", + root: &Box{Direction: COLUMN, Children: []*Box{ + {Weight: 3, Window: "dynamic1"}, + {Weight: 1, Window: "dynamic2"}, + {Weight: 1, Window: "dynamic3"}, + }}, + x0: 0, + y0: 0, + width: 9, + height: 10, + test: func(result map[string]Dimensions) { + assert.EqualValues( + t, + result, + map[string]Dimensions{ + "dynamic1": {X0: 0, X1: 4, Y0: 0, Y1: 9}, // 5 + "dynamic2": {X0: 5, X1: 6, Y0: 0, Y1: 9}, // 2 + "dynamic3": {X0: 7, X1: 8, Y0: 0, Y1: 9}, // 2 + }, + ) + }, + }, + { + testName: "Box with zero weight", + root: &Box{Direction: COLUMN, Children: []*Box{ + {Weight: 1, Window: "dynamic1"}, + {Weight: 0, Window: "dynamic2"}, + }}, + x0: 0, + y0: 0, + width: 10, + height: 10, + test: func(result map[string]Dimensions) { + assert.EqualValues( + t, + result, + map[string]Dimensions{ + "dynamic1": {X0: 0, X1: 9, Y0: 0, Y1: 9}, + "dynamic2": {X0: 10, X1: 9, Y0: 0, Y1: 9}, // when X0 > X1, we will hide the window + }, + ) + }, + }, } for _, s := range scenarios { @@ -209,3 +322,59 @@ func TestArrangeWindows(t *testing.T) { }) } } + +func TestNormalizeWeights(t *testing.T) { + scenarios := []struct { + testName string + input []int + expected []int + }{ + { + testName: "empty", + input: []int{}, + expected: []int{}, + }, + { + testName: "one item of value 1", + input: []int{1}, + expected: []int{1}, + }, + { + testName: "one item of value greater than 1", + input: []int{2}, + expected: []int{1}, + }, + { + testName: "slice contains 1", + input: []int{2, 1}, + expected: []int{2, 1}, + }, + { + testName: "slice contains 2 and 2", + input: []int{2, 2}, + expected: []int{1, 1}, + }, + { + testName: "no common multiple", + input: []int{2, 3}, + expected: []int{2, 3}, + }, + { + testName: "complex case", + input: []int{10, 10, 20}, + expected: []int{1, 1, 2}, + }, + { + testName: "when a zero weight is included it is ignored", + input: []int{10, 10, 20, 0}, + expected: []int{1, 1, 2, 0}, + }, + } + + for _, s := range scenarios { + s := s + t.Run(s.testName, func(t *testing.T) { + assert.EqualValues(t, s.expected, normalizeWeights(s.input)) + }) + } +} diff --git a/vendor/github.com/gdamore/tcell/v2/tscreen.go b/vendor/github.com/gdamore/tcell/v2/tscreen.go index 2ed73a2fe..f3d85cb40 100644 --- a/vendor/github.com/gdamore/tcell/v2/tscreen.go +++ b/vendor/github.com/gdamore/tcell/v2/tscreen.go @@ -858,6 +858,7 @@ func (t *tScreen) Show() { } func (t *tScreen) clearScreen() { + t.TPuts(t.ti.AttrOff) fg, bg, _ := t.style.Decompose() t.sendFgBg(fg, bg) t.TPuts(t.ti.Clear) diff --git a/vendor/github.com/jesseduffield/gocui/gui.go b/vendor/github.com/jesseduffield/gocui/gui.go index 58e396492..17150c477 100644 --- a/vendor/github.com/jesseduffield/gocui/gui.go +++ b/vendor/github.com/jesseduffield/gocui/gui.go @@ -1053,7 +1053,7 @@ func (g *Gui) draw(v *View) error { return nil } - if !v.Visible || v.y1 < v.y0 { + if !v.Visible || v.y1 < v.y0 || v.x1 < v.x0 { return nil } diff --git a/vendor/modules.txt b/vendor/modules.txt index 68bceb9b0..4079924d6 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -36,7 +36,7 @@ github.com/fsnotify/fsnotify # github.com/gdamore/encoding v1.0.0 ## explicit; go 1.9 github.com/gdamore/encoding -# github.com/gdamore/tcell/v2 v2.5.0 +# github.com/gdamore/tcell/v2 v2.5.1 ## explicit; go 1.12 github.com/gdamore/tcell/v2 github.com/gdamore/tcell/v2/terminfo @@ -169,7 +169,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/filesystem github.com/jesseduffield/go-git/v5/utils/merkletrie/index github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame github.com/jesseduffield/go-git/v5/utils/merkletrie/noder -# github.com/jesseduffield/gocui v0.3.1-0.20220416053910-5b19e175bc67 +# github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6 ## explicit; go 1.12 github.com/jesseduffield/gocui # github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e