From badc43b225d4379588cdadbf818e5cbdbc89e53b Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 10:18:20 -0700 Subject: [PATCH 01/10] allow changing window size --- go.mod | 9 +- go.sum | 17 +- pkg/config/app_config.go | 21 +- pkg/gui/arrangement.go | 237 ++++ pkg/gui/gui.go | 17 + pkg/gui/keybindings.go | 18 + pkg/gui/layout.go | 239 +--- pkg/gui/theme.go | 4 +- pkg/gui/view_helpers.go | 52 + pkg/gui/views.go | 128 ++ pkg/gui/window.go | 7 + pkg/i18n/english.go | 6 + scripts/bump_gocui.sh | 5 + scripts/bump_lazycore.sh | 5 + .../github.com/jesseduffield/lazycore/LICENSE | 21 + .../lazycore/pkg/boxlayout/boxlayout.go | 211 +++ .../jesseduffield/lazycore/pkg/utils/utils.go | 16 + vendor/github.com/samber/lo/CHANGELOG.md | 124 +- vendor/github.com/samber/lo/Makefile | 11 +- vendor/github.com/samber/lo/README.md | 1196 ++++++++++++++--- vendor/github.com/samber/lo/channel.go | 228 ++++ vendor/github.com/samber/lo/condition.go | 22 + .../github.com/samber/lo/docker-compose.yml | 4 +- vendor/github.com/samber/lo/errors.go | 236 +++- vendor/github.com/samber/lo/find.go | 158 ++- vendor/github.com/samber/lo/func.go | 8 + vendor/github.com/samber/lo/intersect.go | 25 + vendor/github.com/samber/lo/map.go | 42 +- vendor/github.com/samber/lo/math.go | 7 +- vendor/github.com/samber/lo/pointers.go | 32 - vendor/github.com/samber/lo/retry.go | 9 +- vendor/github.com/samber/lo/slice.go | 229 +++- vendor/github.com/samber/lo/string.go | 37 +- vendor/github.com/samber/lo/test.go | 32 + vendor/github.com/samber/lo/tuples.go | 136 +- .../github.com/samber/lo/type_manipulation.go | 88 ++ .../testify/assert/assertion_compare.go | 76 +- .../assert/assertion_compare_can_convert.go | 16 + .../assert/assertion_compare_legacy.go | 16 + .../testify/assert/assertion_format.go | 22 + .../testify/assert/assertion_forward.go | 44 + .../testify/assert/assertion_order.go | 8 +- .../stretchr/testify/assert/assertions.go | 190 ++- vendor/gopkg.in/yaml.v3/decode.go | 78 +- vendor/gopkg.in/yaml.v3/parserc.go | 11 +- vendor/modules.txt | 10 +- 46 files changed, 3452 insertions(+), 656 deletions(-) create mode 100644 pkg/gui/arrangement.go create mode 100644 pkg/gui/views.go create mode 100644 pkg/gui/window.go create mode 100755 scripts/bump_gocui.sh create mode 100755 scripts/bump_lazycore.sh create mode 100644 vendor/github.com/jesseduffield/lazycore/LICENSE create mode 100644 vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go create mode 100644 vendor/github.com/jesseduffield/lazycore/pkg/utils/utils.go create mode 100644 vendor/github.com/samber/lo/channel.go create mode 100644 vendor/github.com/samber/lo/func.go delete mode 100644 vendor/github.com/samber/lo/pointers.go create mode 100644 vendor/github.com/samber/lo/test.go create mode 100644 vendor/github.com/samber/lo/type_manipulation.go create mode 100644 vendor/github.com/stretchr/testify/assert/assertion_compare_can_convert.go create mode 100644 vendor/github.com/stretchr/testify/assert/assertion_compare_legacy.go diff --git a/go.mod b/go.mod index c7925e43..b8a9dcd7 100644 --- a/go.mod +++ b/go.mod @@ -16,13 +16,15 @@ require ( github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6 github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 + github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316 github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56 + github.com/mattn/go-runewidth v0.0.13 github.com/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767 github.com/mgutz/str v1.2.0 - github.com/samber/lo v1.20.0 + github.com/samber/lo v1.31.0 github.com/sirupsen/logrus v1.4.2 github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.8.0 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 ) @@ -39,7 +41,6 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.4 // indirect github.com/mattn/go-isatty v0.0.11 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect github.com/onsi/ginkgo v1.8.0 // indirect @@ -57,6 +58,6 @@ require ( golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect gopkg.in/yaml.v2 v2.2.2 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.2.0 // indirect ) diff --git a/go.sum b/go.sum index b72e4b19..7daa24f7 100644 --- a/go.sum +++ b/go.sum @@ -54,6 +54,8 @@ github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6 h1:Fmay0Lz21 github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU= github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0= github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo= +github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316 h1:UzTg0utG+cLS94Qjb/ZFDzfMeZDFXErSbNgJOyj70EA= +github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316/go.mod h1:qxN4mHOAyeIDLP7IK7defgPClM/z1Kze8VVQiaEjzsQ= github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56 h1:33wSxJWU/f2TAozHYtJ8zqBxEnEVYM+22moLoiAkxvg= github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56/go.mod h1:FZJBwOhE+RXz8EVZfY+xnbCw2cVOwxlK3/aIi581z/s= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= @@ -99,8 +101,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/samber/lo v1.20.0 h1:20FtphdORvp4yxklurzZv2HX+g+0urEMQziODC5bV70= -github.com/samber/lo v1.20.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A= +github.com/samber/lo v1.31.0 h1:Sfa+/064Tdo4SvlohQUQzBhgSer9v/coGvKQI/XLWAM= +github.com/samber/lo v1.31.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -108,11 +110,14 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= @@ -177,8 +182,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index c087675e..9094104f 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -114,6 +114,16 @@ type GuiConfig struct { // By default, containers are now sorted by status. This setting allows users to // use legacy behaviour instead. LegacySortContainers bool `yaml:"legacySortContainers,omitempty"` + + // If 0.333, then the side panels will be 1/3 of the screen's width + SidePanelWidth float64 `yaml:"sidePanelWidth"` + + // Determines whether we show the bottom line (the one containing keybinding + // info and the status of the app). + ShowBottomLine bool `yaml:"showBottomLine"` + + // When true, ensures that the focused side panel always takes up the full vertical space + ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"` } // CommandTemplatesConfig determines what commands actually get called when we @@ -320,10 +330,13 @@ func GetDefaultConfig() UserConfig { InactiveBorderColor: []string{"default"}, OptionsTextColor: []string{"blue"}, }, - ShowAllContainers: false, - ReturnImmediately: false, - WrapMainPanel: true, - LegacySortContainers: false, + ShowAllContainers: false, + ReturnImmediately: false, + WrapMainPanel: true, + LegacySortContainers: false, + SidePanelWidth: 0.3333, + ShowBottomLine: true, + ExpandFocusedSidePanel: false, }, ConfirmOnQuit: false, Logs: LogsConfig{ diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go new file mode 100644 index 00000000..58b53706 --- /dev/null +++ b/pkg/gui/arrangement.go @@ -0,0 +1,237 @@ +package gui + +import ( + "github.com/jesseduffield/lazycore/pkg/boxlayout" + "github.com/jesseduffield/lazydocker/pkg/utils" + "github.com/mattn/go-runewidth" + "github.com/samber/lo" +) + +// In this file we use the boxlayout package, along with knowledge about the app's state, +// to arrange the windows (i.e. panels) on the screen. + +const INFO_SECTION_PADDING = " " + +func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions { + minimumHeight := 9 + minimumWidth := 10 + width, height := gui.g.Size() + if width < minimumWidth || height < minimumHeight { + return boxlayout.ArrangeWindows(&boxlayout.Box{Window: "limit"}, 0, 0, width, height) + } + + sideSectionWeight, mainSectionWeight := gui.getMidSectionWeights() + + sidePanelsDirection := boxlayout.COLUMN + portraitMode := width <= 84 && height > 45 + if portraitMode { + sidePanelsDirection = boxlayout.ROW + } + + showInfoSection := gui.Config.UserConfig.Gui.ShowBottomLine + infoSectionSize := 0 + if showInfoSection { + infoSectionSize = 1 + } + + root := &boxlayout.Box{ + Direction: boxlayout.ROW, + Children: []*boxlayout.Box{ + { + Direction: sidePanelsDirection, + Weight: 1, + Children: []*boxlayout.Box{ + { + Direction: boxlayout.ROW, + Weight: sideSectionWeight, + ConditionalChildren: gui.sidePanelChildren, + }, + { + Window: "main", + Weight: mainSectionWeight, + }, + }, + }, + { + Direction: boxlayout.COLUMN, + Size: infoSectionSize, + Children: gui.infoSectionChildren(informationStr, appStatus), + }, + }, + } + + return boxlayout.ArrangeWindows(root, 0, 0, width, height) +} + +func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V { + result := map[K]V{} + for _, currMap := range maps { + for key, value := range currMap { + result[key] = value + } + } + + return result +} + +func (gui *Gui) getMidSectionWeights() (int, int) { + currentWindow := gui.currentWindow() + + // we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4 + sidePanelWidthRatio := gui.Config.UserConfig.Gui.SidePanelWidth + // we could make this better by creating ratios like 2:3 rather than always 1:something + mainSectionWeight := int(1/sidePanelWidthRatio) - 1 + sideSectionWeight := 1 + + if currentWindow == "main" && gui.State.ScreenMode == SCREEN_FULL { + mainSectionWeight = 1 + sideSectionWeight = 0 + } else { + if gui.State.ScreenMode == SCREEN_HALF { + mainSectionWeight = 1 + } else if gui.State.ScreenMode == SCREEN_FULL { + mainSectionWeight = 0 + } + } + + return sideSectionWeight, mainSectionWeight +} + +func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box { + result := []*boxlayout.Box{} + + if len(appStatus) > 0 { + result = append(result, + &boxlayout.Box{ + Window: "appStatus", + Size: runewidth.StringWidth(appStatus) + runewidth.StringWidth(INFO_SECTION_PADDING), + }, + ) + } + + result = append(result, + []*boxlayout.Box{ + { + Window: "options", + Weight: 1, + }, + { + Window: "information", + // unlike appStatus, informationStr has various colors so we need to decolorise before taking the length + Size: runewidth.StringWidth(INFO_SECTION_PADDING) + runewidth.StringWidth(utils.Decolorise(informationStr)), + }, + }..., + ) + + return result +} + +func (gui *Gui) sideViewNames() []string { + if gui.DockerCommand.InDockerComposeProject { + return []string{"project", "services", "containers", "images", "volumes"} + } else { + return []string{"project", "containers", "images", "volumes"} + } +} + +func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { + currentWindow := gui.currentSideWindowName() + sideWindowNames := gui.sideViewNames() + + if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF { + fullHeightBox := func(window string) *boxlayout.Box { + if window == currentWindow { + return &boxlayout.Box{ + Window: window, + Weight: 1, + } + } else { + return &boxlayout.Box{ + Window: window, + Size: 0, + } + } + } + + return lo.Map(sideWindowNames, func(window string, _ int) *boxlayout.Box { + return fullHeightBox(window) + }) + + } else if height >= 28 { + accordionMode := gui.Config.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 append([]*boxlayout.Box{ + { + Window: sideWindowNames[0], + Size: 3, + }, + }, lo.Map(sideWindowNames[1:], func(window string, _ int) *boxlayout.Box { + return accordionBox(&boxlayout.Box{Window: window, Weight: 1}) + })...) + } else { + 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{ + Window: window, + Size: squashedHeight, + } + } + } + + return lo.Map(sideWindowNames, func(window string, _ int) *boxlayout.Box { + return squashedSidePanelBox(window) + }) + } +} + +// TODO: reintroduce +// func (gui *Gui) currentSideWindowName() string { +// // there is always one and only one cyclable context in the context stack. We'll look from top to bottom +// gui.State.ContextManager.RLock() +// defer gui.State.ContextManager.RUnlock() + +// for idx := range gui.State.ContextManager.ContextStack { +// reversedIdx := len(gui.State.ContextManager.ContextStack) - 1 - idx +// context := gui.State.ContextManager.ContextStack[reversedIdx] + +// if context.GetKind() == types.SIDE_CONTEXT { +// return context.GetWindowName() +// } +// } + +// return "files" // default +// } + +func (gui *Gui) currentNonPopupWindowName() string { + return gui.peekPreviousView() +} + +// TODO: do this better. +func (gui *Gui) currentSideWindowName() string { + windowName := gui.currentWindow() + if !lo.Contains(gui.sideViewNames(), windowName) { + return gui.peekPreviousView() + } + + return windowName +} diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 2818e565..ccafc9ef 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -73,6 +73,9 @@ type Gui struct { T *tasks.TaskManager ErrorChan chan error CyclableViews []string + Views Views + + ViewsSetup bool } type servicePanelState struct { @@ -131,8 +134,22 @@ type guiState struct { // We increment it each time we switch to a new subprocess // Every time we go to a subprocess we need to close a few goroutines so this index is used for that purpose SessionIndex int + + ScreenMode WindowMaximisation } +// screen sizing determines how much space your selected window takes up (window +// as in panel, not your terminal's window). Sometimes you want a bit more space +// to see the contents of a panel, and this keeps track of how much maximisation +// you've set +type WindowMaximisation int + +const ( + SCREEN_NORMAL WindowMaximisation = iota + SCREEN_HALF + SCREEN_FULL +) + // NewGui builds a new gui handler func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand *commands.OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*Gui, error) { initialState := guiState{ diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 9a25f3dd..05ce3498 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -506,6 +506,18 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Modifier: gocui.ModNone, Handler: gui.scrollRightMain, }, + { + ViewName: "", + Key: '+', + Handler: wrappedHandler(gui.nextScreenMode), + Description: gui.Tr.LcNextScreenMode, + }, + { + ViewName: "", + Key: '_', + Handler: wrappedHandler(gui.prevScreenMode), + Description: gui.Tr.LcPrevScreenMode, + }, } // TODO: add more views here @@ -573,3 +585,9 @@ func (gui *Gui) keybindings(g *gocui.Gui) error { return nil } + +func wrappedHandler(f func() error) func(*gocui.Gui, *gocui.View) error { + return func(g *gocui.Gui, v *gocui.View) error { + return f() + } +} diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index d4765f5e..22f5fd89 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -1,11 +1,11 @@ package gui import ( - "github.com/fatih/color" "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazydocker/pkg/utils" ) +const UNKNOWN_VIEW_ERROR_MSG = "unknown view" + // getFocusLayout returns a manager function for when view gain and lose focus func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error { var previousView *gocui.View @@ -59,204 +59,61 @@ func (gui *Gui) onFocus(v *gocui.View) { // layout is called for every screen re-render e.g. when the screen is resized func (gui *Gui) layout(g *gocui.Gui) error { + if !gui.ViewsSetup { + if err := gui.createAllViews(); err != nil { + return err + } + + gui.ViewsSetup = true + } + g.Highlight = true width, height := g.Size() - information := gui.Config.Version - if gui.g.Mouse { - donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.Donate) - information = donate + " " + information - } - - minimumHeight := 9 - minimumWidth := 10 - if height < minimumHeight || width < minimumWidth { - v, err := g.SetView("limit", 0, 0, width-1, height-1, 0) - if err != nil { - if err.Error() != "unknown view" { - return err - } - v.Title = gui.Tr.NotEnoughSpace - v.Wrap = true - _, _ = g.SetViewOnTop("limit") - } - return nil - } - - currView := gui.g.CurrentView() - currentCyclebleView := gui.peekPreviousView() - if currView != nil { - viewName := currView.Name() - usePreviouseView := true - for _, view := range gui.CyclableViews { - if view == viewName { - currentCyclebleView = viewName - usePreviouseView = false - break - } - } - if usePreviouseView { - currentCyclebleView = gui.peekPreviousView() - } - } - - usableSpace := height - 4 - - tallPanels := 3 - var vHeights map[string]int - if gui.DockerCommand.InDockerComposeProject { - tallPanels++ - vHeights = map[string]int{ - "project": 3, - "services": usableSpace/tallPanels + usableSpace%tallPanels, - "containers": usableSpace / tallPanels, - "images": usableSpace / tallPanels, - "volumes": usableSpace / tallPanels, - "options": 1, - } - } else { - vHeights = map[string]int{ - "project": 3, - "containers": usableSpace/tallPanels + usableSpace%tallPanels, - "images": usableSpace / tallPanels, - "volumes": usableSpace / tallPanels, - "options": 1, - } - } - - if height < 28 { - defaultHeight := 3 - if height < 21 { - defaultHeight = 1 - } - vHeights = map[string]int{ - "project": defaultHeight, - "containers": defaultHeight, - "images": defaultHeight, - "volumes": defaultHeight, - "options": defaultHeight, - } - if gui.DockerCommand.InDockerComposeProject { - vHeights["services"] = defaultHeight - } - vHeights[currentCyclebleView] = height - defaultHeight*tallPanels - 1 - } - - optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1) - leftSideWidth := width / 3 - appStatus := gui.statusManager.getStatusString() - appStatusOptionsBoundary := 0 - if appStatus != "" { - appStatusOptionsBoundary = len(appStatus) + 2 - } - _, _ = g.SetViewOnBottom("limit") - _ = g.DeleteView("limit") + viewDimensions := gui.getWindowDimensions(gui.getInformationContent(), appStatus) + // we assume that the view has already been created. + setViewFromDimensions := func(viewName string, windowName string) (*gocui.View, error) { + dimensionsObj, ok := viewDimensions[windowName] - v, err := g.SetView("main", leftSideWidth+1, 0, width-1, height-2, gocui.LEFT) - if err != nil { - if err.Error() != "unknown view" { - return err - } - v.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel - v.FgColor = gocui.ColorDefault - - // when you run a docker container with the -it flags (interactive mode) it adds carriage returns for some reason. This is not docker's fault, it's an os-level default. - v.IgnoreCarriageReturns = true - } - - if v, err := g.SetView("project", 0, 0, leftSideWidth, vHeights["project"]-1, gocui.BOTTOM|gocui.RIGHT); err != nil { - if err.Error() != "unknown view" { - return err - } - v.Title = gui.Tr.ProjectTitle - v.FgColor = gocui.ColorDefault - } - - var servicesView *gocui.View - aboveContainersView := "project" - if gui.DockerCommand.InDockerComposeProject { - aboveContainersView = "services" - servicesView, err = g.SetViewBeneath("services", "project", vHeights["services"]) + view, err := g.View(viewName) if err != nil { - if err.Error() != "unknown view" { - return err - } - servicesView.Highlight = true - servicesView.Title = gui.Tr.ServicesTitle - servicesView.FgColor = gocui.ColorDefault + return nil, err } + + if !ok { + // view not specified in dimensions object: so create the view and hide it + // making the view take up the whole space in the background in case it needs + // to render content as soon as it appears, because lazyloaded content (via a pty task) + // cares about the size of the view. + _, err := g.SetView(viewName, 0, 0, width, height, 0) + view.Visible = false + return view, err + } + + frameOffset := 1 + if view.Frame { + frameOffset = 0 + } + _, err = g.SetView( + viewName, + dimensionsObj.X0-frameOffset, + dimensionsObj.Y0-frameOffset, + dimensionsObj.X1+frameOffset, + dimensionsObj.Y1+frameOffset, + 0, + ) + view.Visible = true + + return view, err } - containersView, err := g.SetViewBeneath("containers", aboveContainersView, vHeights["containers"]) - if err != nil { - if err.Error() != "unknown view" { + for _, viewName := range []string{"project", "services", "containers", "images", "volumes", "options", "information", "appStatus", "main", "limit"} { + _, err := setViewFromDimensions(viewName, viewName) + if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG { return err } - containersView.Highlight = true - if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.InDockerComposeProject { - containersView.Title = gui.Tr.ContainersTitle - } else { - containersView.Title = gui.Tr.StandaloneContainersTitle - } - containersView.FgColor = gocui.ColorDefault - } - - imagesView, err := g.SetViewBeneath("images", "containers", vHeights["images"]) - if err != nil { - if err.Error() != "unknown view" { - return err - } - imagesView.Highlight = true - imagesView.Title = gui.Tr.ImagesTitle - imagesView.FgColor = gocui.ColorDefault - } - - volumesView, err := g.SetViewBeneath("volumes", "images", vHeights["volumes"]) - if err != nil { - if err.Error() != "unknown view" { - return err - } - volumesView.Highlight = true - volumesView.Title = gui.Tr.VolumesTitle - volumesView.FgColor = gocui.ColorDefault - } - - if v, err := g.SetView("options", appStatusOptionsBoundary-1, height-2, optionsVersionBoundary-1, height, 0); err != nil { - if err.Error() != "unknown view" { - return err - } - v.Frame = false - if v.FgColor, err = gui.GetOptionsPanelTextColor(); err != nil { - return err - } - } - - if appStatusView, err := g.SetView("appStatus", -1, height-2, width, height, 0); err != nil { - if err.Error() != "unknown view" { - return err - } - appStatusView.BgColor = gocui.ColorDefault - appStatusView.FgColor = gocui.ColorCyan - appStatusView.Frame = false - if _, err := g.SetViewOnBottom("appStatus"); err != nil { - return err - } - } - - if v, err := g.SetView("information", optionsVersionBoundary-1, height-2, width, height, 0); err != nil { - if err.Error() != "unknown view" { - return err - } - v.BgColor = gocui.ColorDefault - v.FgColor = gocui.ColorGreen - v.Frame = false - if err := gui.renderString(g, "information", information); err != nil { - return err - } - - gui.waitForIntro.Done() } if gui.g.CurrentView() == nil { @@ -303,3 +160,9 @@ func (gui *Gui) focusPointInView(view *gocui.View) { gui.focusY(state.selectedLine, state.lineCount, view) } } + +func (gui *Gui) prepareView(viewName string) (*gocui.View, error) { + // arbitrarily giving the view enough size so that we don't get an error, but + // it's expected that the view will be given the correct size before being shown + return gui.g.SetView(viewName, 0, 0, 10, 10, 0) +} diff --git a/pkg/gui/theme.go b/pkg/gui/theme.go index b9b28bcf..0fb740da 100644 --- a/pkg/gui/theme.go +++ b/pkg/gui/theme.go @@ -5,8 +5,8 @@ import ( ) // GetOptionsPanelTextColor gets the color of the options panel text -func (gui *Gui) GetOptionsPanelTextColor() (gocui.Attribute, error) { - return GetGocuiStyle(gui.Config.UserConfig.Gui.Theme.OptionsTextColor), nil +func (gui *Gui) GetOptionsPanelTextColor() gocui.Attribute { + return GetGocuiStyle(gui.Config.UserConfig.Gui.Theme.OptionsTextColor) } // SetColorScheme sets the color scheme for the app based on the user config diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index a08b3a73..6e1a62cf 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -291,6 +291,10 @@ func (gui *Gui) trimmedContent(v *gocui.View) string { func (gui *Gui) currentViewName() string { currentView := gui.g.CurrentView() + // this can happen when the app is first starting up + if currentView == nil { + return gui.initiallyFocusedViewName() + } return currentView.Name() } @@ -383,3 +387,51 @@ func (gui *Gui) handleClick(v *gocui.View, itemCount int, selectedLine *int, han return handleSelect(gui.g, v) } + +func (gui *Gui) nextScreenMode() error { + if gui.currentViewName() == "main" { + gui.State.ScreenMode = prevIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode) + + return nil + } + + gui.State.ScreenMode = nextIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode) + + return nil +} + +func (gui *Gui) prevScreenMode() error { + if gui.currentViewName() == "main" { + gui.State.ScreenMode = nextIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode) + + return nil + } + + gui.State.ScreenMode = prevIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode) + + return nil +} + +func nextIntInCycle(sl []WindowMaximisation, current WindowMaximisation) WindowMaximisation { + for i, val := range sl { + if val == current { + if i == len(sl)-1 { + return sl[0] + } + return sl[i+1] + } + } + return sl[0] +} + +func prevIntInCycle(sl []WindowMaximisation, current WindowMaximisation) WindowMaximisation { + for i, val := range sl { + if val == current { + if i > 0 { + return sl[i-1] + } + return sl[len(sl)-1] + } + } + return sl[len(sl)-1] +} diff --git a/pkg/gui/views.go b/pkg/gui/views.go new file mode 100644 index 00000000..3f83dd29 --- /dev/null +++ b/pkg/gui/views.go @@ -0,0 +1,128 @@ +package gui + +import ( + "github.com/fatih/color" + "github.com/jesseduffield/gocui" + "github.com/samber/lo" +) + +type Views struct { + Project *gocui.View + Services *gocui.View + Containers *gocui.View + Images *gocui.View + Volumes *gocui.View + + Main *gocui.View + + Options *gocui.View + Confirmation *gocui.View + Menu *gocui.View + Information *gocui.View + AppStatus *gocui.View + Limit *gocui.View +} + +type viewNameMapping struct { + viewPtr **gocui.View + name string +} + +func (gui *Gui) orderedViews() []*gocui.View { + return lo.Map(gui.orderedViewNameMappings(), func(v viewNameMapping, _ int) *gocui.View { + return *v.viewPtr + }) +} + +func (gui *Gui) orderedViewNameMappings() []viewNameMapping { + return []viewNameMapping{ + // first layer. Ordering within this layer does not matter because there are + // no overlapping views + {viewPtr: &gui.Views.Project, name: "project"}, + {viewPtr: &gui.Views.Services, name: "services"}, + {viewPtr: &gui.Views.Containers, name: "containers"}, + {viewPtr: &gui.Views.Images, name: "images"}, + {viewPtr: &gui.Views.Volumes, name: "volumes"}, + + {viewPtr: &gui.Views.Main, name: "main"}, + + // bottom line + {viewPtr: &gui.Views.Options, name: "options"}, + {viewPtr: &gui.Views.AppStatus, name: "appStatus"}, + {viewPtr: &gui.Views.Information, name: "information"}, + + // popups. + {viewPtr: &gui.Views.Menu, name: "menu"}, + {viewPtr: &gui.Views.Confirmation, name: "confirmation"}, + + // this guy will cover everything else when it appears + {viewPtr: &gui.Views.Limit, name: "limit"}, + } +} + +func (gui *Gui) createAllViews() error { + var err error + for _, mapping := range gui.orderedViewNameMappings() { + *mapping.viewPtr, err = gui.prepareView(mapping.name) + if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG { + return err + } + (*mapping.viewPtr).FgColor = gocui.ColorDefault + } + + gui.Views.Main.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel + // when you run a docker container with the -it flags (interactive mode) it adds carriage returns for some reason. This is not docker's fault, it's an os-level default. + gui.Views.Main.IgnoreCarriageReturns = true + + gui.Views.Project.Title = gui.Tr.ProjectTitle + + gui.Views.Services.Highlight = true + gui.Views.Services.Title = gui.Tr.ServicesTitle + + gui.Views.Containers.Highlight = true + if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.InDockerComposeProject { + gui.Views.Containers.Title = gui.Tr.ContainersTitle + } else { + gui.Views.Containers.Title = gui.Tr.StandaloneContainersTitle + } + + gui.Views.Images.Highlight = true + gui.Views.Images.Title = gui.Tr.ImagesTitle + + gui.Views.Volumes.Highlight = true + gui.Views.Volumes.Title = gui.Tr.VolumesTitle + + gui.Views.Options.Frame = false + gui.Views.Options.FgColor = gui.GetOptionsPanelTextColor() + + gui.Views.AppStatus.FgColor = gocui.ColorCyan + gui.Views.AppStatus.Frame = false + + gui.Views.Information.Frame = false + gui.Views.Information.FgColor = gocui.ColorGreen + + if err := gui.renderString(gui.g, "information", gui.getInformationContent()); err != nil { + return err + } + + gui.Views.Confirmation.Visible = false + gui.Views.Menu.Visible = false + + gui.Views.Limit.Visible = false + gui.Views.Limit.Title = gui.Tr.NotEnoughSpace + gui.Views.Limit.Wrap = true + + gui.waitForIntro.Done() + + return nil +} + +func (gui *Gui) getInformationContent() string { + informationStr := gui.Config.Version + if !gui.g.Mouse { + return informationStr + } + + donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.Donate) + return donate + " " + informationStr +} diff --git a/pkg/gui/window.go b/pkg/gui/window.go new file mode 100644 index 00000000..42c12dd5 --- /dev/null +++ b/pkg/gui/window.go @@ -0,0 +1,7 @@ +package gui + +func (gui *Gui) currentWindow() string { + // at the moment, we only have one view per window in lazydocker, so we + // are using the view name as the window name + return gui.currentViewName() +} diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index cec206e0..62d9a1c4 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -105,6 +105,9 @@ type TranslationSet struct { No string Yes string + + LcNextScreenMode string + LcPrevScreenMode string } func englishSet() TranslationSet { @@ -217,5 +220,8 @@ func englishSet() TranslationSet { No: "no", Yes: "yes", + + LcNextScreenMode: "next screen mode (normal/half/fullscreen)", + LcPrevScreenMode: "prev screen mode", } } diff --git a/scripts/bump_gocui.sh b/scripts/bump_gocui.sh new file mode 100755 index 00000000..3e0adeba --- /dev/null +++ b/scripts/bump_gocui.sh @@ -0,0 +1,5 @@ +# Go's proxy servers are not very up-to-date so that's why we use `GOPROXY=direct` +# We specify the `awesome` branch to avoid the default behaviour of looking for a semver tag. +GOPROXY=direct go get -u github.com/jesseduffield/gocui@awesome && go mod vendor && go mod tidy + +# Note to self if you ever want to fork a repo be sure to use this same approach: it's important to use the branch name (e.g. master) diff --git a/scripts/bump_lazycore.sh b/scripts/bump_lazycore.sh new file mode 100755 index 00000000..810e5f08 --- /dev/null +++ b/scripts/bump_lazycore.sh @@ -0,0 +1,5 @@ +# Go's proxy servers are not very up-to-date so that's why we use `GOPROXY=direct` +# We specify the `awesome` branch to avoid the default behaviour of looking for a semver tag. +GOPROXY=direct go get -u github.com/jesseduffield/lazycore@master && go mod vendor && go mod tidy + +# Note to self if you ever want to fork a repo be sure to use this same approach: it's important to use the branch name (e.g. master) diff --git a/vendor/github.com/jesseduffield/lazycore/LICENSE b/vendor/github.com/jesseduffield/lazycore/LICENSE new file mode 100644 index 00000000..2a7175dc --- /dev/null +++ b/vendor/github.com/jesseduffield/lazycore/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Jesse Duffield + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go b/vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go new file mode 100644 index 00000000..c516cbba --- /dev/null +++ b/vendor/github.com/jesseduffield/lazycore/pkg/boxlayout/boxlayout.go @@ -0,0 +1,211 @@ +package boxlayout + +import ( + "github.com/jesseduffield/lazycore/pkg/utils" + "github.com/samber/lo" +) + +type Dimensions struct { + X0 int + X1 int + Y0 int + Y1 int +} + +type Direction int + +const ( + ROW Direction = iota + COLUMN +) + +// to give a high-level explanation of what's going on here. We layout our windows by arranging a bunch of boxes in the available space. +// If a box has children, it needs to specify how it wants to arrange those children: ROW or COLUMN. +// If a box represents a window, you can put the window name in the Window field. +// When determining how to divvy-up the available height (for row children) or width (for column children), we first +// give the boxes with a static `size` the space that they want. Then we apportion +// the remaining space based on the weights of the dynamic boxes (you can't define +// both size and weight at the same time: you gotta pick one). If there are two +// boxes, one with weight 1 and the other with weight 2, the first one gets 33% +// of the available space and the second one gets the remaining 66% + +type Box struct { + // Direction decides how the children boxes are laid out. ROW means the children will each form a row i.e. that they will be stacked on top of eachother. + Direction Direction + + // function which takes the width and height assigned to the box and decides which orientation it will have + ConditionalDirection func(width int, height int) Direction + + Children []*Box + + // function which takes the width and height assigned to the box and decides the layout of the children. + ConditionalChildren func(width int, height int) []*Box + + // Window refers to the name of the window this box represents, if there is one + Window string + + // static Size. If parent box's direction is ROW this refers to height, otherwise width + Size int + + // dynamic size. Once all statically sized children have been considered, Weight decides how much of the remaining space will be taken up by the box + // TODO: consider making there be one int and a type enum so we can't have size and Weight simultaneously defined + Weight int +} + +func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions { + children := root.getChildren(width, height) + if len(children) == 0 { + // leaf node + if root.Window != "" { + dimensionsForWindow := Dimensions{X0: x0, Y0: y0, X1: x0 + width - 1, Y1: y0 + height - 1} + return map[string]Dimensions{root.Window: dimensionsForWindow} + } + return map[string]Dimensions{} + } + + direction := root.getDirection(width, height) + + var availableSize int + if direction == COLUMN { + availableSize = width + } else { + availableSize = height + } + + sizes := calcSizes(children, availableSize) + + result := map[string]Dimensions{} + offset := 0 + for i, child := range children { + boxSize := sizes[i] + + var resultForChild map[string]Dimensions + if direction == COLUMN { + resultForChild = ArrangeWindows(child, x0+offset, y0, boxSize, height) + } else { + resultForChild = ArrangeWindows(child, x0, y0+offset, width, boxSize) + } + + result = mergeDimensionMaps(result, resultForChild) + offset += boxSize + } + + return result +} + +func calcSizes(boxes []*Box, availableSpace int) []int { + normalizedWeights := normalizeWeights(lo.Map(boxes, func(box *Box, _ int) 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 lo.SomeBy(weights, func(weight int) bool { return weight == 1 }) { + return weights + } + + // map weights to factorSlices and find the lowest common factor + positiveWeights := lo.Filter(weights, func(weight int, _ int) bool { return weight > 0 }) + factorSlices := lo.Map(positiveWeights, func(weight int, _ int) []int { return calcFactors(weight) }) + commonFactors := factorSlices[0] + for _, factors := range factorSlices { + commonFactors = lo.Intersect(commonFactors, factors) + } + + if len(commonFactors) == 0 { + return weights + } + + newWeights := lo.Map(weights, func(weight int, _ 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 +} + +func (b *Box) getDirection(width int, height int) Direction { + if b.ConditionalDirection != nil { + return b.ConditionalDirection(width, height) + } + return b.Direction +} + +func (b *Box) getChildren(width int, height int) []*Box { + if b.ConditionalChildren != nil { + return b.ConditionalChildren(width, height) + } + return b.Children +} + +func mergeDimensionMaps(a map[string]Dimensions, b map[string]Dimensions) map[string]Dimensions { + result := map[string]Dimensions{} + for _, dimensionMap := range []map[string]Dimensions{a, b} { + for k, v := range dimensionMap { + result[k] = v + } + } + return result +} diff --git a/vendor/github.com/jesseduffield/lazycore/pkg/utils/utils.go b/vendor/github.com/jesseduffield/lazycore/pkg/utils/utils.go new file mode 100644 index 00000000..9fa93376 --- /dev/null +++ b/vendor/github.com/jesseduffield/lazycore/pkg/utils/utils.go @@ -0,0 +1,16 @@ +package utils + +// Min returns the minimum of two integers +func Min(x, y int) int { + if x < y { + return x + } + return y +} + +func Max(x, y int) int { + if x > y { + return x + } + return y +} diff --git a/vendor/github.com/samber/lo/CHANGELOG.md b/vendor/github.com/samber/lo/CHANGELOG.md index 2eaa45c9..9ba32f1b 100644 --- a/vendor/github.com/samber/lo/CHANGELOG.md +++ b/vendor/github.com/samber/lo/CHANGELOG.md @@ -2,6 +2,128 @@ @samber: I sometimes forget to update this file. Ping me on [Twitter](https://twitter.com/samuelberthe) or open an issue in case of error. We need to keep a clear changelog for easier lib upgrade. +## 1.31.0 (2022-10-06) + +Adding: + +- lo.SliceToChannel +- lo.Generator +- lo.Batch +- lo.BatchWithTimeout + +## 1.30.1 (2022-10-06) + +Fix: + +- lo.Try1: remove generic type +- lo.Validate: format error properly + +## 1.30.0 (2022-10-04) + +Adding: + +- lo.TernaryF +- lo.Validate + +## 1.29.0 (2022-10-02) + +Adding: + +- lo.ErrorAs +- lo.TryOr +- lo.TryOrX + +## 1.28.0 (2022-09-05) + +Adding: + +- lo.ChannelDispatcher with 6 dispatching strategies: + - lo.DispatchingStrategyRoundRobin + - lo.DispatchingStrategyRandom + - lo.DispatchingStrategyWeightedRandom + - lo.DispatchingStrategyFirst + - lo.DispatchingStrategyLeast + - lo.DispatchingStrategyMost + +## 1.27.1 (2022-08-15) + +Bugfix: + +- Removed comparable constraint for lo.FindKeyBy + +## 1.27.0 (2022-07-29) + +Breaking: + +- Change of MapToSlice prototype: `MapToSlice[K comparable, V any, R any](in map[K]V, iteratee func(V, K) R) []R` -> `MapToSlice[K comparable, V any, R any](in map[K]V, iteratee func(K, V) R) []R` + +Added: + +- lo.ChunkString +- lo.SliceToMap (alias to lo.Associate) + +## 1.26.0 (2022-07-24) + +Adding: + +- lo.Associate +- lo.ReduceRight +- lo.FromPtrOr +- lo.MapToSlice +- lo.IsSorted +- lo.IsSortedByKey + +## 1.25.0 (2022-07-04) + +Adding: + +- lo.FindUniques +- lo.FindUniquesBy +- lo.FindDuplicates +- lo.FindDuplicatesBy +- lo.IsNotEmpty + +## 1.24.0 (2022-07-04) + +Adding: + +- lo.Without +- lo.WithoutEmpty + +## 1.23.0 (2022-07-04) + +Adding: + +- lo.FindKey +- lo.FindKeyBy + +## 1.22.0 (2022-07-04) + +Adding: + +- lo.Slice +- lo.FromPtr +- lo.IsEmpty +- lo.Compact +- lo.ToPairs: alias to lo.Entries +- lo.FromPairs: alias to lo.FromEntries +- lo.Partial + +Change: + +- lo.Must + lo.MustX: add context to panic message + +Fix: + +- lo.Nth: out of bound exception (#137) + +## 1.21.0 (2022-05-10) + +Adding: + +- lo.ToAnySlice +- lo.FromAnySlice + ## 1.20.0 (2022-05-02) Adding: @@ -55,7 +177,7 @@ Improvement: Adding: -- lo.Colaesce +- lo.Coalesce ## 1.13.0 (2022-04-14) diff --git a/vendor/github.com/samber/lo/Makefile b/vendor/github.com/samber/lo/Makefile index 91416187..57bb4915 100644 --- a/vendor/github.com/samber/lo/Makefile +++ b/vendor/github.com/samber/lo/Makefile @@ -1,10 +1,5 @@ BIN=go -# BIN=go1.18beta1 - -go1.18beta1: - go install golang.org/dl/go1.18beta1@latest - go1.18beta1 download build: ${BIN} build -v ./... @@ -20,7 +15,7 @@ watch-bench: reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...' coverage: - ${BIN} test -v -coverprofile cover.out . + ${BIN} test -v -coverprofile=cover.out -covermode=atomic . ${BIN} tool cover -html=cover.out -o cover.html # tools @@ -31,7 +26,7 @@ tools: ${BIN} install github.com/jondot/goweight@latest ${BIN} install github.com/golangci/golangci-lint/cmd/golangci-lint@latest ${BIN} get -t -u golang.org/x/tools/cmd/cover - ${BIN} get -t -u github.com/sonatype-nexus-community/nancy@latest + ${BIN} install github.com/sonatype-nexus-community/nancy@latest go mod tidy lint: @@ -40,11 +35,9 @@ lint-fix: golangci-lint run --timeout 60s --max-same-issues 50 --fix ./... audit: tools - ${BIN} mod tidy ${BIN} list -json -m all | nancy sleuth outdated: tools - ${BIN} mod tidy ${BIN} list -u -m -json all | go-mod-outdated -update -direct weight: tools diff --git a/vendor/github.com/samber/lo/README.md b/vendor/github.com/samber/lo/README.md index 10107ce0..49cbe49d 100644 --- a/vendor/github.com/samber/lo/README.md +++ b/vendor/github.com/samber/lo/README.md @@ -1,10 +1,12 @@ # lo -![Build Status](https://github.com/samber/lo/actions/workflows/go.yml/badge.svg) +[![tag](https://img.shields.io/github/tag/samber/lo.svg)](https://github.com/samber/lo/releases) [![GoDoc](https://godoc.org/github.com/samber/lo?status.svg)](https://pkg.go.dev/github.com/samber/lo) +![Build Status](https://github.com/samber/lo/actions/workflows/go.yml/badge.svg) [![Go report](https://goreportcard.com/badge/github.com/samber/lo)](https://goreportcard.com/report/github.com/samber/lo) +[![codecov](https://codecov.io/gh/samber/lo/branch/master/graph/badge.svg)](https://codecov.io/gh/samber/lo) -✨ **`lo` is a Lodash-style Go library based on Go 1.18+ Generics.** +✨ **`samber/lo` is a Lodash-style Go library based on Go 1.18+ Generics.** This project started as an experiment with the new generics implementation. It may look like [Lodash](https://github.com/lodash/lodash) in some aspects. I used to code with the fantastic ["go-funk"](https://github.com/thoas/go-funk) package, but "go-funk" uses reflection and therefore is not typesafe. @@ -12,7 +14,12 @@ As expected, benchmarks demonstrate that generics will be much faster than imple In the future, 5 to 10 helpers will overlap with those coming into the Go standard library (under package names `slices` and `maps`). I feel this library is legitimate and offers many more valuable abstractions. -### Why this name? +**See also:** + +- [samber/do](https://github.com/samber/do): A dependency injection toolkit based on Go 1.18+ Generics +- [samber/mo](https://github.com/samber/mo): Monads based on Go 1.18+ Generics (Option, Result, Either...) + +**Why this name?** I wanted a **short name**, similar to "Lodash" and no Go package currently uses this name. @@ -52,126 +59,187 @@ GoDoc: [https://godoc.org/github.com/samber/lo](https://godoc.org/github.com/sam Supported helpers for slices: -- Filter -- Map -- FilterMap -- FlatMap -- Reduce -- ForEach -- Times -- Uniq -- UniqBy -- GroupBy -- Chunk -- PartitionBy -- Flatten -- Shuffle -- Reverse -- Fill -- Repeat -- KeyBy -- Drop -- DropRight -- DropWhile -- DropRightWhile -- Reject -- Count -- CountBy +- [Filter](#filter) +- [Map](#map) +- [FilterMap](#filtermap) +- [FlatMap](#flatmap) +- [Reduce](#reduce) +- [ReduceRight](#reduceright) +- [ForEach](#foreach) +- [Times](#times) +- [Uniq](#uniq) +- [UniqBy](#uniqby) +- [GroupBy](#groupby) +- [Chunk](#chunk) +- [PartitionBy](#partitionby) +- [Flatten](#flatten) +- [Shuffle](#shuffle) +- [Reverse](#reverse) +- [Fill](#fill) +- [Repeat](#repeat) +- [RepeatBy](#repeatby) +- [KeyBy](#keyby) +- [Associate / SliceToMap](#associate-alias-slicetomap) +- [Drop](#drop) +- [DropRight](#dropright) +- [DropWhile](#dropwhile) +- [DropRightWhile](#droprightwhile) +- [Reject](#reject) +- [Count](#count) +- [CountBy](#countby) +- [Subset](#subset) +- [Slice](#slice) +- [Replace](#replace) +- [ReplaceAll](#replaceall) +- [Compact](#compact) +- [IsSorted](#issorted) +- [IsSortedByKey](#issortedbykey) Supported helpers for maps: -- Keys -- Values -- PickBy -- PickByKeys -- PickByValues -- OmitBy -- OmitByKeys -- OmitByValues -- Entries -- FromEntries -- Invert -- Assign (merge of maps) -- MapKeys -- MapValues +- [Keys](#keys) +- [Values](#values) +- [PickBy](#pickby) +- [PickByKeys](#pickbykeys) +- [PickByValues](#pickbyvalues) +- [OmitBy](#omitby) +- [OmitByKeys](#omitbykeys) +- [OmitByValues](#omitbyvalues) +- [Entries / ToPairs](#entries-alias-topairs) +- [FromEntries / FromPairs](#fromentries-alias-frompairs) +- [Invert](#invert) +- [Assign (merge of maps)](#assign) +- [MapKeys](#mapkeys) +- [MapValues](#mapvalues) +- [MapToSlice](#maptoslice) Supported math helpers: -- Range / RangeFrom / RangeWithSteps -- Clamp -- SumBy +- [Range / RangeFrom / RangeWithSteps](#range--rangefrom--rangewithsteps) +- [Clamp](#clamp) +- [SumBy](#sumby) Supported helpers for strings: -- Substring -- RuneLength +- [Substring](#substring) +- [ChunkString](#chunkstring) +- [RuneLength](#runelength) Supported helpers for tuples: -- T2 -> T9 -- Unpack2 -> Unpack9 -- Zip2 -> Zip9 -- Unzip2 -> Unzip9 +- [T2 -> T9](#t2---t9) +- [Unpack2 -> Unpack9](#unpack2---unpack9) +- [Zip2 -> Zip9](#zip2---zip9) +- [Unzip2 -> Unzip9](#unzip2---unzip9) + +Supported helpers for channels: + +- [ChannelDispatcher](#channeldispatcher) +- [SliceToChannel](#slicetochannel) +- [Generator](#generator) +- [Batch](#batch) +- [BatchWithTimeout](#batchwithtimeout) Supported intersection helpers: -- Contains -- ContainsBy -- Every -- EveryBy -- Some -- SomeBy -- None -- NoneBy -- Intersect -- Difference -- Union +- [Contains](#contains) +- [ContainsBy](#containsby) +- [Every](#every) +- [EveryBy](#everyby) +- [Some](#some) +- [SomeBy](#someby) +- [None](#none) +- [NoneBy](#noneby) +- [Intersect](#intersect) +- [Difference](#difference) +- [Union](#union) +- [Without](#without) +- [WithoutEmpty](#withoutempty) Supported search helpers: -- IndexOf -- LastIndexOf -- Find -- FindIndexOf -- FindLastIndexOf -- Min -- MinBy -- Max -- MaxBy -- Last -- Nth -- Sample -- Samples +- [IndexOf](#indexof) +- [LastIndexOf](#lastindexof) +- [Find](#find) +- [FindIndexOf](#findindexof) +- [FindLastIndexOf](#findlastindexof) +- [FindKey](#findkey) +- [FindKeyBy](#findkeyby) +- [FindUniques](#finduniques) +- [FindUniquesBy](#finduniquesby) +- [FindDuplicates](#findduplicates) +- [FindDuplicatesBy](#findduplicatesby) +- [Min](#min) +- [MinBy](#minby) +- [Max](#max) +- [MaxBy](#maxby) +- [Last](#last) +- [Nth](#nth) +- [Sample](#sample) +- [Samples](#samples) -Other functional programming helpers: +Conditional helpers: -- Ternary (1 line if/else statement) -- If / ElseIf / Else -- Switch / Case / Default -- ToPtr -- ToSlicePtr -- Empty -- Coalesce +- [Ternary](#ternary) +- [TernaryF](#ternaryf) +- [If / ElseIf / Else](#if--elseif--else) +- [Switch / Case / Default](#switch--case--default) + +Type manipulation helpers: + +- [ToPtr](#toptr) +- [FromPtr](#fromptr) +- [FromPtrOr](#fromptror) +- [ToSlicePtr](#tosliceptr) +- [ToAnySlice](#toanyslice) +- [FromAnySlice](#fromanyslice) +- [Empty](#empty) +- [IsEmpty](#isempty) +- [IsNotEmpty](#isnotempty) +- [Coalesce](#coalesce) + +Function helpers: + +- [Partial](#partial) Concurrency helpers: -- Attempt -- AttemptWithDelay -- Debounce -- Async +- [Attempt](#attempt) +- [AttemptWithDelay](#attemptwithdelay) +- [Debounce](#debounce) +- [Synchronize](#synchronize) +- [Async](#async) Error handling: -- Must -- Try -- TryCatch -- TryWithErrorValue -- TryCatchWithErrorValue +- [Validate](#validate) +- [Must](#must) +- [Try](#try) +- [Try1 -> Try6](#try0-6) +- [TryOr](#tryor) +- [TryOr1 -> TryOr6](#tryor0-6) +- [TryCatch](#trycatch) +- [TryWithErrorValue](#trywitherrorvalue) +- [TryCatchWithErrorValue](#trycatchwitherrorvalue) +- [ErrorsAs](#errorsas) Constraints: - Clonable +### Filter + +Iterates over a collection and returns an array of all the elements the predicate function returns `true` for. + +```go +even := lo.Filter[int]([]int{1, 2, 3, 4}, func(x int, index int) bool { + return x%2 == 0 +}) +// []int{2, 4} +``` + +[[play](https://go.dev/play/p/Apjg3WeSi7K)] + ### Map Manipulates a slice of one type and transforms it into a slice of another type: @@ -179,12 +247,14 @@ Manipulates a slice of one type and transforms it into a slice of another type: ```go import "github.com/samber/lo" -lo.Map[int64, string]([]int64{1, 2, 3, 4}, func(x int64, _ int) string { +lo.Map[int64, string]([]int64{1, 2, 3, 4}, func(x int64, index int) string { return strconv.FormatInt(x, 10) }) // []string{"1", "2", "3", "4"} ``` +[[play](https://go.dev/play/p/OkPcYAhBo0D)] + Parallel processing: like `lo.Map()`, but the mapper function is called in a goroutine. Results are returned in the same order. ```go @@ -196,20 +266,6 @@ lop.Map[int64, string]([]int64{1, 2, 3, 4}, func(x int64, _ int) string { // []string{"1", "2", "3", "4"} ``` -### FlatMap - -Manipulates a slice and transforms and flattens it to a slice of another type. - -```go -lo.FlatMap[int, string]([]int{0, 1, 2}, func(x int, _ int) []string { - return []string{ - strconv.FormatInt(x, 10), - strconv.FormatInt(x, 10), - } -}) -// []string{"0", "0", "1", "1", "2", "2"} -``` - ### FilterMap Returns a slice which obtained after both filtering and mapping using the given callback function. @@ -226,36 +282,23 @@ matching := lo.FilterMap[string, string]([]string{"cpu", "gpu", "mouse", "keyboa // []string{"xpu", "xpu"} ``` -### Filter +[[play](https://go.dev/play/p/-AuYXfy7opz)] -Iterates over a collection and returns an array of all the elements the predicate function returns `true` for. +### FlatMap + +Manipulates a slice and transforms and flattens it to a slice of another type. ```go -even := lo.Filter[int]([]int{1, 2, 3, 4}, func(x int, _ int) bool { - return x%2 == 0 +lo.FlatMap[int, string]([]int{0, 1, 2}, func(x int, _ int) []string { + return []string{ + strconv.FormatInt(x, 10), + strconv.FormatInt(x, 10), + } }) -// []int{2, 4} +// []string{"0", "0", "1", "1", "2", "2"} ``` -### Contains - -Returns true if an element is present in a collection. - -```go -present := lo.Contains[int]([]int{0, 1, 2, 3, 4, 5}, 5) -// true -``` - -### ContainsBy - -Returns true if the predicate function returns `true`. - -```go -present := lo.ContainsBy[int]([]int{0, 1, 2, 3, 4, 5}, func(x int) bool { - return x == 3 -}) -// true -``` +[[play](https://go.dev/play/p/YSoYmQTA8-U)] ### Reduce @@ -268,6 +311,21 @@ sum := lo.Reduce[int, int]([]int{1, 2, 3, 4}, func(agg int, item int, _ int) int // 10 ``` +[[play](https://go.dev/play/p/R4UHXZNaaUG)] + +### ReduceRight + +Like `lo.Reduce` except that it iterates over elements of collection from right to left. + +```go +result := lo.ReduceRight[[]int, []int]([][]int{{0, 1}, {2, 3}, {4, 5}}, func(agg []int, item []int, _ int) []int { + return append(agg, item...) +}, []int{}) +// []int{4, 5, 2, 3, 0, 1} +``` + +[[play](https://go.dev/play/p/Fq3W70l7wXF)] + ### ForEach Iterates over elements of a collection and invokes the function over each element. @@ -281,6 +339,8 @@ lo.ForEach[string]([]string{"hello", "world"}, func(x string, _ int) { // prints "hello\nworld\n" ``` +[[play](https://go.dev/play/p/oofyiUPRf8t)] + Parallel processing: like `lo.ForEach()`, but the callback is called as a goroutine. ```go @@ -305,6 +365,8 @@ lo.Times[string](3, func(i int) string { // []string{"0", "1", "2"} ``` +[[play](https://go.dev/play/p/vgQj3Glr6lT)] + Parallel processing: like `lo.Times()`, but callback is called in goroutine. ```go @@ -325,6 +387,8 @@ uniqValues := lo.Uniq[int]([]int{1, 2, 2, 1}) // []int{1, 2} ``` +[[play](https://go.dev/play/p/DTzbeXZ6iEN)] + ### UniqBy Returns a duplicate-free version of an array, in which only the first occurrence of each element is kept. The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is invoked for each element in array to generate the criterion by which uniqueness is computed. @@ -336,6 +400,8 @@ uniqValues := lo.UniqBy[int, int]([]int{0, 1, 2, 3, 4, 5}, func(i int) int { // []int{0, 1, 2} ``` +[[play](https://go.dev/play/p/g42Z3QSb53u)] + ### GroupBy Returns an object composed of keys generated from the results of running each element of collection through iteratee. @@ -349,6 +415,8 @@ groups := lo.GroupBy[int, int]([]int{0, 1, 2, 3, 4, 5}, func(i int) int { // map[int][]int{0: []int{0, 3}, 1: []int{1, 4}, 2: []int{2, 5}} ``` +[[play](https://go.dev/play/p/XnQBd_v6brd)] + Parallel processing: like `lo.GroupBy()`, but callback is called in goroutine. ```go @@ -378,6 +446,8 @@ lo.Chunk[int]([]int{0}, 2) // [][]int{{0}} ``` +[[play](https://go.dev/play/p/EeKl0AuTehH)] + ### PartitionBy Returns an array of elements split into groups. The order of grouped values is determined by the order they occur in collection. The grouping is generated from the results of running each element of collection through iteratee. @@ -396,12 +466,14 @@ partitions := lo.PartitionBy[int, string]([]int{-2, -1, 0, 1, 2, 3, 4, 5}, func( // [][]int{{-2, -1}, {0, 2, 4}, {1, 3, 5}} ``` +[[play](https://go.dev/play/p/NfQ_nGjkgXW)] + Parallel processing: like `lo.PartitionBy()`, but callback is called in goroutine. Results are returned in the same order. ```go import lop "github.com/samber/lo/parallel" -partitions := lo.PartitionBy[int, string]([]int{-2, -1, 0, 1, 2, 3, 4, 5}, func(x int) string { +partitions := lop.PartitionBy[int, string]([]int{-2, -1, 0, 1, 2, 3, 4, 5}, func(x int) string { if x < 0 { return "negative" } else if x%2 == 0 { @@ -421,24 +493,30 @@ flat := lo.Flatten[int]([][]int{{0, 1}, {2, 3, 4, 5}}) // []int{0, 1, 2, 3, 4, 5} ``` +[[play](https://go.dev/play/p/rbp9ORaMpjw)] + ### Shuffle Returns an array of shuffled values. Uses the Fisher-Yates shuffle algorithm. ```go randomOrder := lo.Shuffle[int]([]int{0, 1, 2, 3, 4, 5}) -// []int{0, 1, 2, 3, 4, 5} +// []int{1, 4, 0, 3, 5, 2} ``` +[[play](https://go.dev/play/p/Qp73bnTDnc7)] + ### Reverse Reverses array so that the first element becomes the last, the second element becomes the second to last, and so on. ```go -reverseOder := lo.Reverse[int]([]int{0, 1, 2, 3, 4, 5}) +reverseOrder := lo.Reverse[int]([]int{0, 1, 2, 3, 4, 5}) // []int{5, 4, 3, 2, 1, 0} ``` +[[play](https://go.dev/play/p/fhUMLvZ7vS6)] + ### Fill Fills elements of array with `initial` value. @@ -456,6 +534,8 @@ initializedSlice := lo.Fill[foo]([]foo{foo{"a"}, foo{"a"}}, foo{"b"}) // []foo{foo{"b"}, foo{"b"}} ``` +[[play](https://go.dev/play/p/VwR34GzqEub)] + ### Repeat Builds a slice with N copies of initial value. @@ -473,22 +553,26 @@ slice := lo.Repeat[foo](2, foo{"a"}) // []foo{foo{"a"}, foo{"a"}} ``` +[[play](https://go.dev/play/p/g3uHXbmc3b6)] + ### RepeatBy Builds a slice with values returned by N calls of callback. ```go -slice := lo.RepeatBy[int](0, func (i int) int { - return math.Pow(i, 2) +slice := lo.RepeatBy[string](0, func (i int) string { + return strconv.FormatInt(int64(math.Pow(float64(i), 2)), 10) }) // []int{} -slice := lo.RepeatBy[int](5, func (i int) int { - return math.Pow(i, 2) +slice := lo.RepeatBy[string](5, func(i int) string { + return strconv.FormatInt(int64(math.Pow(float64(i), 2)), 10) }) // []int{0, 1, 4, 9, 16} ``` +[[play](https://go.dev/play/p/ozZLCtX_hNU)] + ### KeyBy Transforms a slice or an array of structs to a map based on a pivot callback. @@ -513,6 +597,26 @@ result := lo.KeyBy[string, Character](characters, func(char Character) string { //map[a:{dir:left code:97} d:{dir:right code:100}] ``` +[[play](https://go.dev/play/p/mdaClUAT-zZ)] + +### Associate (alias: SliceToMap) + +Returns a map containing key-value pairs provided by transform function applied to elements of the given slice. +If any of two pairs would have the same key the last one gets added to the map. + +The order of keys in returned map is not specified and is not guaranteed to be the same from the original array. + +```go +in := []*foo{{baz: "apple", bar: 1}, {baz: "banana", bar: 2}} + +aMap := lo.Associate[*foo, string, int](in, func (f *foo) (string, int) { + return f.baz, f.bar +}) +// map[string][int]{ "apple":1, "banana":2 } +``` + +[[play](https://go.dev/play/p/WHa2CfMO3Lr)] + ### Drop Drops n elements from the beginning of a slice or array. @@ -522,6 +626,8 @@ l := lo.Drop[int]([]int{0, 1, 2, 3, 4, 5}, 2) // []int{2, 3, 4, 5} ``` +[[play](https://go.dev/play/p/JswS7vXRJP2)] + ### DropRight Drops n elements from the end of a slice or array. @@ -531,6 +637,8 @@ l := lo.DropRight[int]([]int{0, 1, 2, 3, 4, 5}, 2) // []int{0, 1, 2, 3} ``` +[[play](https://go.dev/play/p/GG0nXkSJJa3)] + ### DropWhile Drop elements from the beginning of a slice or array while the predicate returns true. @@ -542,6 +650,8 @@ l := lo.DropWhile[string]([]string{"a", "aa", "aaa", "aa", "aa"}, func(val strin // []string{"aaa", "aa", "aa"} ``` +[[play](https://go.dev/play/p/7gBPYw2IK16)] + ### DropRightWhile Drop elements from the end of a slice or array while the predicate returns true. @@ -553,6 +663,8 @@ l := lo.DropRightWhile[string]([]string{"a", "aa", "aaa", "aa", "aa"}, func(val // []string{"a", "aa", "aaa"} ``` +[[play](https://go.dev/play/p/3-n71oEC0Hz)] + ### Reject The opposite of Filter, this method returns the elements of collection that predicate does not return truthy for. @@ -564,6 +676,8 @@ odd := lo.Reject[int]([]int{1, 2, 3, 4}, func(x int, _ int) bool { // []int{1, 3} ``` +[[play](https://go.dev/play/p/YkLMODy1WEL)] + ### Count Counts the number of elements in the collection that compare equal to value. @@ -573,6 +687,8 @@ count := lo.Count[int]([]int{1, 5, 1}, 1) // 2 ``` +[[play](https://go.dev/play/p/Y3FlK54yveC)] + ### CountBy Counts the number of elements in the collection for which predicate is true. @@ -584,9 +700,11 @@ count := lo.CountBy[int]([]int{1, 5, 1}, func(i int) bool { // 2 ``` +[[play](https://go.dev/play/p/ByQbNYQQi4X)] + ### Subset -Return part of a slice. +Returns a copy of a slice from `offset` up to `length` elements. Like `slice[start:start+length]`, but does not panic on overflow. ```go in := []int{0, 1, 2, 3, 4} @@ -601,6 +719,30 @@ sub := lo.Subset(in, -2, math.MaxUint) // []int{3, 4} ``` +[[play](https://go.dev/play/p/tOQu1GhFcog)] + +### Slice + +Returns a copy of a slice from `start` up to, but not including `end`. Like `slice[start:end]`, but does not panic on overflow. + +```go +in := []int{0, 1, 2, 3, 4} + +slice := lo.Slice(in, 0, 5) +// []int{0, 1, 2, 3, 4} + +slice := lo.Slice(in, 2, 3) +// []int{2} + +slice := lo.Slice(in, 2, 6) +// []int{2, 3, 4} + +slice := lo.Slice(in, 4, 3) +// []int{} +``` + +[[play](https://go.dev/play/p/8XWYhfMMA1h)] + ### Replace Returns a copy of the slice with the first n non-overlapping instances of old replaced by new. @@ -621,6 +763,8 @@ slice := lo.Replace(in, 0, 42, -1) // []int{42, 1, 42, 1, 2, 3, 42} ``` +[[play](https://go.dev/play/p/XfPzmf9gql6)] + ### ReplaceAll Returns a copy of the slice with all non-overlapping instances of old replaced by new. @@ -635,15 +779,56 @@ slice := lo.ReplaceAll(in, -1, 42) // []int{0, 1, 0, 1, 2, 3, 0} ``` +[[play](https://go.dev/play/p/a9xZFUHfYcV)] + +### Compact + +Returns a slice of all non-zero elements. + +```go +in := []string{"", "foo", "", "bar", ""} + +slice := lo.Compact[string](in) +// []string{"foo", "bar"} +``` + +[[play](https://go.dev/play/p/tXiy-iK6PAc)] + +### IsSorted + +Checks if a slice is sorted. + +```go +slice := lo.IsSorted([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) +// true +``` + +[[play](https://go.dev/play/p/mc3qR-t4mcx)] + +### IsSortedByKey + +Checks if a slice is sorted by iteratee. + +```go +slice := lo.IsSortedByKey([]string{"a", "bb", "ccc"}, func(s string) int { + return len(s) +}) +// true +``` + +[[play](https://go.dev/play/p/wiG6XyBBu49)] + ### Keys Creates an array of the map keys. ```go keys := lo.Keys[string, int](map[string]int{"foo": 1, "bar": 2}) -// []string{"bar", "foo"} +// []string{"foo", "bar"} ``` +[[play](https://go.dev/play/p/Uu11fHASqrU)] + ### Values Creates an array of the map values. @@ -653,6 +838,8 @@ values := lo.Values[string, int](map[string]int{"foo": 1, "bar": 2}) // []int{1, 2} ``` +[[play](https://go.dev/play/p/nnRTQkzQfF6)] + ### PickBy Returns same map type filtered by given predicate. @@ -664,6 +851,8 @@ m := lo.PickBy[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(k // map[string]int{"foo": 1, "baz": 3} ``` +[[play](https://go.dev/play/p/kdg8GR_QMmf)] + ### PickByKeys Returns same map type filtered by given keys. @@ -673,6 +862,8 @@ m := lo.PickByKeys[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, [] // map[string]int{"foo": 1, "baz": 3} ``` +[[play](https://go.dev/play/p/R1imbuci9qU)] + ### PickByValues Returns same map type filtered by given values. @@ -682,6 +873,8 @@ m := lo.PickByValues[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, // map[string]int{"foo": 1, "baz": 3} ``` +[[play](https://go.dev/play/p/1zdzSvbfsJc)] + ### OmitBy Returns same map type filtered by given predicate. @@ -693,6 +886,8 @@ m := lo.OmitBy[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(k // map[string]int{"bar": 2} ``` +[[play](https://go.dev/play/p/EtBsR43bdsd)] + ### OmitByKeys Returns same map type filtered by given keys. @@ -702,6 +897,8 @@ m := lo.OmitByKeys[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, [] // map[string]int{"bar": 2} ``` +[[play](https://go.dev/play/p/t1QjCrs-ysk)] + ### OmitByValues Returns same map type filtered by given values. @@ -711,7 +908,9 @@ m := lo.OmitByValues[string, int](map[string]int{"foo": 1, "bar": 2, "baz": 3}, // map[string]int{"bar": 2} ``` -### Entries +[[play](https://go.dev/play/p/9UYZi-hrs8j)] + +### Entries (alias: ToPairs) Transforms a map into array of key/value pairs. @@ -729,7 +928,9 @@ entries := lo.Entries[string, int](map[string]int{"foo": 1, "bar": 2}) // } ``` -### FromEntries +[[play](https://go.dev/play/p/3Dhgx46gawJ)] + +### FromEntries (alias: FromPairs) Transforms an array of key/value pairs into a map. @@ -747,18 +948,22 @@ m := lo.FromEntries[string, int]([]lo.Entry[string, int]{ // map[string]int{"foo": 1, "bar": 2} ``` +[[play](https://go.dev/play/p/oIr5KHFGCEN)] + ### Invert Creates a map composed of the inverted keys and values. If map contains duplicate values, subsequent values overwrite property assignments of previous values. ```go -m1 := lo.Invert[string, int]([map[string]int{"a": 1, "b": 2}) +m1 := lo.Invert[string, int](map[string]int{"a": 1, "b": 2}) // map[int]string{1: "a", 2: "b"} -m2 := lo.Invert[string, int]([map[string]int{"a": 1, "b": 2, "c": 1}) +m2 := lo.Invert[string, int](map[string]int{"a": 1, "b": 2, "c": 1}) // map[int]string{1: "c", 2: "b"} ``` +[[play](https://go.dev/play/p/rFQ4rak6iA1)] + ### Assign Merges multiple maps from left to right. @@ -771,6 +976,8 @@ mergedMaps := lo.Assign[string, int]( // map[string]int{"a": 1, "b": 3, "c": 4} ``` +[[play](https://go.dev/play/p/VhwfJOyxf5o)] + ### MapKeys Manipulates a map keys and transforms it to a map of another type. @@ -782,6 +989,8 @@ m2 := lo.MapKeys[int, int, string](map[int]int{1: 1, 2: 2, 3: 3, 4: 4}, func(_ i // map[string]int{"1": 1, "2": 2, "3": 3, "4": 4} ``` +[[play](https://go.dev/play/p/9_4WPIqOetJ)] + ### MapValues Manipulates a map values and transforms it to a map of another type. @@ -795,6 +1004,23 @@ m2 := lo.MapValues[int, int64, string](m1, func(x int64, _ int) string { // map[int]string{1: "1", 2: "2", 3: "3"} ``` +[[play](https://go.dev/play/p/T_8xAfvcf0W)] + +### MapToSlice + +Transforms a map into a slice based on specific iteratee. + +```go +m := map[int]int64{1: 4, 2: 5, 3: 6} + +s := lo.MapToSlice(m, func(k int, v int64) string { + return fmt.Sprintf("%d_%d", k, v) +}) +// []string{"1_4", "2_5", "3_6"} +``` + +[[play](https://go.dev/play/p/ZuiCZpDt6LD)] + ### Range / RangeFrom / RangeWithSteps Creates an array of numbers (positive and/or negative) progressing from start up to, but not including end. @@ -803,28 +1029,30 @@ Creates an array of numbers (positive and/or negative) progressing from start up result := Range(4) // [0, 1, 2, 3] -result := Range(-4); +result := Range(-4) // [0, -1, -2, -3] -result := RangeFrom(1, 5); -// [1, 2, 3, 4] +result := RangeFrom(1, 5) +// [1, 2, 3, 4, 5] -result := RangeFrom[float64](1.0, 5); -// [1.0, 2.0, 3.0, 4.0] +result := RangeFrom[float64](1.0, 5) +// [1.0, 2.0, 3.0, 4.0, 5.0] -result := RangeWithSteps(0, 20, 5); +result := RangeWithSteps(0, 20, 5) // [0, 5, 10, 15] -result := RangeWithSteps[float32](-1.0, -4.0, -1.0); +result := RangeWithSteps[float32](-1.0, -4.0, -1.0) // [-1.0, -2.0, -3.0] -result := RangeWithSteps(1, 4, -1); +result := RangeWithSteps(1, 4, -1) // [] -result := Range(0); +result := Range(0) // [] ``` +[[play](https://go.dev/play/p/0r6VimXAi9H)] + ### Clamp Clamps number within the inclusive lower and upper bounds. @@ -840,7 +1068,9 @@ r3 := lo.Clamp(42, -10, 10) // 10 ``` -### SumBy +[[play](https://go.dev/play/p/RU4lJNC2hlI)] + +### SumBy Summarizes the values in a collection using the given return value from the iteration function. If collection is empty 0 is returned. @@ -853,6 +1083,8 @@ sum := lo.SumBy(strings, func(item string) int { // 6 ``` +[[play](https://go.dev/play/p/Dz_a_7jN_ca)] + ### Substring Return part of a string. @@ -868,6 +1100,28 @@ sub := lo.Substring("hello", -2, math.MaxUint) // "lo" ``` +[[play](https://go.dev/play/p/TQlxQi82Lu1)] + +### ChunkString + +Returns an array of strings split into groups the length of size. If array can't be split evenly, the final chunk will be the remaining elements. + +```go +lo.ChunkString("123456", 2) +// []string{"12", "34", "56"} + +lo.ChunkString("1234567", 2) +// []string{"12", "34", "56", "7"} + +lo.ChunkString("", 2) +// []string{""} + +lo.ChunkString("1", 2) +// []string{"1"} +``` + +[[play](https://go.dev/play/p/__FLTuJVz54)] + ### RuneLength An alias to utf8.RuneCountInString which returns the number of runes in string. @@ -880,19 +1134,23 @@ sub := len("hellô") // 6 ``` +[[play](https://go.dev/play/p/tuhgW_lWY8l)] + ### T2 -> T9 Creates a tuple from a list of values. ```go -tuple1 := lo.T2[string, int]("x", 1) +tuple1 := lo.T2("x", 1) // Tuple2[string, int]{A: "x", B: 1} func example() (string, int) { return "y", 2 } -tuple2 := lo.T2[string, int](example()) +tuple2 := lo.T2(example()) // Tuple2[string, int]{A: "y", B: 2} ``` +[[play](https://go.dev/play/p/IllL3ZO4BQm)] + ### Unpack2 -> Unpack9 Returns values contained in tuple. @@ -902,6 +1160,8 @@ r1, r2 := lo.Unpack2[string, int](lo.Tuple2[string, int]{"a", 1}) // "a", 1 ``` +[[play](https://go.dev/play/p/xVP_k0kJ96W)] + ### Zip2 -> Zip9 Zip creates a slice of grouped elements, the first of which contains the first elements of the given arrays, the second of which contains the second elements of the given arrays, and so on. @@ -913,6 +1173,8 @@ tuples := lo.Zip2[string, int]([]string{"a", "b"}, []int{1, 2}) // []Tuple2[string, int]{{A: "a", B: 1}, {A: "b", B: 2}} ``` +[[play](https://go.dev/play/p/jujaA6GaJTp)] + ### Unzip2 -> Unzip9 Unzip accepts an array of grouped elements and creates an array regrouping the elements to their pre-zip configuration. @@ -923,6 +1185,235 @@ a, b := lo.Unzip2[string, int]([]Tuple2[string, int]{{A: "a", B: 1}, {A: "b", B: // []int{1, 2} ``` +[[play](https://go.dev/play/p/ciHugugvaAW)] + +### ChannelDispatcher + +Distributes messages from input channels into N child channels. Close events are propagated to children. + +Underlying channels can have a fixed buffer capacity or be unbuffered when cap is 0. + +```go +ch := make(chan int, 42) +for i := 0; i <= 10; i++ { + ch <- i +} + +children := lo.ChannelDispatcher(ch, 5, 10, DispatchingStrategyRoundRobin[int]) +// []<-chan int{...} + +consumer := func(c <-chan int) { + for { + msg, ok := <-c + if !ok { + println("closed") + break + } + + println(msg) + } +} + +for i := range children { + go consumer(children[i]) +} +``` + +Many distributions strategies are available: + +- [lo.DispatchingStrategyRoundRobin](./channel.go): Distributes messages in a rotating sequential manner. +- [lo.DispatchingStrategyRandom](./channel.go): Distributes messages in a random manner. +- [lo.DispatchingStrategyWeightedRandom](./channel.go): Distributes messages in a weighted manner. +- [lo.DispatchingStrategyFirst](./channel.go): Distributes messages in the first non-full channel. +- [lo.DispatchingStrategyLeast](./channel.go): Distributes messages in the emptiest channel. +- [lo.DispatchingStrategyMost](./channel.go): Distributes to the fulliest channel. + +Some strategies bring fallback, in order to favor non-blocking behaviors. See implementations. + +For custom strategies, just implement the `lo.DispatchingStrategy` prototype: + +```go +type DispatchingStrategy[T any] func(message T, messageIndex uint64, channels []<-chan T) int +``` + +Eg: + +```go +type Message struct { + TenantID uuid.UUID +} + +func hash(id uuid.UUID) int { + h := fnv.New32a() + h.Write([]byte(id.String())) + return int(h.Sum32()) +} + +// Routes messages per TenantID. +customStrategy := func(message pubsub.AMQPSubMessage, messageIndex uint64, channels []<-chan pubsub.AMQPSubMessage) int { + destination := hash(message.TenantID) % len(channels) + + // check if channel is full + if len(channels[destination]) < cap(channels[destination]) { + return destination + } + + // fallback when child channel is full + return utils.DispatchingStrategyRoundRobin(message, uint64(destination), channels) +} + +children := lo.ChannelDispatcher(ch, 5, 10, customStrategy) +... +``` + +### SliceToChannel + +Returns a read-only channels of collection elements. Channel is closed after last element. Channel capacity can be customized. + +```go +list := []int{1, 2, 3, 4, 5} + +for v := range lo.SliceToChannel(2, list) { + println(v) +} +// prints 1, then 2, then 3, then 4, then 5 +``` + +### Generator + +Implements the generator design pattern. Channel is closed after last element. Channel capacity can be customized. + +```go +generator := func(yield func(int)) { + yield(1) + yield(2) + yield(3) +} + +for v := range lo.Generator(2, generator) { + println(v) +} +// prints 1, then 2, then 3 +``` + +### Batch + +Creates a slice of n elements from a channel. Returns the slice, the slice length, the read time and the channel status (opened/closed). + +```go +ch := lo.SliceToChannel(2, []int{1, 2, 3, 4, 5}) + +items1, length1, duration1, ok1 := lo.Batch(ch, 3) +// []int{1, 2, 3}, 3, 0s, true +items2, length2, duration2, ok2 := lo.Batch(ch, 3) +// []int{4, 5}, 2, 0s, false +``` + +Example: RabbitMQ consumer 👇 + +```go +ch := readFromQueue() + +for { + // read 1k items + items, length, _, ok := lo.Batch(ch, 1000) + + // do batching stuff + + if !ok { + break + } +} +``` + +### BatchWithTimeout + +Creates a slice of n elements from a channel, with timeout. Returns the slice, the slice length, the read time and the channel status (opened/closed). + +```go +generator := func(yield func(int)) { + for i := 0; i < 5; i++ { + yield(i) + time.Sleep(35*time.Millisecond) + } +} + +ch := lo.Generator(0, generator) + +items1, length1, duration1, ok1 := lo.BatchWithTimeout(ch, 3, 100*time.Millisecond) +// []int{1, 2}, 2, 100ms, true +items2, length2, duration2, ok2 := lo.BatchWithTimeout(ch, 3, 100*time.Millisecond) +// []int{3, 4, 5}, 3, 75ms, true +items3, length3, duration2, ok3 := lo.BatchWithTimeout(ch, 3, 100*time.Millisecond) +// []int{}, 0, 10ms, false +``` + +Example: RabbitMQ consumer 👇 + +```go +ch := readFromQueue() + +for { + // read 1k items + // wait up to 1 second + items, length, _, ok := lo.BatchWithTimeout(ch, 1000, 1*time.Second) + + // do batching stuff + + if !ok { + break + } +} +``` + +Example: Multithreaded RabbitMQ consumer 👇 + +```go +ch := readFromQueue() + +// 5 workers +// prefetch 1k messages per worker +children := lo.ChannelDispatcher(ch, 5, 1000, DispatchingStrategyFirst[int]) + +consumer := func(c <-chan int) { + for { + // read 1k items + // wait up to 1 second + items, length, _, ok := lo.BatchWithTimeout(ch, 1000, 1*time.Second) + + // do batching stuff + + if !ok { + break + } + } +} + +for i := range children { + go consumer(children[i]) +} +``` + +### Contains + +Returns true if an element is present in a collection. + +```go +present := lo.Contains[int]([]int{0, 1, 2, 3, 4, 5}, 5) +// true +``` + +### ContainsBy + +Returns true if the predicate function returns `true`. + +```go +present := lo.ContainsBy[int]([]int{0, 1, 2, 3, 4, 5}, func(x int) bool { + return x == 3 +}) +// true +``` + ### Every Returns true if all elements of a subset are contained into a collection or if the subset is empty. @@ -961,7 +1452,7 @@ ok := lo.Some[int]([]int{0, 1, 2, 3, 4, 5}, []int{-1, 6}) ### SomeBy -Returns true if the predicate returns true for any of the elements in the collection. +Returns true if the predicate returns true for any of the elements in the collection. If the collection is empty SomeBy returns false. ```go @@ -1032,6 +1523,27 @@ union := lo.Union[int]([]int{0, 1, 2, 3, 4, 5}, []int{0, 2, 10}) // []int{0, 1, 2, 3, 4, 5, 10} ``` +### Without + +Returns slice excluding all given values. + +```go +subset := lo.Without[int]([]int{0, 2, 10}, 2) +// []int{0, 10} + +subset := lo.Without[int]([]int{0, 2, 10}, 0, 1, 2, 3, 4, 5) +// []int{10} +``` + +### WithoutEmpty + +Returns slice excluding empty values. + +```go +subset := lo.WithoutEmpty[int]([]int{0, 2, 10}) +// []int{2, 10} +``` + ### IndexOf Returns the index at which the first occurrence of a value is found in an array or return -1 if the value cannot be found. @@ -1044,7 +1556,7 @@ notFound := lo.IndexOf[int]([]int{0, 1, 2, 1, 2, 3}, 6) // -1 ``` -### LastIndex +### LastIndexOf Returns the index at which the last occurrence of a value is found in an array or return -1 if the value cannot be found. @@ -1104,6 +1616,80 @@ str, index, ok := lo.FindLastIndexOf[string]([]string{"foobar"}, func(i string) // "", -1, false ``` +### FindKey + +Returns the key of the first value matching. + +```go +result1, ok1 := lo.FindKey(map[string]int{"foo": 1, "bar": 2, "baz": 3}, 2) +// "bar", true + +result2, ok2 := lo.FindKey(map[string]int{"foo": 1, "bar": 2, "baz": 3}, 42) +// "", false + +type test struct { + foobar string +} +result3, ok3 := lo.FindKey(map[string]test{"foo": test{"foo"}, "bar": test{"bar"}, "baz": test{"baz"}}, test{"foo"}) +// "foo", true +``` + +### FindKeyBy + +Returns the key of the first element predicate returns truthy for. + +```go +result1, ok1 := lo.FindKeyBy(map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(k string, v int) bool { + return k == "foo" +}) +// "foo", true + +result2, ok2 := lo.FindKeyBy(map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(k string, v int) bool { + return false +}) +// "", false +``` + +### FindUniques + +Returns a slice with all the unique elements of the collection. The order of result values is determined by the order they occur in the array. + +```go +uniqueValues := lo.FindUniques[int]([]int{1, 2, 2, 1, 2, 3}) +// []int{3} +``` + +### FindUniquesBy + +Returns a slice with all the unique elements of the collection. The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is invoked for each element in array to generate the criterion by which uniqueness is computed. + +```go +uniqueValues := lo.FindUniquesBy[int, int]([]int{3, 4, 5, 6, 7}, func(i int) int { + return i%3 +}) +// []int{5} +``` + +### FindDuplicates + +Returns a slice with the first occurence of each duplicated elements of the collection. The order of result values is determined by the order they occur in the array. + +```go +duplicatedValues := lo.FindDuplicates[int]([]int{1, 2, 2, 1, 2, 3}) +// []int{1, 2} +``` + +### FindDuplicatesBy + +Returns a slice with the first occurence of each duplicated elements of the collection. The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is invoked for each element in array to generate the criterion by which uniqueness is computed. + +```go +duplicatedValues := lo.FindDuplicatesBy[int, int]([]int{3, 4, 5, 6, 7}, func(i int) int { + return i%3 +}) +// []int{3, 4} +``` + ### Min Search the minimum value of a collection. @@ -1216,6 +1802,31 @@ result := lo.Ternary[string](false, "a", "b") // "b" ``` +[[play](https://go.dev/play/p/t-D7WBL44h2)] + +### TernaryF + +A 1 line if/else statement whose options are functions. + +```go +result := lo.TernaryF[string](true, func() string { return "a" }, func() string { return "b" }) +// "a" + +result := lo.TernaryF[string](false, func() string { return "a" }, func() string { return "b" }) +// "b" +``` + +Useful to avoid nil-pointer dereferencing in intializations, or avoid running unnecessary code + +```go +var s *string + +someStr := TernaryF[string](s == nil, func() string { return uuid.New().String() }, func() string { return *s }) +// ef782193-c30c-4e2e-a7ae-f8ab5e125e02 +``` + +[[play](https://go.dev/play/p/AO4VW20JoqM)] + ### If / ElseIf / Else ```go @@ -1260,6 +1871,8 @@ result := lo.IfF[int](true, func () int { // 1 ``` +[[play](https://go.dev/play/p/WSw3ApMxhyW)] + ### Switch / Case / Default ```go @@ -1309,6 +1922,8 @@ result := lo.Switch[int, string](1). // "1" ``` +[[play](https://go.dev/play/p/TGbKUMAeRUd)] + ### ToPtr Returns a pointer copy of value. @@ -1318,6 +1933,32 @@ ptr := lo.ToPtr[string]("hello world") // *string{"hello world"} ``` +### FromPtr + +Returns the pointer value or empty. + +```go +str := "hello world" +value := lo.FromPtr[string](&str) +// "hello world" + +value := lo.FromPtr[string](nil) +// "" +``` + +### FromPtrOr + +Returns the pointer value or the fallback value. + +```go +str := "hello world" +value := lo.FromPtrOr[string](&str, "empty") +// "hello world" + +value := lo.FromPtrOr[string](nil, "empty") +// "empty" +``` + ### ToSlicePtr Returns a slice of pointer copy of value. @@ -1327,6 +1968,27 @@ ptr := lo.ToSlicePtr[string]([]string{"hello", "world"}) // []*string{"hello", "world"} ``` +### ToAnySlice + +Returns a slice with all elements mapped to `any` type. + +```go +elements := lo.ToAnySlice[int]([]int{1, 5, 1}) +// []any{1, 5, 1} +``` + +### FromAnySlice + +Returns an `any` slice with all elements mapped to a type. Returns false in case of type conversion failure. + +```go +elements, ok := lo.FromAnySlice[string]([]any{"foobar", 42}) +// []string{}, false + +elements, ok := lo.FromAnySlice[string]([]any{"foobar", "42"}) +// []string{"foobar", "42"}, true +``` + ### Empty Returns an empty value. @@ -1340,6 +2002,56 @@ lo.Empty[bool]() // false ``` +### IsEmpty + +Returns true if argument is a zero value. + +```go +lo.IsEmpty[int](0) +// true +lo.IsEmpty[int](42) +// false + +lo.IsEmpty[string]("") +// true +lo.IsEmpty[bool]("foobar") +// false + +type test struct { + foobar string +} + +lo.IsEmpty[test](test{foobar: ""}) +// true +lo.IsEmpty[test](test{foobar: "foobar"}) +// false +``` + +### IsNotEmpty + +Returns true if argument is a zero value. + +```go +lo.IsNotEmpty[int](0) +// false +lo.IsNotEmpty[int](42) +// true + +lo.IsNotEmpty[string]("") +// false +lo.IsNotEmpty[bool]("foobar") +// true + +type test struct { + foobar string +} + +lo.IsNotEmpty[test](test{foobar: ""}) +// false +lo.IsNotEmpty[test](test{foobar: "foobar"}) +// true +``` + ### Coalesce Returns the first non-empty arguments. Arguments must be comparable. @@ -1357,9 +2069,24 @@ result, ok := lo.Coalesce[*string](nil, nilStr, &str) // &"foobar" true ``` +### Partial + +Returns new function that, when called, has its first argument set to the provided value. + +```go +add := func(x, y int) int { return x + y } +f := lo.Partial(add, 5) + +f(10) +// 15 + +f(42) +// 47 +``` + ### Attempt -Invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a sucessfull response is returned. +Invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a successful response is returned. ```go iter, err := lo.Attempt(42, func(i int) error { @@ -1395,11 +2122,13 @@ iter, err := lo.Attempt(0, func(i int) error { For more advanced retry strategies (delay, exponential backoff...), please take a look on [cenkalti/backoff](https://github.com/cenkalti/backoff). +[[play](https://go.dev/play/p/3ggJZ2ZKcMj)] + ### AttemptWithDelay -Invokes a function N times until it returns valid output, with a pause betwwen each call. Returning either the caught error or nil. +Invokes a function N times until it returns valid output, with a pause between each call. Returning either the caught error or nil. -When first argument is less than `1`, the function runs until a sucessfull response is returned. +When first argument is less than `1`, the function runs until a successful response is returned. ```go iter, duration, err := lo.AttemptWithDelay(5, 2*time.Second, func(i int, duration time.Duration) error { @@ -1416,6 +2145,8 @@ iter, duration, err := lo.AttemptWithDelay(5, 2*time.Second, func(i int, duratio For more advanced retry strategies (delay, exponential backoff...), please take a look on [cenkalti/backoff](https://github.com/cenkalti/backoff). +[[play](https://go.dev/play/p/tVs6CygC7m1)] + ### Debounce `NewDebounce` creates a debounced instance that delays invoking functions given until after wait milliseconds have elapsed, until `cancel` is called. @@ -1434,6 +2165,8 @@ time.Sleep(1 * time.Second) cancel() ``` +[[play](https://go.dev/play/p/mz32VMK2nqe)] + ### Synchronize Wraps the underlying callback in a mutex. It receives an optional mutex. @@ -1493,6 +2226,22 @@ ch := lo.Async2(func() (int, string) { // chan lo.Tuple2[int, string] ({42, "Hello"}) ``` +### Validate + +Helper function that creates an error when a condition is not met. + +```go +slice := []string{"a"} +val := lo.Validate(len(slice) == 0, "Slice should be empty but contains %v", slice) +// error("Slice should be empty but contains [a]") + +slice := []string{} +val := lo.Validate(len(slice) == 0, "Slice should be empty but contains %v", slice) +// nil +``` + +[[play](https://go.dev/play/p/vPyh51XpCBt)] + ### Must Wraps a function call to panics if second argument is `error` or `false`, returns the value otherwise. @@ -1505,8 +2254,12 @@ val := lo.Must(time.Parse("2006-01-02", "bad-value")) // panics ``` +[[play](https://go.dev/play/p/TMoWrRp3DyC)] + ### Must{0->6} -Must* has the same behavior than Must, but returns multiple values. + +Must\* has the same behavior than Must, but returns multiple values. + ```go func example0() (error) func example1() (int, error) @@ -1525,7 +2278,8 @@ val1, val2, val3, val4, val5 := lo.Must5(example5()) val1, val2, val3, val4, val5, val6 := lo.Must6(example6()) ``` -You can wrap functions like `func (...) (..., ok bool)`. +You can wrap functions like `func (...) (..., ok bool)`. + ```go // math.Signbit(float64) bool lo.Must0(math.Signbit(v)) @@ -1534,7 +2288,23 @@ lo.Must0(math.Signbit(v)) before, after := lo.Must2(bytes.Cut(s, sep)) ``` -## Try +You can give context to the panic message by adding some printf-like arguments. + +```go +val, ok := lo.Find(myString, func(i string) bool { + return i == requiredChar +}) +lo.Must0(ok, "'%s' must always contain '%s'", myString, requiredChar) + +list := []int{0, 1, 2} +item := 5 +lo.Must0(lo.Contains[int](list, item), "'%s' must always contain '%s'", list, item) +... +``` + +[[play](https://go.dev/play/p/TMoWrRp3DyC)] + +### Try Calls the function and return false in case of error and on panic. @@ -1556,7 +2326,9 @@ ok := lo.Try(func() error { // false ``` -## Try{0->6} +[[play](https://go.dev/play/p/mTyyWUvn9u4)] + +### Try{0->6} The same behavior than `Try`, but callback returns 2 variables. @@ -1568,7 +2340,52 @@ ok := lo.Try2(func() (string, error) { // false ``` -## TryWithErrorValue +[[play](https://go.dev/play/p/mTyyWUvn9u4)] + +### TryOr + +Calls the function and return a default value in case of error and on panic. + +```go +str, ok := lo.TryOr(func() (string, error) { + panic("error") + return "hello", nil +}, "world") +// world +// false + +ok := lo.TryOr(func() error { + return "hello", nil +}, "world") +// hello +// true + +ok := lo.TryOr(func() error { + return "hello", fmt.Errorf("error") +}, "world") +// world +// false +``` + +[[play](https://go.dev/play/p/B4F7Wg2Zh9X)] + +### TryOr{0->6} + +The same behavior than `TryOr`, but callback returns 2 variables. + +```go +str, nbr, ok := lo.TryOr2(func() (string, int, error) { + panic("error") + return "hello", 42, nil +}, "world", 21) +// world +// 21 +// false +``` + +[[play](https://go.dev/play/p/B4F7Wg2Zh9X)] + +### TryWithErrorValue The same behavior than `Try`, but also returns value passed to panic. @@ -1580,7 +2397,9 @@ err, ok := lo.TryWithErrorValue(func() error { // "error", false ``` -## TryCatch +[[play](https://go.dev/play/p/Kc7afQIT2Fs)] + +### TryCatch The same behavior than `Try`, but calls the catch function in case of error. @@ -1597,7 +2416,9 @@ ok := lo.TryCatch(func() error { // caught == true ``` -## TryCatchWithErrorValue +[[play](https://go.dev/play/p/PnOON-EqBiU)] + +### TryCatchWithErrorValue The same behavior than `TryWithErrorValue`, but calls the catch function in case of error. @@ -1614,6 +2435,33 @@ ok := lo.TryCatchWithErrorValue(func() error { // caught == true ``` +[[play](https://go.dev/play/p/8Pc9gwX_GZO)] + +### ErrorsAs + +A shortcut for: + +```go +err := doSomething() + +var rateLimitErr *RateLimitError +if ok := errors.As(err, &rateLimitErr); ok { + // retry later +} +``` + +1 line `lo` helper: + +```go +err := doSomething() + +if rateLimitErr, ok := lo.ErrorsAs[*RateLimitError](err); ok { + // retry later +} +``` + +[[play](https://go.dev/play/p/8wk5rH8UfrE)] + ## 🛩 Benchmark We executed a simple benchmark with the a dead-simple `lo.Map` loop: @@ -1645,10 +2493,10 @@ PASS ok github.com/samber/lo 6.657s ``` -- `lo.Map` is way faster (x7) than `go-funk`, a relection-based Map implementation. +- `lo.Map` is way faster (x7) than `go-funk`, a reflection-based Map implementation. - `lo.Map` have the same allocation profile than `for`. - `lo.Map` is 4% slower than `for`. -- `lop.Map` is slower than `lo.Map` because it implies more memory allocation and locks. `lop.Map` will be usefull for long-running callbacks, such as i/o bound processing. +- `lop.Map` is slower than `lo.Map` because it implies more memory allocation and locks. `lop.Map` will be useful for long-running callbacks, such as i/o bound processing. - `for` beats other implementations for memory and CPU. ## 🤝 Contributing @@ -1659,14 +2507,6 @@ ok github.com/samber/lo 6.657s Don't hesitate ;) -### Install go 1.18 - -```bash -make go1.18beta1 -``` - -If your OS currently not default to Go 1.18, replace `BIN=go` by `BIN=go1.18beta1` in the Makefile. - ### With Docker ```bash diff --git a/vendor/github.com/samber/lo/channel.go b/vendor/github.com/samber/lo/channel.go new file mode 100644 index 00000000..ccbd3602 --- /dev/null +++ b/vendor/github.com/samber/lo/channel.go @@ -0,0 +1,228 @@ +package lo + +import ( + "math/rand" + "time" +) + +type DispatchingStrategy[T any] func(msg T, index uint64, channels []<-chan T) int + +// ChannelDispatcher distributes messages from input channels into N child channels. +// Close events are propagated to children. +// Underlying channels can have a fixed buffer capacity or be unbuffered when cap is 0. +func ChannelDispatcher[T any](stream <-chan T, count int, channelBufferCap int, strategy DispatchingStrategy[T]) []<-chan T { + children := createChannels[T](count, channelBufferCap) + + roChildren := channelsToReadOnly(children) + + go func() { + // propagate channel closing to children + defer closeChannels(children) + + var i uint64 = 0 + + for { + msg, ok := <-stream + if !ok { + return + } + + destination := strategy(msg, i, roChildren) % count + children[destination] <- msg + + i++ + } + }() + + return roChildren +} + +func createChannels[T any](count int, channelBufferCap int) []chan T { + children := make([]chan T, 0, count) + + for i := 0; i < count; i++ { + children = append(children, make(chan T, channelBufferCap)) + } + + return children +} + +func channelsToReadOnly[T any](children []chan T) []<-chan T { + roChildren := make([]<-chan T, 0, len(children)) + + for i := range children { + roChildren = append(roChildren, children[i]) + } + + return roChildren +} + +func closeChannels[T any](children []chan T) { + for i := 0; i < len(children); i++ { + close(children[i]) + } +} + +func channelIsNotFull[T any](ch <-chan T) bool { + return cap(ch) == 0 || len(ch) < cap(ch) +} + +// DispatchingStrategyRoundRobin distributes messages in a rotating sequential manner. +// If the channel capacity is exceeded, the next channel will be selected and so on. +func DispatchingStrategyRoundRobin[T any](msg T, index uint64, channels []<-chan T) int { + for { + i := int(index % uint64(len(channels))) + if channelIsNotFull(channels[i]) { + return i + } + + index++ + time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥 + } +} + +// DispatchingStrategyRandom distributes messages in a random manner. +// If the channel capacity is exceeded, another random channel will be selected and so on. +func DispatchingStrategyRandom[T any](msg T, index uint64, channels []<-chan T) int { + for { + i := rand.Intn(len(channels)) + if channelIsNotFull(channels[i]) { + return i + } + + time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥 + } +} + +// DispatchingStrategyRandom distributes messages in a weighted manner. +// If the channel capacity is exceeded, another random channel will be selected and so on. +func DispatchingStrategyWeightedRandom[T any](weights []int) DispatchingStrategy[T] { + seq := []int{} + + for i := 0; i < len(weights); i++ { + for j := 0; j < weights[i]; j++ { + seq = append(seq, i) + } + } + + return func(msg T, index uint64, channels []<-chan T) int { + for { + i := seq[rand.Intn(len(seq))] + if channelIsNotFull(channels[i]) { + return i + } + + time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥 + } + } +} + +// DispatchingStrategyFirst distributes messages in the first non-full channel. +// If the capacity of the first channel is exceeded, the second channel will be selected and so on. +func DispatchingStrategyFirst[T any](msg T, index uint64, channels []<-chan T) int { + for { + for i := range channels { + if channelIsNotFull(channels[i]) { + return i + } + } + + time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥 + } +} + +// DispatchingStrategyLeast distributes messages in the emptiest channel. +func DispatchingStrategyLeast[T any](msg T, index uint64, channels []<-chan T) int { + seq := Range(len(channels)) + + return MinBy(seq, func(item int, min int) bool { + return len(channels[item]) < len(channels[min]) + }) +} + +// DispatchingStrategyMost distributes messages in the fulliest channel. +// If the channel capacity is exceeded, the next channel will be selected and so on. +func DispatchingStrategyMost[T any](msg T, index uint64, channels []<-chan T) int { + seq := Range(len(channels)) + + return MaxBy(seq, func(item int, max int) bool { + return len(channels[item]) > len(channels[max]) && channelIsNotFull(channels[item]) + }) +} + +// SliceToChannel returns a read-only channels of collection elements. +func SliceToChannel[T any](bufferSize int, collection []T) <-chan T { + ch := make(chan T, bufferSize) + + go func() { + for _, item := range collection { + ch <- item + } + + close(ch) + }() + + return ch +} + +// Generator implements the generator design pattern. +func Generator[T any](bufferSize int, generator func(yield func(T))) <-chan T { + ch := make(chan T, bufferSize) + + go func() { + // WARNING: infinite loop + generator(func(t T) { + ch <- t + }) + + close(ch) + }() + + return ch +} + +// Batch creates a slice of n elements from a channel. Returns the slice and the slice length. +// @TODO: we should probaby provide an helper that reuse the same buffer. +func Batch[T any](ch <-chan T, size int) (collection []T, length int, readTime time.Duration, ok bool) { + buffer := make([]T, 0, size) + index := 0 + now := time.Now() + + for ; index < size; index++ { + item, ok := <-ch + if !ok { + return buffer, index, time.Since(now), false + } + + buffer = append(buffer, item) + } + + return buffer, index, time.Since(now), true +} + +// BatchWithTimeout creates a slice of n elements from a channel, with timeout. Returns the slice and the slice length. +// @TODO: we should probaby provide an helper that reuse the same buffer. +func BatchWithTimeout[T any](ch <-chan T, size int, timeout time.Duration) (collection []T, length int, readTime time.Duration, ok bool) { + expire := time.NewTimer(timeout) + defer expire.Stop() + + buffer := make([]T, 0, size) + index := 0 + now := time.Now() + + for ; index < size; index++ { + select { + case item, ok := <-ch: + if !ok { + return buffer, index, time.Since(now), false + } + + buffer = append(buffer, item) + + case <-expire.C: + return buffer, index, time.Since(now), true + } + } + + return buffer, index, time.Since(now), true +} diff --git a/vendor/github.com/samber/lo/condition.go b/vendor/github.com/samber/lo/condition.go index 6fd31f1e..1d4e75d2 100644 --- a/vendor/github.com/samber/lo/condition.go +++ b/vendor/github.com/samber/lo/condition.go @@ -1,6 +1,7 @@ package lo // Ternary is a 1 line if/else statement. +// Play: https://go.dev/play/p/t-D7WBL44h2 func Ternary[T any](condition bool, ifOutput T, elseOutput T) T { if condition { return ifOutput @@ -9,12 +10,23 @@ func Ternary[T any](condition bool, ifOutput T, elseOutput T) T { return elseOutput } +// TernaryF is a 1 line if/else statement whose options are functions +// Play: https://go.dev/play/p/AO4VW20JoqM +func TernaryF[T any](condition bool, ifFunc func() T, elseFunc func() T) T { + if condition { + return ifFunc() + } + + return elseFunc() +} + type ifElse[T any] struct { result T done bool } // If. +// Play: https://go.dev/play/p/WSw3ApMxhyW func If[T any](condition bool, result T) *ifElse[T] { if condition { return &ifElse[T]{result, true} @@ -25,6 +37,7 @@ func If[T any](condition bool, result T) *ifElse[T] { } // IfF. +// Play: https://go.dev/play/p/WSw3ApMxhyW func IfF[T any](condition bool, resultF func() T) *ifElse[T] { if condition { return &ifElse[T]{resultF(), true} @@ -35,6 +48,7 @@ func IfF[T any](condition bool, resultF func() T) *ifElse[T] { } // ElseIf. +// Play: https://go.dev/play/p/WSw3ApMxhyW func (i *ifElse[T]) ElseIf(condition bool, result T) *ifElse[T] { if !i.done && condition { i.result = result @@ -45,6 +59,7 @@ func (i *ifElse[T]) ElseIf(condition bool, result T) *ifElse[T] { } // ElseIfF. +// Play: https://go.dev/play/p/WSw3ApMxhyW func (i *ifElse[T]) ElseIfF(condition bool, resultF func() T) *ifElse[T] { if !i.done && condition { i.result = resultF() @@ -55,6 +70,7 @@ func (i *ifElse[T]) ElseIfF(condition bool, resultF func() T) *ifElse[T] { } // Else. +// Play: https://go.dev/play/p/WSw3ApMxhyW func (i *ifElse[T]) Else(result T) T { if i.done { return i.result @@ -64,6 +80,7 @@ func (i *ifElse[T]) Else(result T) T { } // ElseF. +// Play: https://go.dev/play/p/WSw3ApMxhyW func (i *ifElse[T]) ElseF(resultF func() T) T { if i.done { return i.result @@ -79,6 +96,7 @@ type switchCase[T comparable, R any] struct { } // Switch is a pure functional switch/case/default statement. +// Play: https://go.dev/play/p/TGbKUMAeRUd func Switch[T comparable, R any](predicate T) *switchCase[T, R] { var result R @@ -90,6 +108,7 @@ func Switch[T comparable, R any](predicate T) *switchCase[T, R] { } // Case. +// Play: https://go.dev/play/p/TGbKUMAeRUd func (s *switchCase[T, R]) Case(val T, result R) *switchCase[T, R] { if !s.done && s.predicate == val { s.result = result @@ -100,6 +119,7 @@ func (s *switchCase[T, R]) Case(val T, result R) *switchCase[T, R] { } // CaseF. +// Play: https://go.dev/play/p/TGbKUMAeRUd func (s *switchCase[T, R]) CaseF(val T, cb func() R) *switchCase[T, R] { if !s.done && s.predicate == val { s.result = cb() @@ -110,6 +130,7 @@ func (s *switchCase[T, R]) CaseF(val T, cb func() R) *switchCase[T, R] { } // Default. +// Play: https://go.dev/play/p/TGbKUMAeRUd func (s *switchCase[T, R]) Default(result R) R { if !s.done { s.result = result @@ -119,6 +140,7 @@ func (s *switchCase[T, R]) Default(result R) R { } // DefaultF. +// Play: https://go.dev/play/p/TGbKUMAeRUd func (s *switchCase[T, R]) DefaultF(cb func() R) R { if !s.done { s.result = cb() diff --git a/vendor/github.com/samber/lo/docker-compose.yml b/vendor/github.com/samber/lo/docker-compose.yml index c6f3f652..511e85fd 100644 --- a/vendor/github.com/samber/lo/docker-compose.yml +++ b/vendor/github.com/samber/lo/docker-compose.yml @@ -2,8 +2,8 @@ version: '3' services: dev: - build: . + image: golang:1.18-bullseye volumes: - ./:/go/src/github.com/samber/lo working_dir: /go/src/github.com/samber/lo - command: bash -c 'make tools ; make watch-test' + command: make watch-test diff --git a/vendor/github.com/samber/lo/errors.go b/vendor/github.com/samber/lo/errors.go index 0b50ad49..0a4cb5b8 100644 --- a/vendor/github.com/samber/lo/errors.go +++ b/vendor/github.com/samber/lo/errors.go @@ -1,62 +1,115 @@ package lo +import ( + "errors" + "fmt" + "reflect" +) + +// Validate is a helper that creates an error when a condition is not met. +// Play: https://go.dev/play/p/vPyh51XpCBt +func Validate(ok bool, format string, args ...any) error { + if !ok { + return fmt.Errorf(fmt.Sprintf(format, args...)) + } + return nil +} + +func messageFromMsgAndArgs(msgAndArgs ...interface{}) string { + if len(msgAndArgs) == 1 { + if msgAsStr, ok := msgAndArgs[0].(string); ok { + return msgAsStr + } + return fmt.Sprintf("%+v", msgAndArgs[0]) + } + if len(msgAndArgs) > 1 { + return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) + } + return "" +} + // must panics if err is error or false. -func must(err any) { - b, isBool := err.(bool) - if isBool && !b { - panic("not ok") +func must(err any, messageArgs ...interface{}) { + if err == nil { + return } - e, isError := err.(error) - if isError { - panic(e) + switch e := err.(type) { + case bool: + if !e { + message := messageFromMsgAndArgs(messageArgs...) + if message == "" { + message = "not ok" + } + + panic(message) + } + + case error: + message := messageFromMsgAndArgs(messageArgs...) + if message != "" { + panic(message + ": " + e.Error()) + } else { + panic(e.Error()) + } + + default: + panic("must: invalid err type '" + reflect.TypeOf(err).Name() + "', should either be a bool or an error") } } // Must is a helper that wraps a call to a function returning a value and an error // and panics if err is error or false. -func Must[T any](val T, err any) T { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must[T any](val T, err any, messageArgs ...interface{}) T { + must(err, messageArgs...) return val } // Must0 has the same behavior than Must, but callback returns no variable. -func Must0(err any) { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must0(err any, messageArgs ...interface{}) { + must(err, messageArgs...) } // Must1 is an alias to Must -func Must1[T any](val T, err any) T { - return Must(val, err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must1[T any](val T, err any, messageArgs ...interface{}) T { + return Must(val, err, messageArgs...) } // Must2 has the same behavior than Must, but callback returns 2 variables. -func Must2[T1 any, T2 any](val1 T1, val2 T2, err any) (T1, T2) { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must2[T1 any, T2 any](val1 T1, val2 T2, err any, messageArgs ...interface{}) (T1, T2) { + must(err, messageArgs...) return val1, val2 } // Must3 has the same behavior than Must, but callback returns 3 variables. -func Must3[T1 any, T2 any, T3 any](val1 T1, val2 T2, val3 T3, err any) (T1, T2, T3) { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must3[T1 any, T2 any, T3 any](val1 T1, val2 T2, val3 T3, err any, messageArgs ...interface{}) (T1, T2, T3) { + must(err, messageArgs...) return val1, val2, val3 } // Must4 has the same behavior than Must, but callback returns 4 variables. -func Must4[T1 any, T2 any, T3 any, T4 any](val1 T1, val2 T2, val3 T3, val4 T4, err any) (T1, T2, T3, T4) { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must4[T1 any, T2 any, T3 any, T4 any](val1 T1, val2 T2, val3 T3, val4 T4, err any, messageArgs ...interface{}) (T1, T2, T3, T4) { + must(err, messageArgs...) return val1, val2, val3, val4 } // Must5 has the same behavior than Must, but callback returns 5 variables. -func Must5[T1 any, T2 any, T3 any, T4 any, T5 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, err any) (T1, T2, T3, T4, T5) { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must5[T1 any, T2 any, T3 any, T4 any, T5 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, err any, messageArgs ...interface{}) (T1, T2, T3, T4, T5) { + must(err, messageArgs...) return val1, val2, val3, val4, val5 } // Must6 has the same behavior than Must, but callback returns 6 variables. -func Must6[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, val6 T6, err any) (T1, T2, T3, T4, T5, T6) { - must(err) +// Play: https://go.dev/play/p/TMoWrRp3DyC +func Must6[T1 any, T2 any, T3 any, T4 any, T5 any, T6 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, val6 T6, err any, messageArgs ...interface{}) (T1, T2, T3, T4, T5, T6) { + must(err, messageArgs...) return val1, val2, val3, val4, val5, val6 } @@ -79,6 +132,7 @@ func Try(callback func() error) (ok bool) { } // Try0 has the same behavior than Try, but callback returns no variable. +// Play: https://go.dev/play/p/mTyyWUvn9u4 func Try0(callback func()) bool { return Try(func() error { callback() @@ -87,11 +141,13 @@ func Try0(callback func()) bool { } // Try1 is an alias to Try. -func Try1[T any](callback func() error) bool { +// Play: https://go.dev/play/p/mTyyWUvn9u4 +func Try1(callback func() error) bool { return Try(callback) } // Try2 has the same behavior than Try, but callback returns 2 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 func Try2[T any](callback func() (T, error)) bool { return Try(func() error { _, err := callback() @@ -100,6 +156,7 @@ func Try2[T any](callback func() (T, error)) bool { } // Try3 has the same behavior than Try, but callback returns 3 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 func Try3[T, R any](callback func() (T, R, error)) bool { return Try(func() error { _, _, err := callback() @@ -108,6 +165,7 @@ func Try3[T, R any](callback func() (T, R, error)) bool { } // Try4 has the same behavior than Try, but callback returns 4 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 func Try4[T, R, S any](callback func() (T, R, S, error)) bool { return Try(func() error { _, _, _, err := callback() @@ -116,6 +174,7 @@ func Try4[T, R, S any](callback func() (T, R, S, error)) bool { } // Try5 has the same behavior than Try, but callback returns 5 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 func Try5[T, R, S, Q any](callback func() (T, R, S, Q, error)) bool { return Try(func() error { _, _, _, _, err := callback() @@ -124,6 +183,7 @@ func Try5[T, R, S, Q any](callback func() (T, R, S, Q, error)) bool { } // Try6 has the same behavior than Try, but callback returns 6 variables. +// Play: https://go.dev/play/p/mTyyWUvn9u4 func Try6[T, R, S, Q, U any](callback func() (T, R, S, Q, U, error)) bool { return Try(func() error { _, _, _, _, _, err := callback() @@ -131,7 +191,125 @@ func Try6[T, R, S, Q, U any](callback func() (T, R, S, Q, U, error)) bool { }) } +// TryOr has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr[A any](callback func() (A, error), fallbackA A) (A, bool) { + return TryOr1(callback, fallbackA) +} + +// TryOr1 has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr1[A any](callback func() (A, error), fallbackA A) (A, bool) { + ok := false + + Try0(func() { + a, err := callback() + if err == nil { + fallbackA = a + ok = true + } + }) + + return fallbackA, ok +} + +// TryOr2 has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr2[A any, B any](callback func() (A, B, error), fallbackA A, fallbackB B) (A, B, bool) { + ok := false + + Try0(func() { + a, b, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + ok = true + } + }) + + return fallbackA, fallbackB, ok +} + +// TryOr3 has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr3[A any, B any, C any](callback func() (A, B, C, error), fallbackA A, fallbackB B, fallbackC C) (A, B, C, bool) { + ok := false + + Try0(func() { + a, b, c, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + fallbackC = c + ok = true + } + }) + + return fallbackA, fallbackB, fallbackC, ok +} + +// TryOr4 has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr4[A any, B any, C any, D any](callback func() (A, B, C, D, error), fallbackA A, fallbackB B, fallbackC C, fallbackD D) (A, B, C, D, bool) { + ok := false + + Try0(func() { + a, b, c, d, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + fallbackC = c + fallbackD = d + ok = true + } + }) + + return fallbackA, fallbackB, fallbackC, fallbackD, ok +} + +// TryOr5 has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr5[A any, B any, C any, D any, E any](callback func() (A, B, C, D, E, error), fallbackA A, fallbackB B, fallbackC C, fallbackD D, fallbackE E) (A, B, C, D, E, bool) { + ok := false + + Try0(func() { + a, b, c, d, e, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + fallbackC = c + fallbackD = d + fallbackE = e + ok = true + } + }) + + return fallbackA, fallbackB, fallbackC, fallbackD, fallbackE, ok +} + +// TryOr6 has the same behavior than Must, but returns a default value in case of error. +// Play: https://go.dev/play/p/B4F7Wg2Zh9X +func TryOr6[A any, B any, C any, D any, E any, F any](callback func() (A, B, C, D, E, F, error), fallbackA A, fallbackB B, fallbackC C, fallbackD D, fallbackE E, fallbackF F) (A, B, C, D, E, F, bool) { + ok := false + + Try0(func() { + a, b, c, d, e, f, err := callback() + if err == nil { + fallbackA = a + fallbackB = b + fallbackC = c + fallbackD = d + fallbackE = e + fallbackF = f + ok = true + } + }) + + return fallbackA, fallbackB, fallbackC, fallbackD, fallbackE, fallbackF, ok +} + // TryWithErrorValue has the same behavior than Try, but also returns value passed to panic. +// Play: https://go.dev/play/p/Kc7afQIT2Fs func TryWithErrorValue(callback func() error) (errorValue any, ok bool) { ok = true @@ -152,6 +330,7 @@ func TryWithErrorValue(callback func() error) (errorValue any, ok bool) { } // TryCatch has the same behavior than Try, but calls the catch function in case of error. +// Play: https://go.dev/play/p/PnOON-EqBiU func TryCatch(callback func() error, catch func()) { if !Try(callback) { catch() @@ -159,8 +338,17 @@ func TryCatch(callback func() error, catch func()) { } // TryCatchWithErrorValue has the same behavior than TryWithErrorValue, but calls the catch function in case of error. +// Play: https://go.dev/play/p/8Pc9gwX_GZO func TryCatchWithErrorValue(callback func() error, catch func(any)) { if err, ok := TryWithErrorValue(callback); !ok { catch(err) } } + +// ErrorsAs is a shortcut for errors.As(err, &&T). +// Play: https://go.dev/play/p/8wk5rH8UfrE +func ErrorsAs[T error](err error) (T, bool) { + var t T + ok := errors.As(err, &t) + return t, ok +} diff --git a/vendor/github.com/samber/lo/find.go b/vendor/github.com/samber/lo/find.go index 8f24e238..e40bfcc8 100644 --- a/vendor/github.com/samber/lo/find.go +++ b/vendor/github.com/samber/lo/find.go @@ -2,7 +2,6 @@ package lo import ( "fmt" - "math" "math/rand" "golang.org/x/exp/constraints" @@ -87,6 +86,140 @@ func FindOrElse[T any](collection []T, fallback T, predicate func(T) bool) T { return fallback } +// FindKey returns the key of the first value matching. +func FindKey[K comparable, V comparable](object map[K]V, value V) (K, bool) { + for k, v := range object { + if v == value { + return k, true + } + } + + return Empty[K](), false +} + +// FindKeyBy returns the key of the first element predicate returns truthy for. +func FindKeyBy[K comparable, V any](object map[K]V, predicate func(K, V) bool) (K, bool) { + for k, v := range object { + if predicate(k, v) { + return k, true + } + } + + return Empty[K](), false +} + +// FindUniques returns a slice with all the unique elements of the collection. +// The order of result values is determined by the order they occur in the collection. +func FindUniques[T comparable](collection []T) []T { + isDupl := make(map[T]bool, len(collection)) + + for _, item := range collection { + duplicated, ok := isDupl[item] + if !ok { + isDupl[item] = false + } else if !duplicated { + isDupl[item] = true + } + } + + result := make([]T, 0, len(collection)-len(isDupl)) + + for _, item := range collection { + if duplicated := isDupl[item]; !duplicated { + result = append(result, item) + } + } + + return result +} + +// FindUniquesBy returns a slice with all the unique elements of the collection. +// The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is +// invoked for each element in array to generate the criterion by which uniqueness is computed. +func FindUniquesBy[T any, U comparable](collection []T, iteratee func(T) U) []T { + isDupl := make(map[U]bool, len(collection)) + + for _, item := range collection { + key := iteratee(item) + + duplicated, ok := isDupl[key] + if !ok { + isDupl[key] = false + } else if !duplicated { + isDupl[key] = true + } + } + + result := make([]T, 0, len(collection)-len(isDupl)) + + for _, item := range collection { + key := iteratee(item) + + if duplicated := isDupl[key]; !duplicated { + result = append(result, item) + } + } + + return result +} + +// FindDuplicates returns a slice with the first occurence of each duplicated elements of the collection. +// The order of result values is determined by the order they occur in the collection. +func FindDuplicates[T comparable](collection []T) []T { + isDupl := make(map[T]bool, len(collection)) + + for _, item := range collection { + duplicated, ok := isDupl[item] + if !ok { + isDupl[item] = false + } else if !duplicated { + isDupl[item] = true + } + } + + result := make([]T, 0, len(collection)-len(isDupl)) + + for _, item := range collection { + if duplicated := isDupl[item]; duplicated { + result = append(result, item) + isDupl[item] = false + } + } + + return result +} + +// FindDuplicatesBy returns a slice with the first occurence of each duplicated elements of the collection. +// The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is +// invoked for each element in array to generate the criterion by which uniqueness is computed. +func FindDuplicatesBy[T any, U comparable](collection []T, iteratee func(T) U) []T { + isDupl := make(map[U]bool, len(collection)) + + for _, item := range collection { + key := iteratee(item) + + duplicated, ok := isDupl[key] + if !ok { + isDupl[key] = false + } else if !duplicated { + isDupl[key] = true + } + } + + result := make([]T, 0, len(collection)-len(isDupl)) + + for _, item := range collection { + key := iteratee(item) + + if duplicated := isDupl[key]; duplicated { + result = append(result, item) + isDupl[key] = false + } + } + + return result +} + // Min search the minimum value of a collection. func Min[T constraints.Ordered](collection []T) T { var min T @@ -187,19 +320,18 @@ func Last[T any](collection []T) (T, error) { // Nth returns the element at index `nth` of collection. If `nth` is negative, the nth element // from the end is returned. An error is returned when nth is out of slice bounds. -func Nth[T any](collection []T, nth int) (T, error) { - if int(math.Abs(float64(nth))) >= len(collection) { +func Nth[T any, N constraints.Integer](collection []T, nth N) (T, error) { + n := int(nth) + l := len(collection) + if n >= l || -n > l { var t T - return t, fmt.Errorf("nth: %d out of slice bounds", nth) + return t, fmt.Errorf("nth: %d out of slice bounds", n) } - length := len(collection) - - if nth >= 0 { - return collection[nth], nil + if n >= 0 { + return collection[n], nil } - - return collection[length+nth], nil + return collection[l+n], nil } // Sample returns a random item from collection. @@ -216,11 +348,7 @@ func Sample[T any](collection []T) T { func Samples[T any](collection []T, count int) []T { size := len(collection) - // put values into a map, for faster deletion - cOpy := make([]T, 0, size) - for _, v := range collection { - cOpy = append(cOpy, v) - } + cOpy := append([]T{}, collection...) results := []T{} diff --git a/vendor/github.com/samber/lo/func.go b/vendor/github.com/samber/lo/func.go new file mode 100644 index 00000000..90afc9ac --- /dev/null +++ b/vendor/github.com/samber/lo/func.go @@ -0,0 +1,8 @@ +package lo + +// Partial returns new function that, when called, has its first argument set to the provided value. +func Partial[T1, T2, R any](f func(T1, T2) R, arg1 T1) func(T2) R { + return func(t2 T2) R { + return f(arg1, t2) + } +} diff --git a/vendor/github.com/samber/lo/intersect.go b/vendor/github.com/samber/lo/intersect.go index eac16392..169cc2cf 100644 --- a/vendor/github.com/samber/lo/intersect.go +++ b/vendor/github.com/samber/lo/intersect.go @@ -175,3 +175,28 @@ func Union[T comparable](list1 []T, list2 []T) []T { return result } + +// Without returns slice excluding all given values. +func Without[T comparable](collection []T, exclude ...T) []T { + result := make([]T, 0, len(collection)) + for _, e := range collection { + if !Contains(exclude, e) { + result = append(result, e) + } + } + return result +} + +// WithoutEmpty returns slice excluding empty values. +func WithoutEmpty[T comparable](collection []T) []T { + var empty T + + result := make([]T, 0, len(collection)) + for _, e := range collection { + if e != empty { + result = append(result, e) + } + } + + return result +} diff --git a/vendor/github.com/samber/lo/map.go b/vendor/github.com/samber/lo/map.go index 294421c5..7915e37c 100644 --- a/vendor/github.com/samber/lo/map.go +++ b/vendor/github.com/samber/lo/map.go @@ -1,6 +1,7 @@ package lo // Keys creates an array of the map keys. +// Play: https://go.dev/play/p/Uu11fHASqrU func Keys[K comparable, V any](in map[K]V) []K { result := make([]K, 0, len(in)) @@ -12,6 +13,7 @@ func Keys[K comparable, V any](in map[K]V) []K { } // Values creates an array of the map values. +// Play: https://go.dev/play/p/nnRTQkzQfF6 func Values[K comparable, V any](in map[K]V) []V { result := make([]V, 0, len(in)) @@ -23,6 +25,7 @@ func Values[K comparable, V any](in map[K]V) []V { } // PickBy returns same map type filtered by given predicate. +// Play: https://go.dev/play/p/kdg8GR_QMmf func PickBy[K comparable, V any](in map[K]V, predicate func(K, V) bool) map[K]V { r := map[K]V{} for k, v := range in { @@ -34,6 +37,7 @@ func PickBy[K comparable, V any](in map[K]V, predicate func(K, V) bool) map[K]V } // PickByKeys returns same map type filtered by given keys. +// Play: https://go.dev/play/p/R1imbuci9qU func PickByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V { r := map[K]V{} for k, v := range in { @@ -45,6 +49,7 @@ func PickByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V { } // PickByValues returns same map type filtered by given values. +// Play: https://go.dev/play/p/1zdzSvbfsJc func PickByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V { r := map[K]V{} for k, v := range in { @@ -55,7 +60,8 @@ func PickByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V { return r } -// PickBy returns same map type filtered by given predicate. +// OmitBy returns same map type filtered by given predicate. +// Play: https://go.dev/play/p/EtBsR43bdsd func OmitBy[K comparable, V any](in map[K]V, predicate func(K, V) bool) map[K]V { r := map[K]V{} for k, v := range in { @@ -67,6 +73,7 @@ func OmitBy[K comparable, V any](in map[K]V, predicate func(K, V) bool) map[K]V } // OmitByKeys returns same map type filtered by given keys. +// Play: https://go.dev/play/p/t1QjCrs-ysk func OmitByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V { r := map[K]V{} for k, v := range in { @@ -78,6 +85,7 @@ func OmitByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V { } // OmitByValues returns same map type filtered by given values. +// Play: https://go.dev/play/p/9UYZi-hrs8j func OmitByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V { r := map[K]V{} for k, v := range in { @@ -89,6 +97,7 @@ func OmitByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V { } // Entries transforms a map into array of key/value pairs. +// Play: func Entries[K comparable, V any](in map[K]V) []Entry[K, V] { entries := make([]Entry[K, V], 0, len(in)) @@ -102,7 +111,15 @@ func Entries[K comparable, V any](in map[K]V) []Entry[K, V] { return entries } +// ToPairs transforms a map into array of key/value pairs. +// Alias of Entries(). +// Play: https://go.dev/play/p/3Dhgx46gawJ +func ToPairs[K comparable, V any](in map[K]V) []Entry[K, V] { + return Entries(in) +} + // FromEntries transforms an array of key/value pairs into a map. +// Play: https://go.dev/play/p/oIr5KHFGCEN func FromEntries[K comparable, V any](entries []Entry[K, V]) map[K]V { out := map[K]V{} @@ -113,9 +130,17 @@ func FromEntries[K comparable, V any](entries []Entry[K, V]) map[K]V { return out } +// FromPairs transforms an array of key/value pairs into a map. +// Alias of FromEntries(). +// Play: https://go.dev/play/p/oIr5KHFGCEN +func FromPairs[K comparable, V any](entries []Entry[K, V]) map[K]V { + return FromEntries(entries) +} + // Invert creates a map composed of the inverted keys and values. If map // contains duplicate values, subsequent values overwrite property assignments // of previous values. +// Play: https://go.dev/play/p/rFQ4rak6iA1 func Invert[K comparable, V comparable](in map[K]V) map[V]K { out := map[V]K{} @@ -127,6 +152,7 @@ func Invert[K comparable, V comparable](in map[K]V) map[V]K { } // Assign merges multiple maps from left to right. +// Play: https://go.dev/play/p/VhwfJOyxf5o func Assign[K comparable, V any](maps ...map[K]V) map[K]V { out := map[K]V{} @@ -140,6 +166,7 @@ func Assign[K comparable, V any](maps ...map[K]V) map[K]V { } // MapKeys manipulates a map keys and transforms it to a map of another type. +// Play: https://go.dev/play/p/9_4WPIqOetJ func MapKeys[K comparable, V any, R comparable](in map[K]V, iteratee func(V, K) R) map[R]V { result := map[R]V{} @@ -151,6 +178,7 @@ func MapKeys[K comparable, V any, R comparable](in map[K]V, iteratee func(V, K) } // MapValues manipulates a map values and transforms it to a map of another type. +// Play: https://go.dev/play/p/T_8xAfvcf0W func MapValues[K comparable, V any, R any](in map[K]V, iteratee func(V, K) R) map[K]R { result := map[K]R{} @@ -160,3 +188,15 @@ func MapValues[K comparable, V any, R any](in map[K]V, iteratee func(V, K) R) ma return result } + +// MapToSlice transforms a map into a slice based on specific iteratee +// Play: https://go.dev/play/p/ZuiCZpDt6LD +func MapToSlice[K comparable, V any, R any](in map[K]V, iteratee func(K, V) R) []R { + result := make([]R, 0, len(in)) + + for k, v := range in { + result = append(result, iteratee(k, v)) + } + + return result +} diff --git a/vendor/github.com/samber/lo/math.go b/vendor/github.com/samber/lo/math.go index 1ce1ebe8..30e7e9ef 100644 --- a/vendor/github.com/samber/lo/math.go +++ b/vendor/github.com/samber/lo/math.go @@ -3,6 +3,7 @@ package lo import "golang.org/x/exp/constraints" // Range creates an array of numbers (positive and/or negative) with given length. +// Play: https://go.dev/play/p/0r6VimXAi9H func Range(elementNum int) []int { length := If(elementNum < 0, -elementNum).Else(elementNum) result := make([]int, length) @@ -14,6 +15,7 @@ func Range(elementNum int) []int { } // RangeFrom creates an array of numbers from start with specified length. +// Play: https://go.dev/play/p/0r6VimXAi9H func RangeFrom[T constraints.Integer | constraints.Float](start T, elementNum int) []T { length := If(elementNum < 0, -elementNum).Else(elementNum) result := make([]T, length) @@ -26,6 +28,7 @@ func RangeFrom[T constraints.Integer | constraints.Float](start T, elementNum in // RangeWithSteps creates an array of numbers (positive and/or negative) progressing from start up to, but not including end. // step set to zero will return empty array. +// Play: https://go.dev/play/p/0r6VimXAi9H func RangeWithSteps[T constraints.Integer | constraints.Float](start, end, step T) []T { result := []T{} if start == end || step == 0 { @@ -50,6 +53,7 @@ func RangeWithSteps[T constraints.Integer | constraints.Float](start, end, step } // Clamp clamps number within the inclusive lower and upper bounds. +// Play: https://go.dev/play/p/RU4lJNC2hlI func Clamp[T constraints.Ordered](value T, min T, max T) T { if value < min { return min @@ -59,7 +63,8 @@ func Clamp[T constraints.Ordered](value T, min T, max T) T { return value } -// Summarizes the values in a collection. +// SumBy summarizes the values in a collection using the given return value from the iteration function. If collection is empty 0 is returned. +// Play: https://go.dev/play/p/Dz_a_7jN_ca func SumBy[T any, R constraints.Float | constraints.Integer](collection []T, iteratee func(T) R) R { var sum R = 0 for _, item := range collection { diff --git a/vendor/github.com/samber/lo/pointers.go b/vendor/github.com/samber/lo/pointers.go deleted file mode 100644 index 623d547d..00000000 --- a/vendor/github.com/samber/lo/pointers.go +++ /dev/null @@ -1,32 +0,0 @@ -package lo - -// ToPtr returns a pointer copy of value. -func ToPtr[T any](x T) *T { - return &x -} - -// ToSlicePtr returns a slice of pointer copy of value. -func ToSlicePtr[T any](collection []T) []*T { - return Map(collection, func(x T, _ int) *T { - return &x - }) -} - -// Empty returns an empty value. -func Empty[T any]() T { - var t T - return t -} - -// Coalesce returns the first non-empty arguments. Arguments must be comparable. -func Coalesce[T comparable](v ...T) (result T, ok bool) { - for _, e := range v { - if e != result { - result = e - ok = true - return - } - } - - return -} diff --git a/vendor/github.com/samber/lo/retry.go b/vendor/github.com/samber/lo/retry.go index 7d86795d..b4a61efb 100644 --- a/vendor/github.com/samber/lo/retry.go +++ b/vendor/github.com/samber/lo/retry.go @@ -46,6 +46,7 @@ func (d *debounce) cancel() { } // NewDebounce creates a debounced instance that delays invoking functions given until after wait milliseconds have elapsed. +// Play: https://go.dev/play/p/mz32VMK2nqe func NewDebounce(duration time.Duration, f ...func()) (func(), func()) { d := &debounce{ after: duration, @@ -60,7 +61,8 @@ func NewDebounce(duration time.Duration, f ...func()) (func(), func()) { }, d.cancel } -// Attempt invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a sucessfull response is returned. +// Attempt invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a successful response is returned. +// Play: https://go.dev/play/p/3ggJZ2ZKcMj func Attempt(maxIteration int, f func(int) error) (int, error) { var err error @@ -76,9 +78,10 @@ func Attempt(maxIteration int, f func(int) error) (int, error) { } // AttemptWithDelay invokes a function N times until it returns valid output, -// with a pause betwwen each call. Returning either the caught error or nil. -// When first argument is less than `1`, the function runs until a sucessfull +// with a pause between each call. Returning either the caught error or nil. +// When first argument is less than `1`, the function runs until a successful // response is returned. +// Play: https://go.dev/play/p/tVs6CygC7m1 func AttemptWithDelay(maxIteration int, delay time.Duration, f func(int, time.Duration) error) (int, time.Duration, error) { var err error diff --git a/vendor/github.com/samber/lo/slice.go b/vendor/github.com/samber/lo/slice.go index 04d048be..516fc8b1 100644 --- a/vendor/github.com/samber/lo/slice.go +++ b/vendor/github.com/samber/lo/slice.go @@ -2,9 +2,12 @@ package lo import ( "math/rand" + + "golang.org/x/exp/constraints" ) // Filter iterates over elements of collection, returning an array of all elements predicate returns truthy for. +// Play: https://go.dev/play/p/Apjg3WeSi7K func Filter[V any](collection []V, predicate func(V, int) bool) []V { result := []V{} @@ -18,6 +21,7 @@ func Filter[V any](collection []V, predicate func(V, int) bool) []V { } // Map manipulates a slice and transforms it to a slice of another type. +// Play: https://go.dev/play/p/OkPcYAhBo0D func Map[T any, R any](collection []T, iteratee func(T, int) R) []R { result := make([]R, len(collection)) @@ -32,6 +36,8 @@ func Map[T any, R any](collection []T, iteratee func(T, int) R) []R { // The callback function should return two values: // - the result of the mapping operation and // - whether the result element should be included or not. +// +// Play: https://go.dev/play/p/-AuYXfy7opz func FilterMap[T any, R any](collection []T, callback func(T, int) (R, bool)) []R { result := []R{} @@ -45,6 +51,7 @@ func FilterMap[T any, R any](collection []T, callback func(T, int) (R, bool)) [] } // FlatMap manipulates a slice and transforms and flattens it to a slice of another type. +// Play: https://go.dev/play/p/YSoYmQTA8-U func FlatMap[T any, R any](collection []T, iteratee func(T, int) []R) []R { result := []R{} @@ -57,6 +64,7 @@ func FlatMap[T any, R any](collection []T, iteratee func(T, int) []R) []R { // Reduce reduces collection to a value which is the accumulated result of running each element in collection // through accumulator, where each successive invocation is supplied the return value of the previous. +// Play: https://go.dev/play/p/R4UHXZNaaUG func Reduce[T any, R any](collection []T, accumulator func(R, T, int) R, initial R) R { for i, item := range collection { initial = accumulator(initial, item, i) @@ -65,7 +73,18 @@ func Reduce[T any, R any](collection []T, accumulator func(R, T, int) R, initial return initial } +// ReduceRight helper is like Reduce except that it iterates over elements of collection from right to left. +// Play: https://go.dev/play/p/Fq3W70l7wXF +func ReduceRight[T any, R any](collection []T, accumulator func(R, T, int) R, initial R) R { + for i := len(collection) - 1; i >= 0; i-- { + initial = accumulator(initial, collection[i], i) + } + + return initial +} + // ForEach iterates over elements of collection and invokes iteratee for each element. +// Play: https://go.dev/play/p/oofyiUPRf8t func ForEach[T any](collection []T, iteratee func(T, int)) { for i, item := range collection { iteratee(item, i) @@ -74,6 +93,7 @@ func ForEach[T any](collection []T, iteratee func(T, int)) { // Times invokes the iteratee n times, returning an array of the results of each invocation. // The iteratee is invoked with index as argument. +// Play: https://go.dev/play/p/vgQj3Glr6lT func Times[T any](count int, iteratee func(int) T) []T { result := make([]T, count) @@ -86,6 +106,7 @@ func Times[T any](count int, iteratee func(int) T) []T { // Uniq returns a duplicate-free version of an array, in which only the first occurrence of each element is kept. // The order of result values is determined by the order they occur in the array. +// Play: https://go.dev/play/p/DTzbeXZ6iEN func Uniq[T comparable](collection []T) []T { result := make([]T, 0, len(collection)) seen := make(map[T]struct{}, len(collection)) @@ -105,6 +126,7 @@ func Uniq[T comparable](collection []T) []T { // UniqBy returns a duplicate-free version of an array, in which only the first occurrence of each element is kept. // The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is // invoked for each element in array to generate the criterion by which uniqueness is computed. +// Play: https://go.dev/play/p/g42Z3QSb53u func UniqBy[T any, U comparable](collection []T, iteratee func(T) U) []T { result := make([]T, 0, len(collection)) seen := make(map[U]struct{}, len(collection)) @@ -124,6 +146,7 @@ func UniqBy[T any, U comparable](collection []T, iteratee func(T) U) []T { } // GroupBy returns an object composed of keys generated from the results of running each element of collection through iteratee. +// Play: https://go.dev/play/p/XnQBd_v6brd func GroupBy[T any, U comparable](collection []T, iteratee func(T) U) map[U][]T { result := map[U][]T{} @@ -138,22 +161,25 @@ func GroupBy[T any, U comparable](collection []T, iteratee func(T) U) map[U][]T // Chunk returns an array of elements split into groups the length of size. If array can't be split evenly, // the final chunk will be the remaining elements. +// Play: https://go.dev/play/p/EeKl0AuTehH func Chunk[T any](collection []T, size int) [][]T { if size <= 0 { panic("Second parameter must be greater than 0") } - result := make([][]T, 0, len(collection)/2+1) - length := len(collection) + chunksNum := len(collection) / size + if len(collection)%size != 0 { + chunksNum += 1 + } - for i := 0; i < length; i++ { - chunk := i / size + result := make([][]T, 0, chunksNum) - if i%size == 0 { - result = append(result, make([]T, 0, size)) + for i := 0; i < chunksNum; i++ { + last := (i + 1) * size + if last > len(collection) { + last = len(collection) } - - result[chunk] = append(result[chunk], collection[i]) + result = append(result, collection[i*size:last]) } return result @@ -162,6 +188,7 @@ func Chunk[T any](collection []T, size int) [][]T { // PartitionBy returns an array of elements split into groups. The order of grouped values is // determined by the order they occur in collection. The grouping is generated from the results // of running each element of collection through iteratee. +// Play: https://go.dev/play/p/NfQ_nGjkgXW func PartitionBy[T any, K comparable](collection []T, iteratee func(x T) K) [][]T { result := [][]T{} seen := map[K]int{} @@ -187,17 +214,23 @@ func PartitionBy[T any, K comparable](collection []T, iteratee func(x T) K) [][] } // Flatten returns an array a single level deep. +// Play: https://go.dev/play/p/rbp9ORaMpjw func Flatten[T any](collection [][]T) []T { - result := []T{} + totalLen := 0 + for i := range collection { + totalLen += len(collection[i]) + } - for _, item := range collection { - result = append(result, item...) + result := make([]T, 0, totalLen) + for i := range collection { + result = append(result, collection[i]...) } return result } // Shuffle returns an array of shuffled values. Uses the Fisher-Yates shuffle algorithm. +// Play: https://go.dev/play/p/Qp73bnTDnc7 func Shuffle[T any](collection []T) []T { rand.Shuffle(len(collection), func(i, j int) { collection[i], collection[j] = collection[j], collection[i] @@ -207,6 +240,7 @@ func Shuffle[T any](collection []T) []T { } // Reverse reverses array so that the first element becomes the last, the second element becomes the second to last, and so on. +// Play: https://go.dev/play/p/fhUMLvZ7vS6 func Reverse[T any](collection []T) []T { length := len(collection) half := length / 2 @@ -220,6 +254,7 @@ func Reverse[T any](collection []T) []T { } // Fill fills elements of array with `initial` value. +// Play: https://go.dev/play/p/VwR34GzqEub func Fill[T Clonable[T]](collection []T, initial T) []T { result := make([]T, 0, len(collection)) @@ -231,6 +266,7 @@ func Fill[T Clonable[T]](collection []T, initial T) []T { } // Repeat builds a slice with N copies of initial value. +// Play: https://go.dev/play/p/g3uHXbmc3b6 func Repeat[T Clonable[T]](count int, initial T) []T { result := make([]T, 0, count) @@ -242,6 +278,7 @@ func Repeat[T Clonable[T]](count int, initial T) []T { } // RepeatBy builds a slice with values returned by N calls of callback. +// Play: https://go.dev/play/p/ozZLCtX_hNU func RepeatBy[T any](count int, predicate func(int) T) []T { result := make([]T, 0, count) @@ -253,6 +290,7 @@ func RepeatBy[T any](count int, predicate func(int) T) []T { } // KeyBy transforms a slice or an array of structs to a map based on a pivot callback. +// Play: https://go.dev/play/p/mdaClUAT-zZ func KeyBy[K comparable, V any](collection []V, iteratee func(V) K) map[K]V { result := make(map[K]V, len(collection)) @@ -264,21 +302,55 @@ func KeyBy[K comparable, V any](collection []V, iteratee func(V) K) map[K]V { return result } -// Drop drops n elements from the beginning of a slice or array. -func Drop[T any](collection []T, n int) []T { - if len(collection) <= n { - return make([]T, 0) - } +// Associate returns a map containing key-value pairs provided by transform function applied to elements of the given slice. +// If any of two pairs would have the same key the last one gets added to the map. +// The order of keys in returned map is not specified and is not guaranteed to be the same from the original array. +// Play: https://go.dev/play/p/WHa2CfMO3Lr +func Associate[T any, K comparable, V any](collection []T, transform func(T) (K, V)) map[K]V { + result := make(map[K]V) - result := make([]T, len(collection)-n) - for i := n; i < len(collection); i++ { - result[i-n] = collection[i] + for _, t := range collection { + k, v := transform(t) + result[k] = v } return result } +// SliceToMap returns a map containing key-value pairs provided by transform function applied to elements of the given slice. +// If any of two pairs would have the same key the last one gets added to the map. +// The order of keys in returned map is not specified and is not guaranteed to be the same from the original array. +// Alias of Associate(). +// Play: https://go.dev/play/p/WHa2CfMO3Lr +func SliceToMap[T any, K comparable, V any](collection []T, transform func(T) (K, V)) map[K]V { + return Associate(collection, transform) +} + +// Drop drops n elements from the beginning of a slice or array. +// Play: https://go.dev/play/p/JswS7vXRJP2 +func Drop[T any](collection []T, n int) []T { + if len(collection) <= n { + return make([]T, 0) + } + + result := make([]T, 0, len(collection)-n) + + return append(result, collection[n:]...) +} + +// DropRight drops n elements from the end of a slice or array. +// Play: https://go.dev/play/p/GG0nXkSJJa3 +func DropRight[T any](collection []T, n int) []T { + if len(collection) <= n { + return []T{} + } + + result := make([]T, 0, len(collection)-n) + return append(result, collection[:len(collection)-n]...) +} + // DropWhile drops elements from the beginning of a slice or array while the predicate returns true. +// Play: https://go.dev/play/p/7gBPYw2IK16 func DropWhile[T any](collection []T, predicate func(T) bool) []T { i := 0 for ; i < len(collection); i++ { @@ -287,30 +359,12 @@ func DropWhile[T any](collection []T, predicate func(T) bool) []T { } } - result := make([]T, len(collection)-i) - - for j := 0; i < len(collection); i, j = i+1, j+1 { - result[j] = collection[i] - } - - return result -} - -// DropRight drops n elements from the end of a slice or array. -func DropRight[T any](collection []T, n int) []T { - if len(collection) <= n { - return make([]T, 0) - } - - result := make([]T, len(collection)-n) - for i := len(collection) - 1 - n; i >= 0; i-- { - result[i] = collection[i] - } - - return result + result := make([]T, 0, len(collection)-i) + return append(result, collection[i:]...) } // DropRightWhile drops elements from the end of a slice or array while the predicate returns true. +// Play: https://go.dev/play/p/3-n71oEC0Hz func DropRightWhile[T any](collection []T, predicate func(T) bool) []T { i := len(collection) - 1 for ; i >= 0; i-- { @@ -319,16 +373,12 @@ func DropRightWhile[T any](collection []T, predicate func(T) bool) []T { } } - result := make([]T, i+1) - - for ; i >= 0; i-- { - result[i] = collection[i] - } - - return result + result := make([]T, 0, i+1) + return append(result, collection[:i+1]...) } // Reject is the opposite of Filter, this method returns the elements of collection that predicate does not return truthy for. +// Play: https://go.dev/play/p/YkLMODy1WEL func Reject[V any](collection []V, predicate func(V, int) bool) []V { result := []V{} @@ -342,6 +392,7 @@ func Reject[V any](collection []V, predicate func(V, int) bool) []V { } // Count counts the number of elements in the collection that compare equal to value. +// Play: https://go.dev/play/p/Y3FlK54yveC func Count[T comparable](collection []T, value T) (count int) { for _, item := range collection { if item == value { @@ -353,6 +404,7 @@ func Count[T comparable](collection []T, value T) (count int) { } // CountBy counts the number of elements in the collection for which predicate is true. +// Play: https://go.dev/play/p/ByQbNYQQi4X func CountBy[T any](collection []T, predicate func(T) bool) (count int) { for _, item := range collection { if predicate(item) { @@ -363,7 +415,8 @@ func CountBy[T any](collection []T, predicate func(T) bool) (count int) { return count } -// Subset return part of a slice. +// Subset returns a copy of a slice from `offset` up to `length` elements. Like `slice[start:start+length]`, but does not panic on overflow. +// Play: https://go.dev/play/p/tOQu1GhFcog func Subset[T any](collection []T, offset int, length uint) []T { size := len(collection) @@ -385,17 +438,36 @@ func Subset[T any](collection []T, offset int, length uint) []T { return collection[offset : offset+int(length)] } -// Replace returns a copy of the slice with the first n non-overlapping instances of old replaced by new. -func Replace[T comparable](collection []T, old T, new T, n int) []T { +// Slice returns a copy of a slice from `start` up to, but not including `end`. Like `slice[start:end]`, but does not panic on overflow. +// Play: https://go.dev/play/p/8XWYhfMMA1h +func Slice[T any](collection []T, start int, end int) []T { size := len(collection) - result := make([]T, 0, size) - for _, item := range collection { - if item == old && n != 0 { - result = append(result, new) + if start >= end { + return []T{} + } + + if start > size { + start = size + } + + if end > size { + end = size + } + + return collection[start:end] +} + +// Replace returns a copy of the slice with the first n non-overlapping instances of old replaced by new. +// Play: https://go.dev/play/p/XfPzmf9gql6 +func Replace[T comparable](collection []T, old T, new T, n int) []T { + result := make([]T, len(collection)) + copy(result, collection) + + for i := range result { + if result[i] == old && n != 0 { + result[i] = new n-- - } else { - result = append(result, item) } } @@ -403,6 +475,49 @@ func Replace[T comparable](collection []T, old T, new T, n int) []T { } // ReplaceAll returns a copy of the slice with all non-overlapping instances of old replaced by new. +// Play: https://go.dev/play/p/a9xZFUHfYcV func ReplaceAll[T comparable](collection []T, old T, new T) []T { - return Replace[T](collection, old, new, -1) + return Replace(collection, old, new, -1) +} + +// Compact returns a slice of all non-zero elements. +// Play: https://go.dev/play/p/tXiy-iK6PAc +func Compact[T comparable](collection []T) []T { + var zero T + + result := []T{} + + for _, item := range collection { + if item != zero { + result = append(result, item) + } + } + + return result +} + +// IsSorted checks if a slice is sorted. +// Play: https://go.dev/play/p/mc3qR-t4mcx +func IsSorted[T constraints.Ordered](collection []T) bool { + for i := 1; i < len(collection); i++ { + if collection[i-1] > collection[i] { + return false + } + } + + return true +} + +// IsSortedByKey checks if a slice is sorted by iteratee. +// Play: https://go.dev/play/p/wiG6XyBBu49 +func IsSortedByKey[T any, K constraints.Ordered](collection []T, iteratee func(T) K) bool { + size := len(collection) + + for i := 0; i < size-1; i++ { + if iteratee(collection[i]) > iteratee(collection[i+1]) { + return false + } + } + + return true } diff --git a/vendor/github.com/samber/lo/string.go b/vendor/github.com/samber/lo/string.go index f60f764a..a63167cb 100644 --- a/vendor/github.com/samber/lo/string.go +++ b/vendor/github.com/samber/lo/string.go @@ -1,8 +1,11 @@ package lo -import "unicode/utf8" +import ( + "unicode/utf8" +) // Substring return part of a string. +// Play: https://go.dev/play/p/TQlxQi82Lu1 func Substring[T ~string](str T, offset int, length uint) T { size := len(str) @@ -24,7 +27,39 @@ func Substring[T ~string](str T, offset int, length uint) T { return str[offset : offset+int(length)] } +// ChunkString returns an array of strings split into groups the length of size. If array can't be split evenly, +// the final chunk will be the remaining elements. +// Play: https://go.dev/play/p/__FLTuJVz54 +func ChunkString[T ~string](str T, size int) []T { + if size <= 0 { + panic("lo.ChunkString: Size parameter must be greater than 0") + } + + if len(str) == 0 { + return []T{""} + } + + if size >= len(str) { + return []T{str} + } + + var chunks []T = make([]T, 0, ((len(str)-1)/size)+1) + currentLen := 0 + currentStart := 0 + for i := range str { + if currentLen == size { + chunks = append(chunks, str[currentStart:i]) + currentLen = 0 + currentStart = i + } + currentLen++ + } + chunks = append(chunks, str[currentStart:]) + return chunks +} + // RuneLength is an alias to utf8.RuneCountInString which returns the number of runes in string. +// Play: https://go.dev/play/p/tuhgW_lWY8l func RuneLength(str string) int { return utf8.RuneCountInString(str) } diff --git a/vendor/github.com/samber/lo/test.go b/vendor/github.com/samber/lo/test.go new file mode 100644 index 00000000..26db4c25 --- /dev/null +++ b/vendor/github.com/samber/lo/test.go @@ -0,0 +1,32 @@ +package lo + +import ( + "os" + "testing" + "time" +) + +// https://github.com/stretchr/testify/issues/1101 +func testWithTimeout(t *testing.T, timeout time.Duration) { + t.Helper() + + testFinished := make(chan struct{}) + t.Cleanup(func() { close(testFinished) }) + + go func() { + select { + case <-testFinished: + case <-time.After(timeout): + t.Errorf("test timed out after %s", timeout) + os.Exit(1) + } + }() +} + +type foo struct { + bar string +} + +func (f foo) Clone() foo { + return foo{f.bar} +} diff --git a/vendor/github.com/samber/lo/tuples.go b/vendor/github.com/samber/lo/tuples.go index d5f7a7bc..f8ea2f14 100644 --- a/vendor/github.com/samber/lo/tuples.go +++ b/vendor/github.com/samber/lo/tuples.go @@ -1,81 +1,97 @@ package lo // T2 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T2[A any, B any](a A, b B) Tuple2[A, B] { return Tuple2[A, B]{A: a, B: b} } // T3 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T3[A any, B any, C any](a A, b B, c C) Tuple3[A, B, C] { return Tuple3[A, B, C]{A: a, B: b, C: c} } // T4 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T4[A any, B any, C any, D any](a A, b B, c C, d D) Tuple4[A, B, C, D] { return Tuple4[A, B, C, D]{A: a, B: b, C: c, D: d} } // T5 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T5[A any, B any, C any, D any, E any](a A, b B, c C, d D, e E) Tuple5[A, B, C, D, E] { return Tuple5[A, B, C, D, E]{A: a, B: b, C: c, D: d, E: e} } // T6 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T6[A any, B any, C any, D any, E any, F any](a A, b B, c C, d D, e E, f F) Tuple6[A, B, C, D, E, F] { return Tuple6[A, B, C, D, E, F]{A: a, B: b, C: c, D: d, E: e, F: f} } // T7 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T7[A any, B any, C any, D any, E any, F any, G any](a A, b B, c C, d D, e E, f F, g G) Tuple7[A, B, C, D, E, F, G] { return Tuple7[A, B, C, D, E, F, G]{A: a, B: b, C: c, D: d, E: e, F: f, G: g} } // T8 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T8[A any, B any, C any, D any, E any, F any, G any, H any](a A, b B, c C, d D, e E, f F, g G, h H) Tuple8[A, B, C, D, E, F, G, H] { return Tuple8[A, B, C, D, E, F, G, H]{A: a, B: b, C: c, D: d, E: e, F: f, G: g, H: h} } // T8 creates a tuple from a list of values. +// Play: https://go.dev/play/p/IllL3ZO4BQm func T9[A any, B any, C any, D any, E any, F any, G any, H any, I any](a A, b B, c C, d D, e E, f F, g G, h H, i I) Tuple9[A, B, C, D, E, F, G, H, I] { return Tuple9[A, B, C, D, E, F, G, H, I]{A: a, B: b, C: c, D: d, E: e, F: f, G: g, H: h, I: i} } // Unpack2 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack2[A any, B any](tuple Tuple2[A, B]) (A, B) { return tuple.A, tuple.B } // Unpack3 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack3[A any, B any, C any](tuple Tuple3[A, B, C]) (A, B, C) { return tuple.A, tuple.B, tuple.C } // Unpack4 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack4[A any, B any, C any, D any](tuple Tuple4[A, B, C, D]) (A, B, C, D) { return tuple.A, tuple.B, tuple.C, tuple.D } // Unpack5 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack5[A any, B any, C any, D any, E any](tuple Tuple5[A, B, C, D, E]) (A, B, C, D, E) { return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E } // Unpack6 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack6[A any, B any, C any, D any, E any, F any](tuple Tuple6[A, B, C, D, E, F]) (A, B, C, D, E, F) { return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F } // Unpack7 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack7[A any, B any, C any, D any, E any, F any, G any](tuple Tuple7[A, B, C, D, E, F, G]) (A, B, C, D, E, F, G) { return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F, tuple.G } // Unpack8 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack8[A any, B any, C any, D any, E any, F any, G any, H any](tuple Tuple8[A, B, C, D, E, F, G, H]) (A, B, C, D, E, F, G, H) { return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F, tuple.G, tuple.H } // Unpack9 returns values contained in tuple. +// Play: https://go.dev/play/p/xVP_k0kJ96W func Unpack9[A any, B any, C any, D any, E any, F any, G any, H any, I any](tuple Tuple9[A, B, C, D, E, F, G, H, I]) (A, B, C, D, E, F, G, H, I) { return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F, tuple.G, tuple.H, tuple.I } @@ -83,14 +99,15 @@ func Unpack9[A any, B any, C any, D any, E any, F any, G any, H any, I any](tupl // Zip2 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip2[A any, B any](a []A, b []B) []Tuple2[A, B] { - size := Max[int]([]int{len(a), len(b)}) + size := Max([]int{len(a), len(b)}) result := make([]Tuple2[A, B], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) result = append(result, Tuple2[A, B]{ A: _a, @@ -104,15 +121,16 @@ func Zip2[A any, B any](a []A, b []B) []Tuple2[A, B] { // Zip3 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip3[A any, B any, C any](a []A, b []B, c []C) []Tuple3[A, B, C] { - size := Max[int]([]int{len(a), len(b), len(c)}) + size := Max([]int{len(a), len(b), len(c)}) result := make([]Tuple3[A, B, C], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) result = append(result, Tuple3[A, B, C]{ A: _a, @@ -127,16 +145,17 @@ func Zip3[A any, B any, C any](a []A, b []B, c []C) []Tuple3[A, B, C] { // Zip4 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip4[A any, B any, C any, D any](a []A, b []B, c []C, d []D) []Tuple4[A, B, C, D] { - size := Max[int]([]int{len(a), len(b), len(c), len(d)}) + size := Max([]int{len(a), len(b), len(c), len(d)}) result := make([]Tuple4[A, B, C, D], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) - _d, _ := Nth[D](d, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) result = append(result, Tuple4[A, B, C, D]{ A: _a, @@ -152,17 +171,18 @@ func Zip4[A any, B any, C any, D any](a []A, b []B, c []C, d []D) []Tuple4[A, B, // Zip5 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip5[A any, B any, C any, D any, E any](a []A, b []B, c []C, d []D, e []E) []Tuple5[A, B, C, D, E] { - size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e)}) + size := Max([]int{len(a), len(b), len(c), len(d), len(e)}) result := make([]Tuple5[A, B, C, D, E], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) - _d, _ := Nth[D](d, index) - _e, _ := Nth[E](e, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) result = append(result, Tuple5[A, B, C, D, E]{ A: _a, @@ -179,18 +199,19 @@ func Zip5[A any, B any, C any, D any, E any](a []A, b []B, c []C, d []D, e []E) // Zip6 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip6[A any, B any, C any, D any, E any, F any](a []A, b []B, c []C, d []D, e []E, f []F) []Tuple6[A, B, C, D, E, F] { - size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e), len(f)}) + size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f)}) result := make([]Tuple6[A, B, C, D, E, F], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) - _d, _ := Nth[D](d, index) - _e, _ := Nth[E](e, index) - _f, _ := Nth[F](f, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) + _f, _ := Nth(f, index) result = append(result, Tuple6[A, B, C, D, E, F]{ A: _a, @@ -208,19 +229,20 @@ func Zip6[A any, B any, C any, D any, E any, F any](a []A, b []B, c []C, d []D, // Zip7 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip7[A any, B any, C any, D any, E any, F any, G any](a []A, b []B, c []C, d []D, e []E, f []F, g []G) []Tuple7[A, B, C, D, E, F, G] { - size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g)}) + size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g)}) result := make([]Tuple7[A, B, C, D, E, F, G], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) - _d, _ := Nth[D](d, index) - _e, _ := Nth[E](e, index) - _f, _ := Nth[F](f, index) - _g, _ := Nth[G](g, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) + _f, _ := Nth(f, index) + _g, _ := Nth(g, index) result = append(result, Tuple7[A, B, C, D, E, F, G]{ A: _a, @@ -239,20 +261,21 @@ func Zip7[A any, B any, C any, D any, E any, F any, G any](a []A, b []B, c []C, // Zip8 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip8[A any, B any, C any, D any, E any, F any, G any, H any](a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H) []Tuple8[A, B, C, D, E, F, G, H] { - size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h)}) + size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h)}) result := make([]Tuple8[A, B, C, D, E, F, G, H], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) - _d, _ := Nth[D](d, index) - _e, _ := Nth[E](e, index) - _f, _ := Nth[F](f, index) - _g, _ := Nth[G](g, index) - _h, _ := Nth[H](h, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) + _f, _ := Nth(f, index) + _g, _ := Nth(g, index) + _h, _ := Nth(h, index) result = append(result, Tuple8[A, B, C, D, E, F, G, H]{ A: _a, @@ -272,21 +295,22 @@ func Zip8[A any, B any, C any, D any, E any, F any, G any, H any](a []A, b []B, // Zip9 creates a slice of grouped elements, the first of which contains the first elements // of the given arrays, the second of which contains the second elements of the given arrays, and so on. // When collections have different size, the Tuple attributes are filled with zero value. +// Play: https://go.dev/play/p/jujaA6GaJTp func Zip9[A any, B any, C any, D any, E any, F any, G any, H any, I any](a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I) []Tuple9[A, B, C, D, E, F, G, H, I] { - size := Max[int]([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h), len(i)}) + size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h), len(i)}) result := make([]Tuple9[A, B, C, D, E, F, G, H, I], 0, size) for index := 0; index < size; index++ { - _a, _ := Nth[A](a, index) - _b, _ := Nth[B](b, index) - _c, _ := Nth[C](c, index) - _d, _ := Nth[D](d, index) - _e, _ := Nth[E](e, index) - _f, _ := Nth[F](f, index) - _g, _ := Nth[G](g, index) - _h, _ := Nth[H](h, index) - _i, _ := Nth[I](i, index) + _a, _ := Nth(a, index) + _b, _ := Nth(b, index) + _c, _ := Nth(c, index) + _d, _ := Nth(d, index) + _e, _ := Nth(e, index) + _f, _ := Nth(f, index) + _g, _ := Nth(g, index) + _h, _ := Nth(h, index) + _i, _ := Nth(i, index) result = append(result, Tuple9[A, B, C, D, E, F, G, H, I]{ A: _a, @@ -306,6 +330,7 @@ func Zip9[A any, B any, C any, D any, E any, F any, G any, H any, I any](a []A, // Unzip2 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip2[A any, B any](tuples []Tuple2[A, B]) ([]A, []B) { size := len(tuples) r1 := make([]A, 0, size) @@ -321,6 +346,7 @@ func Unzip2[A any, B any](tuples []Tuple2[A, B]) ([]A, []B) { // Unzip3 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip3[A any, B any, C any](tuples []Tuple3[A, B, C]) ([]A, []B, []C) { size := len(tuples) r1 := make([]A, 0, size) @@ -338,6 +364,7 @@ func Unzip3[A any, B any, C any](tuples []Tuple3[A, B, C]) ([]A, []B, []C) { // Unzip4 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip4[A any, B any, C any, D any](tuples []Tuple4[A, B, C, D]) ([]A, []B, []C, []D) { size := len(tuples) r1 := make([]A, 0, size) @@ -357,6 +384,7 @@ func Unzip4[A any, B any, C any, D any](tuples []Tuple4[A, B, C, D]) ([]A, []B, // Unzip5 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip5[A any, B any, C any, D any, E any](tuples []Tuple5[A, B, C, D, E]) ([]A, []B, []C, []D, []E) { size := len(tuples) r1 := make([]A, 0, size) @@ -378,6 +406,7 @@ func Unzip5[A any, B any, C any, D any, E any](tuples []Tuple5[A, B, C, D, E]) ( // Unzip6 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip6[A any, B any, C any, D any, E any, F any](tuples []Tuple6[A, B, C, D, E, F]) ([]A, []B, []C, []D, []E, []F) { size := len(tuples) r1 := make([]A, 0, size) @@ -401,6 +430,7 @@ func Unzip6[A any, B any, C any, D any, E any, F any](tuples []Tuple6[A, B, C, D // Unzip7 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip7[A any, B any, C any, D any, E any, F any, G any](tuples []Tuple7[A, B, C, D, E, F, G]) ([]A, []B, []C, []D, []E, []F, []G) { size := len(tuples) r1 := make([]A, 0, size) @@ -426,6 +456,7 @@ func Unzip7[A any, B any, C any, D any, E any, F any, G any](tuples []Tuple7[A, // Unzip8 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip8[A any, B any, C any, D any, E any, F any, G any, H any](tuples []Tuple8[A, B, C, D, E, F, G, H]) ([]A, []B, []C, []D, []E, []F, []G, []H) { size := len(tuples) r1 := make([]A, 0, size) @@ -453,6 +484,7 @@ func Unzip8[A any, B any, C any, D any, E any, F any, G any, H any](tuples []Tup // Unzip9 accepts an array of grouped elements and creates an array regrouping the elements // to their pre-zip configuration. +// Play: https://go.dev/play/p/ciHugugvaAW func Unzip9[A any, B any, C any, D any, E any, F any, G any, H any, I any](tuples []Tuple9[A, B, C, D, E, F, G, H, I]) ([]A, []B, []C, []D, []E, []F, []G, []H, []I) { size := len(tuples) r1 := make([]A, 0, size) diff --git a/vendor/github.com/samber/lo/type_manipulation.go b/vendor/github.com/samber/lo/type_manipulation.go new file mode 100644 index 00000000..fe99ee1f --- /dev/null +++ b/vendor/github.com/samber/lo/type_manipulation.go @@ -0,0 +1,88 @@ +package lo + +// ToPtr returns a pointer copy of value. +func ToPtr[T any](x T) *T { + return &x +} + +// FromPtr returns the pointer value or empty. +func FromPtr[T any](x *T) T { + if x == nil { + return Empty[T]() + } + + return *x +} + +// FromPtrOr returns the pointer value or the fallback value. +func FromPtrOr[T any](x *T, fallback T) T { + if x == nil { + return fallback + } + + return *x +} + +// ToSlicePtr returns a slice of pointer copy of value. +func ToSlicePtr[T any](collection []T) []*T { + return Map(collection, func(x T, _ int) *T { + return &x + }) +} + +// ToAnySlice returns a slice with all elements mapped to `any` type +func ToAnySlice[T any](collection []T) []any { + result := make([]any, len(collection)) + for i, item := range collection { + result[i] = item + } + return result +} + +// FromAnySlice returns an `any` slice with all elements mapped to a type. +// Returns false in case of type conversion failure. +func FromAnySlice[T any](in []any) (out []T, ok bool) { + defer func() { + if r := recover(); r != nil { + out = []T{} + ok = false + } + }() + + result := make([]T, len(in)) + for i, item := range in { + result[i] = item.(T) + } + return result, true +} + +// Empty returns an empty value. +func Empty[T any]() T { + var zero T + return zero +} + +// IsEmpty returns true if argument is a zero value. +func IsEmpty[T comparable](v T) bool { + var zero T + return zero == v +} + +// IsNotEmpty returns true if argument is not a zero value. +func IsNotEmpty[T comparable](v T) bool { + var zero T + return zero != v +} + +// Coalesce returns the first non-empty arguments. Arguments must be comparable. +func Coalesce[T comparable](v ...T) (result T, ok bool) { + for _, e := range v { + if e != result { + result = e + ok = true + return + } + } + + return +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_compare.go b/vendor/github.com/stretchr/testify/assert/assertion_compare.go index 41649d26..95d8e59d 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_compare.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_compare.go @@ -1,8 +1,10 @@ package assert import ( + "bytes" "fmt" "reflect" + "time" ) type CompareType int @@ -30,6 +32,9 @@ var ( float64Type = reflect.TypeOf(float64(1)) stringType = reflect.TypeOf("") + + timeType = reflect.TypeOf(time.Time{}) + bytesType = reflect.TypeOf([]byte{}) ) func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { @@ -299,6 +304,47 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { return compareLess, true } } + // Check for known struct types we can check for compare results. + case reflect.Struct: + { + // All structs enter here. We're not interested in most types. + if !canConvert(obj1Value, timeType) { + break + } + + // time.Time can compared! + timeObj1, ok := obj1.(time.Time) + if !ok { + timeObj1 = obj1Value.Convert(timeType).Interface().(time.Time) + } + + timeObj2, ok := obj2.(time.Time) + if !ok { + timeObj2 = obj2Value.Convert(timeType).Interface().(time.Time) + } + + return compare(timeObj1.UnixNano(), timeObj2.UnixNano(), reflect.Int64) + } + case reflect.Slice: + { + // We only care about the []byte type. + if !canConvert(obj1Value, bytesType) { + break + } + + // []byte can be compared! + bytesObj1, ok := obj1.([]byte) + if !ok { + bytesObj1 = obj1Value.Convert(bytesType).Interface().([]byte) + + } + bytesObj2, ok := obj2.([]byte) + if !ok { + bytesObj2 = obj2Value.Convert(bytesType).Interface().([]byte) + } + + return CompareType(bytes.Compare(bytesObj1, bytesObj2)), true + } } return compareEqual, false @@ -310,7 +356,10 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { // assert.Greater(t, float64(2), float64(1)) // assert.Greater(t, "b", "a") func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...) } // GreaterOrEqual asserts that the first element is greater than or equal to the second @@ -320,7 +369,10 @@ func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface // assert.GreaterOrEqual(t, "b", "a") // assert.GreaterOrEqual(t, "b", "b") func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareGreater, compareEqual}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareGreater, compareEqual}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...) } // Less asserts that the first element is less than the second @@ -329,7 +381,10 @@ func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...in // assert.Less(t, float64(1), float64(2)) // assert.Less(t, "a", "b") func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...) } // LessOrEqual asserts that the first element is less than or equal to the second @@ -339,7 +394,10 @@ func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) // assert.LessOrEqual(t, "a", "b") // assert.LessOrEqual(t, "b", "b") func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...) } // Positive asserts that the specified element is positive @@ -347,8 +405,11 @@ func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...inter // assert.Positive(t, 1) // assert.Positive(t, 1.23) func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } zero := reflect.Zero(reflect.TypeOf(e)) - return compareTwoValues(t, e, zero.Interface(), []CompareType{compareGreater}, "\"%v\" is not positive", msgAndArgs) + return compareTwoValues(t, e, zero.Interface(), []CompareType{compareGreater}, "\"%v\" is not positive", msgAndArgs...) } // Negative asserts that the specified element is negative @@ -356,8 +417,11 @@ func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { // assert.Negative(t, -1) // assert.Negative(t, -1.23) func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } zero := reflect.Zero(reflect.TypeOf(e)) - return compareTwoValues(t, e, zero.Interface(), []CompareType{compareLess}, "\"%v\" is not negative", msgAndArgs) + return compareTwoValues(t, e, zero.Interface(), []CompareType{compareLess}, "\"%v\" is not negative", msgAndArgs...) } func compareTwoValues(t TestingT, e1 interface{}, e2 interface{}, allowedComparesResults []CompareType, failMessage string, msgAndArgs ...interface{}) bool { diff --git a/vendor/github.com/stretchr/testify/assert/assertion_compare_can_convert.go b/vendor/github.com/stretchr/testify/assert/assertion_compare_can_convert.go new file mode 100644 index 00000000..da867903 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_compare_can_convert.go @@ -0,0 +1,16 @@ +//go:build go1.17 +// +build go1.17 + +// TODO: once support for Go 1.16 is dropped, this file can be +// merged/removed with assertion_compare_go1.17_test.go and +// assertion_compare_legacy.go + +package assert + +import "reflect" + +// Wrapper around reflect.Value.CanConvert, for compatibility +// reasons. +func canConvert(value reflect.Value, to reflect.Type) bool { + return value.CanConvert(to) +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_compare_legacy.go b/vendor/github.com/stretchr/testify/assert/assertion_compare_legacy.go new file mode 100644 index 00000000..1701af2a --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_compare_legacy.go @@ -0,0 +1,16 @@ +//go:build !go1.17 +// +build !go1.17 + +// TODO: once support for Go 1.16 is dropped, this file can be +// merged/removed with assertion_compare_go1.17_test.go and +// assertion_compare_can_convert.go + +package assert + +import "reflect" + +// Older versions of Go does not have the reflect.Value.CanConvert +// method. +func canConvert(value reflect.Value, to reflect.Type) bool { + return false +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_format.go b/vendor/github.com/stretchr/testify/assert/assertion_format.go index 4dfd1229..7880b8f9 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_format.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_format.go @@ -123,6 +123,18 @@ func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...int return ErrorAs(t, err, target, append([]interface{}{msg}, args...)...) } +// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// assert.ErrorContainsf(t, err, expectedErrorSubString, "error message %s", "formatted") +func ErrorContainsf(t TestingT, theError error, contains string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return ErrorContains(t, theError, contains, append([]interface{}{msg}, args...)...) +} + // ErrorIsf asserts that at least one of the errors in err's chain matches target. // This is a wrapper for errors.Is. func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool { @@ -724,6 +736,16 @@ func WithinDurationf(t TestingT, expected time.Time, actual time.Time, delta tim return WithinDuration(t, expected, actual, delta, append([]interface{}{msg}, args...)...) } +// WithinRangef asserts that a time is within a time range (inclusive). +// +// assert.WithinRangef(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted") +func WithinRangef(t TestingT, actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return WithinRange(t, actual, start, end, append([]interface{}{msg}, args...)...) +} + // YAMLEqf asserts that two YAML strings are equivalent. func YAMLEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { diff --git a/vendor/github.com/stretchr/testify/assert/assertion_forward.go b/vendor/github.com/stretchr/testify/assert/assertion_forward.go index 25337a6f..339515b8 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_forward.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_forward.go @@ -222,6 +222,30 @@ func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args .. return ErrorAsf(a.t, err, target, msg, args...) } +// ErrorContains asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// a.ErrorContains(err, expectedErrorSubString) +func (a *Assertions) ErrorContains(theError error, contains string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ErrorContains(a.t, theError, contains, msgAndArgs...) +} + +// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// a.ErrorContainsf(err, expectedErrorSubString, "error message %s", "formatted") +func (a *Assertions) ErrorContainsf(theError error, contains string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ErrorContainsf(a.t, theError, contains, msg, args...) +} + // ErrorIs asserts that at least one of the errors in err's chain matches target. // This is a wrapper for errors.Is. func (a *Assertions) ErrorIs(err error, target error, msgAndArgs ...interface{}) bool { @@ -1437,6 +1461,26 @@ func (a *Assertions) WithinDurationf(expected time.Time, actual time.Time, delta return WithinDurationf(a.t, expected, actual, delta, msg, args...) } +// WithinRange asserts that a time is within a time range (inclusive). +// +// a.WithinRange(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) +func (a *Assertions) WithinRange(actual time.Time, start time.Time, end time.Time, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return WithinRange(a.t, actual, start, end, msgAndArgs...) +} + +// WithinRangef asserts that a time is within a time range (inclusive). +// +// a.WithinRangef(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted") +func (a *Assertions) WithinRangef(actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return WithinRangef(a.t, actual, start, end, msg, args...) +} + // YAMLEq asserts that two YAML strings are equivalent. func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { diff --git a/vendor/github.com/stretchr/testify/assert/assertion_order.go b/vendor/github.com/stretchr/testify/assert/assertion_order.go index 1c3b4718..75944878 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_order.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_order.go @@ -50,7 +50,7 @@ func isOrdered(t TestingT, object interface{}, allowedComparesResults []CompareT // assert.IsIncreasing(t, []float{1, 2}) // assert.IsIncreasing(t, []string{"a", "b"}) func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...) } // IsNonIncreasing asserts that the collection is not increasing @@ -59,7 +59,7 @@ func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo // assert.IsNonIncreasing(t, []float{2, 1}) // assert.IsNonIncreasing(t, []string{"b", "a"}) func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...) } // IsDecreasing asserts that the collection is decreasing @@ -68,7 +68,7 @@ func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) // assert.IsDecreasing(t, []float{2, 1}) // assert.IsDecreasing(t, []string{"b", "a"}) func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...) } // IsNonDecreasing asserts that the collection is not decreasing @@ -77,5 +77,5 @@ func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo // assert.IsNonDecreasing(t, []float{1, 2}) // assert.IsNonDecreasing(t, []string{"a", "b"}) func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...) } diff --git a/vendor/github.com/stretchr/testify/assert/assertions.go b/vendor/github.com/stretchr/testify/assert/assertions.go index bcac4401..fa1245b1 100644 --- a/vendor/github.com/stretchr/testify/assert/assertions.go +++ b/vendor/github.com/stretchr/testify/assert/assertions.go @@ -8,6 +8,7 @@ import ( "fmt" "math" "os" + "path/filepath" "reflect" "regexp" "runtime" @@ -144,7 +145,8 @@ func CallerInfo() []string { if len(parts) > 1 { dir := parts[len(parts)-2] if (dir != "assert" && dir != "mock" && dir != "require") || file == "mock_test.go" { - callers = append(callers, fmt.Sprintf("%s:%d", file, line)) + path, _ := filepath.Abs(file) + callers = append(callers, fmt.Sprintf("%s:%d", path, line)) } } @@ -563,16 +565,17 @@ func isEmpty(object interface{}) bool { switch objValue.Kind() { // collection types are empty when they have no element - case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + case reflect.Chan, reflect.Map, reflect.Slice: return objValue.Len() == 0 - // pointers are empty if nil or if the value they point to is empty + // pointers are empty if nil or if the value they point to is empty case reflect.Ptr: if objValue.IsNil() { return true } deref := objValue.Elem().Interface() return isEmpty(deref) - // for all other types, compare against the zero value + // for all other types, compare against the zero value + // array types are empty when they match their zero-initialized state default: zero := reflect.Zero(objValue.Type()) return reflect.DeepEqual(object, zero.Interface()) @@ -718,10 +721,14 @@ func NotEqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...inte // return (false, false) if impossible. // return (true, false) if element was not found. // return (true, true) if element was found. -func includeElement(list interface{}, element interface{}) (ok, found bool) { +func containsElement(list interface{}, element interface{}) (ok, found bool) { listValue := reflect.ValueOf(list) - listKind := reflect.TypeOf(list).Kind() + listType := reflect.TypeOf(list) + if listType == nil { + return false, false + } + listKind := listType.Kind() defer func() { if e := recover(); e != nil { ok = false @@ -764,7 +771,7 @@ func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bo h.Helper() } - ok, found := includeElement(s, contains) + ok, found := containsElement(s, contains) if !ok { return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", s), msgAndArgs...) } @@ -787,7 +794,7 @@ func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) h.Helper() } - ok, found := includeElement(s, contains) + ok, found := containsElement(s, contains) if !ok { return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...) } @@ -811,7 +818,6 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok return true // we consider nil to be equal to the nil set } - subsetValue := reflect.ValueOf(subset) defer func() { if e := recover(); e != nil { ok = false @@ -821,17 +827,35 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok listKind := reflect.TypeOf(list).Kind() subsetKind := reflect.TypeOf(subset).Kind() - if listKind != reflect.Array && listKind != reflect.Slice { + if listKind != reflect.Array && listKind != reflect.Slice && listKind != reflect.Map { return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...) } - if subsetKind != reflect.Array && subsetKind != reflect.Slice { + if subsetKind != reflect.Array && subsetKind != reflect.Slice && listKind != reflect.Map { return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...) } + subsetValue := reflect.ValueOf(subset) + if subsetKind == reflect.Map && listKind == reflect.Map { + listValue := reflect.ValueOf(list) + subsetKeys := subsetValue.MapKeys() + + for i := 0; i < len(subsetKeys); i++ { + subsetKey := subsetKeys[i] + subsetElement := subsetValue.MapIndex(subsetKey).Interface() + listElement := listValue.MapIndex(subsetKey).Interface() + + if !ObjectsAreEqual(subsetElement, listElement) { + return Fail(t, fmt.Sprintf("\"%s\" does not contain \"%s\"", list, subsetElement), msgAndArgs...) + } + } + + return true + } + for i := 0; i < subsetValue.Len(); i++ { element := subsetValue.Index(i).Interface() - ok, found := includeElement(list, element) + ok, found := containsElement(list, element) if !ok { return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...) } @@ -852,10 +876,9 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) h.Helper() } if subset == nil { - return Fail(t, fmt.Sprintf("nil is the empty set which is a subset of every set"), msgAndArgs...) + return Fail(t, "nil is the empty set which is a subset of every set", msgAndArgs...) } - subsetValue := reflect.ValueOf(subset) defer func() { if e := recover(); e != nil { ok = false @@ -865,17 +888,35 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) listKind := reflect.TypeOf(list).Kind() subsetKind := reflect.TypeOf(subset).Kind() - if listKind != reflect.Array && listKind != reflect.Slice { + if listKind != reflect.Array && listKind != reflect.Slice && listKind != reflect.Map { return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...) } - if subsetKind != reflect.Array && subsetKind != reflect.Slice { + if subsetKind != reflect.Array && subsetKind != reflect.Slice && listKind != reflect.Map { return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...) } + subsetValue := reflect.ValueOf(subset) + if subsetKind == reflect.Map && listKind == reflect.Map { + listValue := reflect.ValueOf(list) + subsetKeys := subsetValue.MapKeys() + + for i := 0; i < len(subsetKeys); i++ { + subsetKey := subsetKeys[i] + subsetElement := subsetValue.MapIndex(subsetKey).Interface() + listElement := listValue.MapIndex(subsetKey).Interface() + + if !ObjectsAreEqual(subsetElement, listElement) { + return true + } + } + + return Fail(t, fmt.Sprintf("%q is a subset of %q", subset, list), msgAndArgs...) + } + for i := 0; i < subsetValue.Len(); i++ { element := subsetValue.Index(i).Interface() - ok, found := includeElement(list, element) + ok, found := containsElement(list, element) if !ok { return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...) } @@ -1000,27 +1041,21 @@ func Condition(t TestingT, comp Comparison, msgAndArgs ...interface{}) bool { type PanicTestFunc func() // didPanic returns true if the function passed to it panics. Otherwise, it returns false. -func didPanic(f PanicTestFunc) (bool, interface{}, string) { - - didPanic := false - var message interface{} - var stack string - func() { - - defer func() { - if message = recover(); message != nil { - didPanic = true - stack = string(debug.Stack()) - } - }() - - // call the target function - f() +func didPanic(f PanicTestFunc) (didPanic bool, message interface{}, stack string) { + didPanic = true + defer func() { + message = recover() + if didPanic { + stack = string(debug.Stack()) + } }() - return didPanic, message, stack + // call the target function + f() + didPanic = false + return } // Panics asserts that the code inside the specified PanicTestFunc panics. @@ -1111,6 +1146,27 @@ func WithinDuration(t TestingT, expected, actual time.Time, delta time.Duration, return true } +// WithinRange asserts that a time is within a time range (inclusive). +// +// assert.WithinRange(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second)) +func WithinRange(t TestingT, actual, start, end time.Time, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if end.Before(start) { + return Fail(t, "Start should be before end", msgAndArgs...) + } + + if actual.Before(start) { + return Fail(t, fmt.Sprintf("Time %v expected to be in time range %v to %v, but is before the range", actual, start, end), msgAndArgs...) + } else if actual.After(end) { + return Fail(t, fmt.Sprintf("Time %v expected to be in time range %v to %v, but is after the range", actual, start, end), msgAndArgs...) + } + + return true +} + func toFloat(x interface{}) (float64, bool) { var xf float64 xok := true @@ -1161,11 +1217,15 @@ func InDelta(t TestingT, expected, actual interface{}, delta float64, msgAndArgs bf, bok := toFloat(actual) if !aok || !bok { - return Fail(t, fmt.Sprintf("Parameters must be numerical"), msgAndArgs...) + return Fail(t, "Parameters must be numerical", msgAndArgs...) + } + + if math.IsNaN(af) && math.IsNaN(bf) { + return true } if math.IsNaN(af) { - return Fail(t, fmt.Sprintf("Expected must not be NaN"), msgAndArgs...) + return Fail(t, "Expected must not be NaN", msgAndArgs...) } if math.IsNaN(bf) { @@ -1188,7 +1248,7 @@ func InDeltaSlice(t TestingT, expected, actual interface{}, delta float64, msgAn if expected == nil || actual == nil || reflect.TypeOf(actual).Kind() != reflect.Slice || reflect.TypeOf(expected).Kind() != reflect.Slice { - return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...) + return Fail(t, "Parameters must be slice", msgAndArgs...) } actualSlice := reflect.ValueOf(actual) @@ -1250,8 +1310,12 @@ func InDeltaMapValues(t TestingT, expected, actual interface{}, delta float64, m func calcRelativeError(expected, actual interface{}) (float64, error) { af, aok := toFloat(expected) - if !aok { - return 0, fmt.Errorf("expected value %q cannot be converted to float", expected) + bf, bok := toFloat(actual) + if !aok || !bok { + return 0, fmt.Errorf("Parameters must be numerical") + } + if math.IsNaN(af) && math.IsNaN(bf) { + return 0, nil } if math.IsNaN(af) { return 0, errors.New("expected value must not be NaN") @@ -1259,10 +1323,6 @@ func calcRelativeError(expected, actual interface{}) (float64, error) { if af == 0 { return 0, fmt.Errorf("expected value must have a value other than zero to calculate the relative error") } - bf, bok := toFloat(actual) - if !bok { - return 0, fmt.Errorf("actual value %q cannot be converted to float", actual) - } if math.IsNaN(bf) { return 0, errors.New("actual value must not be NaN") } @@ -1298,7 +1358,7 @@ func InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, m if expected == nil || actual == nil || reflect.TypeOf(actual).Kind() != reflect.Slice || reflect.TypeOf(expected).Kind() != reflect.Slice { - return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...) + return Fail(t, "Parameters must be slice", msgAndArgs...) } actualSlice := reflect.ValueOf(actual) @@ -1375,6 +1435,27 @@ func EqualError(t TestingT, theError error, errString string, msgAndArgs ...inte return true } +// ErrorContains asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// assert.ErrorContains(t, err, expectedErrorSubString) +func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if !Error(t, theError, msgAndArgs...) { + return false + } + + actual := theError.Error() + if !strings.Contains(actual, contains) { + return Fail(t, fmt.Sprintf("Error %#v does not contain %#v", actual, contains), msgAndArgs...) + } + + return true +} + // matchRegexp return true if a specified regexp matches a string. func matchRegexp(rx interface{}, str interface{}) bool { @@ -1588,12 +1669,17 @@ func diff(expected interface{}, actual interface{}) string { } var e, a string - if et != reflect.TypeOf("") { - e = spewConfig.Sdump(expected) - a = spewConfig.Sdump(actual) - } else { + + switch et { + case reflect.TypeOf(""): e = reflect.ValueOf(expected).String() a = reflect.ValueOf(actual).String() + case reflect.TypeOf(time.Time{}): + e = spewConfigStringerEnabled.Sdump(expected) + a = spewConfigStringerEnabled.Sdump(actual) + default: + e = spewConfig.Sdump(expected) + a = spewConfig.Sdump(actual) } diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ @@ -1625,6 +1711,14 @@ var spewConfig = spew.ConfigState{ MaxDepth: 10, } +var spewConfigStringerEnabled = spew.ConfigState{ + Indent: " ", + DisablePointerAddresses: true, + DisableCapacities: true, + SortKeys: true, + MaxDepth: 10, +} + type tHelper interface { Helper() } diff --git a/vendor/gopkg.in/yaml.v3/decode.go b/vendor/gopkg.in/yaml.v3/decode.go index df36e3a3..0173b698 100644 --- a/vendor/gopkg.in/yaml.v3/decode.go +++ b/vendor/gopkg.in/yaml.v3/decode.go @@ -100,7 +100,10 @@ func (p *parser) peek() yaml_event_type_t { if p.event.typ != yaml_NO_EVENT { return p.event.typ } - if !yaml_parser_parse(&p.parser, &p.event) { + // It's curious choice from the underlying API to generally return a + // positive result on success, but on this case return true in an error + // scenario. This was the source of bugs in the past (issue #666). + if !yaml_parser_parse(&p.parser, &p.event) || p.parser.error != yaml_NO_ERROR { p.fail() } return p.event.typ @@ -320,6 +323,8 @@ type decoder struct { decodeCount int aliasCount int aliasDepth int + + mergedFields map[interface{}]bool } var ( @@ -808,6 +813,11 @@ func (d *decoder) mapping(n *Node, out reflect.Value) (good bool) { } } + mergedFields := d.mergedFields + d.mergedFields = nil + + var mergeNode *Node + mapIsNew := false if out.IsNil() { out.Set(reflect.MakeMap(outt)) @@ -815,11 +825,18 @@ func (d *decoder) mapping(n *Node, out reflect.Value) (good bool) { } for i := 0; i < l; i += 2 { if isMerge(n.Content[i]) { - d.merge(n.Content[i+1], out) + mergeNode = n.Content[i+1] continue } k := reflect.New(kt).Elem() if d.unmarshal(n.Content[i], k) { + if mergedFields != nil { + ki := k.Interface() + if mergedFields[ki] { + continue + } + mergedFields[ki] = true + } kkind := k.Kind() if kkind == reflect.Interface { kkind = k.Elem().Kind() @@ -833,6 +850,12 @@ func (d *decoder) mapping(n *Node, out reflect.Value) (good bool) { } } } + + d.mergedFields = mergedFields + if mergeNode != nil { + d.merge(n, mergeNode, out) + } + d.stringMapType = stringMapType d.generalMapType = generalMapType return true @@ -844,7 +867,8 @@ func isStringMap(n *Node) bool { } l := len(n.Content) for i := 0; i < l; i += 2 { - if n.Content[i].ShortTag() != strTag { + shortTag := n.Content[i].ShortTag() + if shortTag != strTag && shortTag != mergeTag { return false } } @@ -861,7 +885,6 @@ func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) { var elemType reflect.Type if sinfo.InlineMap != -1 { inlineMap = out.Field(sinfo.InlineMap) - inlineMap.Set(reflect.New(inlineMap.Type()).Elem()) elemType = inlineMap.Type().Elem() } @@ -870,6 +893,9 @@ func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) { d.prepare(n, field) } + mergedFields := d.mergedFields + d.mergedFields = nil + var mergeNode *Node var doneFields []bool if d.uniqueKeys { doneFields = make([]bool, len(sinfo.FieldsList)) @@ -879,13 +905,20 @@ func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) { for i := 0; i < l; i += 2 { ni := n.Content[i] if isMerge(ni) { - d.merge(n.Content[i+1], out) + mergeNode = n.Content[i+1] continue } if !d.unmarshal(ni, name) { continue } - if info, ok := sinfo.FieldsMap[name.String()]; ok { + sname := name.String() + if mergedFields != nil { + if mergedFields[sname] { + continue + } + mergedFields[sname] = true + } + if info, ok := sinfo.FieldsMap[sname]; ok { if d.uniqueKeys { if doneFields[info.Id] { d.terrors = append(d.terrors, fmt.Sprintf("line %d: field %s already set in type %s", ni.Line, name.String(), out.Type())) @@ -911,6 +944,11 @@ func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) { d.terrors = append(d.terrors, fmt.Sprintf("line %d: field %s not found in type %s", ni.Line, name.String(), out.Type())) } } + + d.mergedFields = mergedFields + if mergeNode != nil { + d.merge(n, mergeNode, out) + } return true } @@ -918,19 +956,29 @@ func failWantMap() { failf("map merge requires map or sequence of maps as the value") } -func (d *decoder) merge(n *Node, out reflect.Value) { - switch n.Kind { +func (d *decoder) merge(parent *Node, merge *Node, out reflect.Value) { + mergedFields := d.mergedFields + if mergedFields == nil { + d.mergedFields = make(map[interface{}]bool) + for i := 0; i < len(parent.Content); i += 2 { + k := reflect.New(ifaceType).Elem() + if d.unmarshal(parent.Content[i], k) { + d.mergedFields[k.Interface()] = true + } + } + } + + switch merge.Kind { case MappingNode: - d.unmarshal(n, out) + d.unmarshal(merge, out) case AliasNode: - if n.Alias != nil && n.Alias.Kind != MappingNode { + if merge.Alias != nil && merge.Alias.Kind != MappingNode { failWantMap() } - d.unmarshal(n, out) + d.unmarshal(merge, out) case SequenceNode: - // Step backwards as earlier nodes take precedence. - for i := len(n.Content) - 1; i >= 0; i-- { - ni := n.Content[i] + for i := 0; i < len(merge.Content); i++ { + ni := merge.Content[i] if ni.Kind == AliasNode { if ni.Alias != nil && ni.Alias.Kind != MappingNode { failWantMap() @@ -943,6 +991,8 @@ func (d *decoder) merge(n *Node, out reflect.Value) { default: failWantMap() } + + d.mergedFields = mergedFields } func isMerge(n *Node) bool { diff --git a/vendor/gopkg.in/yaml.v3/parserc.go b/vendor/gopkg.in/yaml.v3/parserc.go index ac66fccc..268558a0 100644 --- a/vendor/gopkg.in/yaml.v3/parserc.go +++ b/vendor/gopkg.in/yaml.v3/parserc.go @@ -687,6 +687,9 @@ func yaml_parser_parse_node(parser *yaml_parser_t, event *yaml_event_t, block, i func yaml_parser_parse_block_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { if first { token := peek_token(parser) + if token == nil { + return false + } parser.marks = append(parser.marks, token.start_mark) skip_token(parser) } @@ -786,7 +789,7 @@ func yaml_parser_split_stem_comment(parser *yaml_parser_t, stem_len int) { } token := peek_token(parser) - if token.typ != yaml_BLOCK_SEQUENCE_START_TOKEN && token.typ != yaml_BLOCK_MAPPING_START_TOKEN { + if token == nil || token.typ != yaml_BLOCK_SEQUENCE_START_TOKEN && token.typ != yaml_BLOCK_MAPPING_START_TOKEN { return } @@ -813,6 +816,9 @@ func yaml_parser_split_stem_comment(parser *yaml_parser_t, stem_len int) { func yaml_parser_parse_block_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { if first { token := peek_token(parser) + if token == nil { + return false + } parser.marks = append(parser.marks, token.start_mark) skip_token(parser) } @@ -922,6 +928,9 @@ func yaml_parser_parse_block_mapping_value(parser *yaml_parser_t, event *yaml_ev func yaml_parser_parse_flow_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { if first { token := peek_token(parser) + if token == nil { + return false + } parser.marks = append(parser.marks, token.start_mark) skip_token(parser) } diff --git a/vendor/modules.txt b/vendor/modules.txt index 3c9757d8..a8818516 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -121,6 +121,10 @@ github.com/jesseduffield/gocui # github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 ## explicit; go 1.18 github.com/jesseduffield/kill +# github.com/jesseduffield/lazycore v0.0.0-20221009164442-17c8b878c316 +## explicit; go 1.18 +github.com/jesseduffield/lazycore/pkg/boxlayout +github.com/jesseduffield/lazycore/pkg/utils # github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56 ## explicit github.com/jesseduffield/yaml @@ -169,7 +173,7 @@ github.com/pmezard/go-difflib/difflib # github.com/rivo/uniseg v0.2.0 ## explicit; go 1.12 github.com/rivo/uniseg -# github.com/samber/lo v1.20.0 +# github.com/samber/lo v1.31.0 ## explicit; go 1.18 github.com/samber/lo # github.com/sirupsen/logrus v1.4.2 @@ -178,7 +182,7 @@ github.com/sirupsen/logrus # github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad ## explicit github.com/spkg/bom -# github.com/stretchr/testify v1.7.0 +# github.com/stretchr/testify v1.8.0 ## explicit; go 1.13 github.com/stretchr/testify/assert # github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 @@ -213,7 +217,7 @@ golang.org/x/xerrors golang.org/x/xerrors/internal # gopkg.in/yaml.v2 v2.2.2 ## explicit -# gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b +# gopkg.in/yaml.v3 v3.0.1 ## explicit gopkg.in/yaml.v3 # gotest.tools/v3 v3.2.0 From ae240021c10f1f4b634f35dec3727e857ddfbf86 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 11:36:51 -0700 Subject: [PATCH 02/10] ensure we never delete views --- pkg/gui/confirmation_panel.go | 59 ++++++++++++++++------------------- pkg/gui/menu_panel.go | 20 +++--------- pkg/gui/view_helpers.go | 3 -- pkg/gui/views.go | 1 + 4 files changed, 33 insertions(+), 50 deletions(-) diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index e2df8438..4c3b761a 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -15,25 +15,27 @@ import ( func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error { return func(g *gocui.Gui, v *gocui.View) error { + if err := gui.closeConfirmationPrompt(); err != nil { + return err + } + if function != nil { if err := function(g, v); err != nil { return err } } - return gui.closeConfirmationPrompt() + + return nil } } func (gui *Gui) closeConfirmationPrompt() error { - view, err := gui.g.View("confirmation") - if err != nil { - return nil // if it's already been closed we can just return - } - if err := gui.returnFocus(gui.g, view); err != nil { + if err := gui.returnFocus(gui.g, gui.Views.Confirmation); err != nil { return err } gui.g.DeleteViewKeybindings("confirmation") - return gui.g.DeleteView("confirmation") + gui.Views.Confirmation.Visible = false + return nil } func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int { @@ -62,42 +64,36 @@ func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt s func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error { gui.onNewPopupPanel() - confirmationView, err := gui.prepareConfirmationPanel(currentView, title, "", false) + err := gui.prepareConfirmationPanel(currentView, title, "", false) if err != nil { return err } - confirmationView.Editable = true + gui.Views.Confirmation.Editable = true return gui.setKeyBindings(g, handleConfirm, nil) } -func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string, hasLoader bool) (*gocui.View, error) { +func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string, hasLoader bool) error { x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, true, prompt) - confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0) + confirmationView := gui.Views.Confirmation + _, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0) if err != nil { - if err.Error() != "unknown view" { - return nil, err - } - confirmationView.HasLoader = hasLoader - gui.g.StartTicking() - confirmationView.Title = title - confirmationView.Wrap = true - confirmationView.FgColor = gocui.ColorDefault + return err } + confirmationView.HasLoader = hasLoader + if hasLoader { + gui.g.StartTicking() + } + confirmationView.Title = title + confirmationView.Visible = true gui.g.Update(func(g *gocui.Gui) error { return gui.switchFocus(gui.g, currentView, confirmationView, false) }) - return confirmationView, nil + return nil } func (gui *Gui) onNewPopupPanel() { - viewNames := []string{ - "commitMessage", - "credentials", - "menu", - } - for _, viewName := range viewNames { - _, _ = gui.g.SetViewOnBottom(viewName) - } + gui.Views.Menu.Visible = false + gui.Views.Confirmation.Visible = false } // it is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password @@ -110,17 +106,16 @@ func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, t func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, hasLoader bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { gui.onNewPopupPanel() g.Update(func(g *gocui.Gui) error { - // delete the existing confirmation panel if it exists - if view, _ := g.View("confirmation"); view != nil { + if gui.currentViewName() == "confirmation" { if err := gui.closeConfirmationPrompt(); err != nil { gui.Log.Error(err.Error()) } } - confirmationView, err := gui.prepareConfirmationPanel(currentView, title, prompt, hasLoader) + err := gui.prepareConfirmationPanel(currentView, title, prompt, hasLoader) if err != nil { return err } - confirmationView.Editable = false + gui.Views.Confirmation.Editable = false if err := gui.renderString(g, "confirmation", prompt); err != nil { return err } diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index 40bb30ac..adb35818 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -57,10 +57,7 @@ func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error { return err } } - err := g.DeleteView("menu") - if err != nil { - return err - } + gui.Views.Menu.Visible = false return gui.returnFocus(g, v) } @@ -73,7 +70,8 @@ func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handl } x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list) - menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0) + _, _ = gui.g.SetView("menu", x0, y0, x1, y1, 0) + menuView := gui.Views.Menu menuView.Title = title menuView.FgColor = gocui.ColorDefault menuView.Clear() @@ -85,11 +83,7 @@ func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handl if err := handlePress(selectedLine); err != nil { return err } - if _, err := gui.g.View("menu"); err == nil { - if _, err := gui.g.SetViewOnBottom("menu"); err != nil { - return err - } - } + menuView.Visible = false return gui.returnFocus(gui.g, menuView) } @@ -105,11 +99,7 @@ func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handl } gui.g.Update(func(g *gocui.Gui) error { - if _, err := gui.g.View("menu"); err == nil { - if _, err := g.SetViewOnTop("menu"); err != nil { - return err - } - } + menuView.Visible = true currentView := gui.g.CurrentView() return gui.switchFocus(gui.g, currentView, menuView, false) }) diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 6e1a62cf..a594ccb5 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -142,9 +142,6 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View, returnin if _, err := g.SetCurrentView(newView.Name()); err != nil { return err } - if _, err := g.SetViewOnTop(newView.Name()); err != nil { - return err - } g.Cursor = newView.Editable diff --git a/pkg/gui/views.go b/pkg/gui/views.go index 3f83dd29..3de54c26 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -106,6 +106,7 @@ func (gui *Gui) createAllViews() error { } gui.Views.Confirmation.Visible = false + gui.Views.Confirmation.Wrap = true gui.Views.Menu.Visible = false gui.Views.Limit.Visible = false From c6607686eb241acbb8b3df9746814bcacd0a18fe Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 16:30:11 -0700 Subject: [PATCH 03/10] fix view stack code --- pkg/gui/arrangement.go | 4 ---- pkg/gui/gui.go | 10 +++++----- pkg/gui/subprocess.go | 4 ++-- pkg/gui/view_helpers.go | 37 ++++++++++++++++++++++++++----------- 4 files changed, 33 insertions(+), 22 deletions(-) diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go index 58b53706..b24c82af 100644 --- a/pkg/gui/arrangement.go +++ b/pkg/gui/arrangement.go @@ -222,10 +222,6 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { // return "files" // default // } -func (gui *Gui) currentNonPopupWindowName() string { - return gui.peekPreviousView() -} - // TODO: do this better. func (gui *Gui) currentSideWindowName() string { windowName := gui.currentWindow() diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index ccafc9ef..17710987 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -8,7 +8,6 @@ import ( "time" "github.com/docker/docker/api/types" - "github.com/golang-collections/collections/stack" // "io" // "io/ioutil" @@ -123,8 +122,9 @@ type panelStates struct { } type guiState struct { - MenuItemCount int // can't store the actual list because it's of interface{} type - PreviousViews *stack.Stack + MenuItemCount int // can't store the actual list because it's of interface{} type + // the names of views in the current focus stack (last item is the current view) + ViewStack []string Platform commands.Platform Panels *panelStates SubProcessOutput string @@ -165,8 +165,8 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand }, Project: &projectState{ContextIndex: 0}, }, - SessionIndex: 0, - PreviousViews: stack.New(), + SessionIndex: 0, + ViewStack: []string{}, } cyclableViews := []string{"project", "containers", "images", "volumes"} diff --git a/pkg/gui/subprocess.go b/pkg/gui/subprocess.go index df23afd6..059f05e1 100644 --- a/pkg/gui/subprocess.go +++ b/pkg/gui/subprocess.go @@ -22,7 +22,7 @@ func (gui *Gui) RunWithSubprocesses() error { break } else if err == gui.Errors.ErrSubProcess { // preparing the state for when we return - gui.pushPreviousView(gui.currentViewName()) + gui.pushView(gui.currentViewName()) // giving goEvery goroutines time to finish gui.State.SessionIndex++ @@ -31,7 +31,7 @@ func (gui *Gui) RunWithSubprocesses() error { } // pop here so we don't stack up view names - gui.popPreviousView() + gui.popView() // ensuring we render e.g. the logs of the currently selected item upon return gui.State.Panels.Main.ObjectKey = "" } else { diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index a594ccb5..a585d807 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -7,6 +7,7 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazydocker/pkg/utils" + "github.com/samber/lo" "github.com/spkg/bom" ) @@ -32,7 +33,7 @@ func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error { panic(err) } gui.resetMainView() - gui.popPreviousView() + gui.popView() return gui.switchFocus(g, v, focusedView, false) } @@ -58,7 +59,7 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error { panic(err) } gui.resetMainView() - gui.popPreviousView() + gui.popView() return gui.switchFocus(g, v, focusedView, false) } @@ -95,28 +96,42 @@ func (gui *Gui) newLineFocused(v *gocui.View) error { } } -func (gui *Gui) popPreviousView() string { - if gui.State.PreviousViews.Len() > 0 { - return gui.State.PreviousViews.Pop().(string) +func (gui *Gui) popView() string { + if len(gui.State.ViewStack) > 0 { + value := gui.State.ViewStack[len(gui.State.ViewStack)-1] + gui.State.ViewStack = gui.State.ViewStack[:len(gui.State.ViewStack)-1] + return value } return "" } func (gui *Gui) peekPreviousView() string { - if gui.State.PreviousViews.Len() > 0 { - return gui.State.PreviousViews.Peek().(string) + if len(gui.State.ViewStack) > 0 { + return gui.State.ViewStack[len(gui.State.ViewStack)-1] } return "" } -func (gui *Gui) pushPreviousView(name string) { - gui.State.PreviousViews.Push(name) +func (gui *Gui) pushView(name string) { + // No matter what view we're pushing, we first remove all popup panels from the stack + gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool { + return viewName != "confirmation" && viewName != "menu" + }) + + // If we're pushing a side panel, we remove all other panels + if lo.Contains(gui.sideViewNames(), name) { + gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool { + return viewName != "main" + }) + } + + gui.State.ViewStack = append(gui.State.ViewStack, name) } func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error { - previousViewName := gui.popPreviousView() + previousViewName := gui.popView() previousView, err := g.View(previousViewName) if err != nil { // always fall back to services view if there's no 'previous' view stored @@ -134,7 +149,7 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View, returnin // we assume we'll never want to return focus to a popup panel i.e. // we should never stack popup panels if oldView != nil && !gui.isPopupPanel(oldView.Name()) && !returning { - gui.pushPreviousView(oldView.Name()) + gui.pushView(oldView.Name()) } gui.Log.Info("setting highlight to true for view " + newView.Name()) From fb7b28243b48aa689dfc741eab31a49c7a4738bd Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 16:48:35 -0700 Subject: [PATCH 04/10] improve subprocess approach --- pkg/app/app.go | 3 +- pkg/gui/containers_panel.go | 6 +-- pkg/gui/custom_commands.go | 3 +- pkg/gui/gui.go | 73 +++++++++++++------------------------ pkg/gui/project_panel.go | 3 +- pkg/gui/services_panel.go | 9 ++--- pkg/gui/subprocess.go | 68 ++++++++++++++++------------------ pkg/i18n/dutch.go | 1 - pkg/i18n/english.go | 2 - pkg/i18n/german.go | 1 - pkg/i18n/polish.go | 1 - pkg/i18n/turkish.go | 1 - 12 files changed, 65 insertions(+), 106 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index a920f440..09a49789 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -56,8 +56,7 @@ func NewApp(config *config.AppConfig) (*App, error) { } func (app *App) Run() error { - err := app.Gui.RunWithSubprocesses() - return err + return app.Gui.Run() } func (app *App) Close() error { diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 8978b99e..e4309e72 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -492,8 +492,7 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error { return gui.createErrorPanel(gui.g, err.Error()) } - gui.SubProcess = c - return gui.Errors.ErrSubProcess + return gui.runSubprocess(c) } func (gui *Gui) handlePruneContainers() error { @@ -537,8 +536,7 @@ func (gui *Gui) containerExecShell(container *commands.Container) error { resolvedCommand := utils.ApplyTemplate("docker exec -it {{ .Container.ID }} /bin/sh -c 'eval $(grep ^$(id -un): /etc/passwd | cut -d : -f 7-)'", commandObject) // attach and return the subprocess error cmd := gui.OSCommand.ExecutableFromString(resolvedCommand) - gui.SubProcess = cmd - return gui.Errors.ErrSubProcess + return gui.runSubprocess(cmd) } func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error { diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index 1a23b719..da03eed9 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -53,8 +53,7 @@ func (gui *Gui) createCommandMenu(customCommands []config.CustomCommand, command // if we have a command for attaching, we attach and return the subprocess error if option.customCommand.Attach { cmd := gui.OSCommand.ExecutableFromString(option.command) - gui.SubProcess = cmd - return gui.Errors.ErrSubProcess + return gui.runSubprocess(cmd) } return gui.WithWaitingStatus(waitingStatus, func() error { diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 17710987..c664edb0 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -2,20 +2,14 @@ package gui import ( "context" - "os/exec" "strings" "sync" "time" "github.com/docker/docker/api/types" - // "io" - // "io/ioutil" - "github.com/go-errors/errors" - // "strings" - throttle "github.com/boz/go-throttle" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazydocker/pkg/commands" @@ -31,7 +25,6 @@ var OverlappingEdges = false // SentinelErrors are the errors that have special meaning and need to be checked // by calling functions. The less of these, the better type SentinelErrors struct { - ErrSubProcess error ErrNoContainers error ErrNoImages error ErrNoVolumes error @@ -49,7 +42,6 @@ type SentinelErrors struct { // localising things in the code. func (gui *Gui) GenerateSentinelErrors() { gui.Errors = SentinelErrors{ - ErrSubProcess: errors.New(gui.Tr.RunningSubprocess), ErrNoContainers: errors.New(gui.Tr.NoContainers), ErrNoImages: errors.New(gui.Tr.NoImages), ErrNoVolumes: errors.New(gui.Tr.NoVolumes), @@ -62,7 +54,6 @@ type Gui struct { Log *logrus.Entry DockerCommand *commands.DockerCommand OSCommand *commands.OSCommand - SubProcess *exec.Cmd State guiState Config *config.AppConfig Tr *i18n.TranslationSet @@ -73,8 +64,20 @@ type Gui struct { ErrorChan chan error CyclableViews []string Views Views - + // returns true if our views have been created and assigned to gui.Views. + // Views are setup only once, upon application start. ViewsSetup bool + + // if we've suspended the gui (e.g. because we've switched to a subprocess) + // we typically want to pause some things that are running like background + // file refreshes + PauseBackgroundThreads bool + + Mutexes +} + +type Mutexes struct { + SubprocessMutex sync.Mutex } type servicePanelState struct { @@ -130,11 +133,6 @@ type guiState struct { SubProcessOutput string Stats map[string]commands.ContainerStats - // SessionIndex tells us how many times we've come back from a subprocess. - // We increment it each time we switch to a new subprocess - // Every time we go to a subprocess we need to close a few goroutines so this index is used for that purpose - SessionIndex int - ScreenMode WindowMaximisation } @@ -165,8 +163,7 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand }, Project: &projectState{ContextIndex: 0}, }, - SessionIndex: 0, - ViewStack: []string{}, + ViewStack: []string{}, } cyclableViews := []string{"project", "containers", "images", "volumes"} @@ -193,13 +190,6 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand return gui, nil } -func max(a, b int) int { - if a > b { - return a - } - return b -} - func (gui *Gui) renderGlobalOptions() error { return gui.renderOptionsMap(map[string]string{ "PgUp/PgDn": gui.Tr.Scroll, @@ -211,15 +201,15 @@ func (gui *Gui) renderGlobalOptions() error { } func (gui *Gui) goEvery(interval time.Duration, function func() error) { - currentSessionIndex := gui.State.SessionIndex _ = function() // time.Tick doesn't run immediately so we'll do that here // TODO: maybe change go func() { ticker := time.NewTicker(interval) defer ticker.Stop() for range ticker.C { - if gui.State.SessionIndex > currentSessionIndex { + if gui.PauseBackgroundThreads { return } + _ = function() } }() @@ -289,6 +279,9 @@ func (gui *Gui) Run() error { } err = g.MainLoop() + if err == gocui.ErrQuit { + return nil + } return err } @@ -411,8 +404,12 @@ func (gui *Gui) handleDonate(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) editFile(filename string) error { - _, err := gui.runSyncOrAsyncCommand(gui.OSCommand.EditFile(filename)) - return err + cmd, err := gui.OSCommand.EditFile(filename) + if err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + + return gui.runSubprocess(cmd) } func (gui *Gui) openFile(filename string) error { @@ -422,28 +419,10 @@ func (gui *Gui) openFile(filename string) error { return nil } -// runSyncOrAsyncCommand takes the output of a command that may have returned -// either no error, an error, or a subprocess to execute, and if a subprocess -// needs to be set on the gui object, it does so, and then returns the error -// the bool returned tells us whether the calling code should continue -func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) { - if err != nil { - if err != gui.Errors.ErrSubProcess { - return false, gui.createErrorPanel(gui.g, err.Error()) - } - } - if sub != nil { - gui.SubProcess = sub - return false, gui.Errors.ErrSubProcess - } - return true, nil -} - func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error { return gui.createPromptPanel(g, v, gui.Tr.CustomCommandTitle, func(g *gocui.Gui, v *gocui.View) error { command := gui.trimmedContent(v) - gui.SubProcess = gui.OSCommand.RunCustomCommand(command) - return gui.Errors.ErrSubProcess + return gui.runSubprocess(gui.OSCommand.RunCustomCommand(command)) }) } diff --git a/pkg/gui/project_panel.go b/pkg/gui/project_panel.go index 743c6825..3dd14175 100644 --- a/pkg/gui/project_panel.go +++ b/pkg/gui/project_panel.go @@ -224,6 +224,5 @@ func (gui *Gui) handleViewAllLogs(g *gocui.Gui, v *gocui.View) error { return gui.createErrorPanel(gui.g, err.Error()) } - gui.SubProcess = c - return gui.Errors.ErrSubProcess + return gui.runSubprocess(c) } diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index d290911e..09a4dafb 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -293,8 +293,7 @@ func (gui *Gui) handleServiceAttach(g *gocui.Gui, v *gocui.View) error { return gui.createErrorPanel(gui.g, err.Error()) } - gui.SubProcess = c - return gui.Errors.ErrSubProcess + return gui.runSubprocess(c) } func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error { @@ -308,8 +307,7 @@ func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error return gui.createErrorPanel(gui.g, err.Error()) } - gui.SubProcess = c - return gui.Errors.ErrSubProcess + return gui.runSubprocess(c) } func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error { @@ -366,8 +364,7 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error { gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}), ), f: func() error { - gui.SubProcess = gui.OSCommand.RunCustomCommand(rebuildCommand) - return gui.Errors.ErrSubProcess + return gui.runSubprocess(gui.OSCommand.RunCustomCommand(rebuildCommand)) }, }, { diff --git a/pkg/gui/subprocess.go b/pkg/gui/subprocess.go index 059f05e1..6d114bc8 100644 --- a/pkg/gui/subprocess.go +++ b/pkg/gui/subprocess.go @@ -4,48 +4,43 @@ import ( "fmt" "io/ioutil" "os" + "os/exec" "os/signal" "strings" "github.com/fatih/color" - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazydocker/pkg/utils" ) -// RunWithSubprocesses loops, instantiating a new gocui.Gui with each iteration -// if the error returned from a run is a ErrSubProcess, it runs the subprocess -// otherwise it handles the error, possibly by quitting the application -func (gui *Gui) RunWithSubprocesses() error { - for { - if err := gui.Run(); err != nil { - if err == gocui.ErrQuit { - break - } else if err == gui.Errors.ErrSubProcess { - // preparing the state for when we return - gui.pushView(gui.currentViewName()) - // giving goEvery goroutines time to finish - gui.State.SessionIndex++ +func (gui *Gui) runSubprocess(cmd *exec.Cmd) error { + gui.Mutexes.SubprocessMutex.Lock() + defer gui.Mutexes.SubprocessMutex.Unlock() - if err := gui.runCommand(); err != nil { - return err - } - - // pop here so we don't stack up view names - gui.popView() - // ensuring we render e.g. the logs of the currently selected item upon return - gui.State.Panels.Main.ObjectKey = "" - } else { - return err - } - } + if err := gui.g.Suspend(); err != nil { + return gui.createErrorPanel(gui.g, err.Error()) } + + gui.PauseBackgroundThreads = true + + cmdErr := gui.runSubprocess(cmd) + + if err := gui.g.Resume(); err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + + gui.PauseBackgroundThreads = false + + if cmdErr != nil { + return gui.createErrorPanel(gui.g, cmdErr.Error()) + } + return nil } -func (gui *Gui) runCommand() error { - gui.SubProcess.Stdout = os.Stdout - gui.SubProcess.Stderr = os.Stdout - gui.SubProcess.Stdin = os.Stdin +func (gui *Gui) runCommand(cmd *exec.Cmd) error { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stdout + cmd.Stdin = os.Stdin stop := make(chan os.Signal, 1) defer signal.Stop(stop) @@ -54,23 +49,22 @@ func (gui *Gui) runCommand() error { signal.Notify(stop, os.Interrupt) <-stop - if err := gui.OSCommand.Kill(gui.SubProcess); err != nil { + if err := gui.OSCommand.Kill(cmd); err != nil { gui.Log.Error(err) } }() - fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString("+ "+strings.Join(gui.SubProcess.Args, " "), color.FgBlue)) + fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString("+ "+strings.Join(cmd.Args, " "), color.FgBlue)) - if err := gui.SubProcess.Run(); err != nil { + if err := cmd.Run(); err != nil { // not handling the error explicitly because usually we're going to see it // in the output anyway gui.Log.Error(err) } - gui.SubProcess.Stdin = nil - gui.SubProcess.Stdout = ioutil.Discard - gui.SubProcess.Stderr = ioutil.Discard - gui.SubProcess = nil + cmd.Stdin = nil + cmd.Stdout = ioutil.Discard + cmd.Stderr = ioutil.Discard gui.promptToReturn() diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go index 90a75f31..1532eeea 100644 --- a/pkg/i18n/dutch.go +++ b/pkg/i18n/dutch.go @@ -8,7 +8,6 @@ func dutchSet() TranslationSet { StoppingStatus: "stoppen", RunningCustomCommandStatus: "Aangepast commando draaien", - RunningSubprocess: "subprocess draaien", NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement", ErrorOccurred: "Er is iets fout gegaan! Zou je hier een issue aan willen maken: https://github.com/jesseduffield/lazydocker/issues", diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 62d9a1c4..9f225c4d 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -13,7 +13,6 @@ type TranslationSet struct { Scroll string Close string ErrorTitle string - RunningSubprocess string NoViewMachingNewLineFocusedSwitchStatement string OpenConfig string EditConfig string @@ -121,7 +120,6 @@ func englishSet() TranslationSet { RunningCustomCommandStatus: "running custom command", RunningBulkCommandStatus: "running bulk command", - RunningSubprocess: "running subprocess", NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement", ErrorOccurred: "An error occurred! Please create an issue at https://github.com/jesseduffield/lazydocker/issues", diff --git a/pkg/i18n/german.go b/pkg/i18n/german.go index aba4f73c..4a205aa2 100644 --- a/pkg/i18n/german.go +++ b/pkg/i18n/german.go @@ -8,7 +8,6 @@ func germanSet() TranslationSet { StoppingStatus: "anhalten", RunningCustomCommandStatus: "führt benutzerdefinierten Befehl aus", - RunningSubprocess: "Unterprozess ausführen", NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement", ErrorOccurred: "Es ist ein Fehler aufgetreten! Bitte erstelle ein Issue hier: https://github.com/jesseduffield/lazydocker/issues", diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go index 2525d639..2cf12dd0 100644 --- a/pkg/i18n/polish.go +++ b/pkg/i18n/polish.go @@ -8,7 +8,6 @@ func polishSet() TranslationSet { StoppingStatus: "zatrzymywanie", RunningCustomCommandStatus: "uruchamianie własnej komendty", - RunningSubprocess: "uruchamianie podprocesu", NoViewMachingNewLineFocusedSwitchStatement: "Żaden widok nie odpowiada instrukcji przełączenia newLineFocused", ErrorOccurred: "Wystąpił błąd! Proszę go zgłosić na https://github.com/jesseduffield/lazydocker/issues", diff --git a/pkg/i18n/turkish.go b/pkg/i18n/turkish.go index 9b51af22..4a378b7a 100644 --- a/pkg/i18n/turkish.go +++ b/pkg/i18n/turkish.go @@ -8,7 +8,6 @@ func turkishSet() TranslationSet { StoppingStatus: "durduruluyor", RunningCustomCommandStatus: "özel komut çalıştır", - RunningSubprocess: "İkincil işlem yürütülüyor", NoViewMachingNewLineFocusedSwitchStatement: "NewLineFocused anahtar deyimi ile eşleşen görünüm yok", ErrorOccurred: "Bir hata oluştu! Lütfen https://github.com/jesseduffield/lazydocker/issues adresinden bir hataya ilişkin konu oluşturun", From aa8504b8f66267174f240e529ecc3ebee6d0d8ed Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 17:13:43 -0700 Subject: [PATCH 05/10] fix focus issues --- pkg/gui/arrangement.go | 31 +++-------- pkg/gui/confirmation_panel.go | 6 +-- pkg/gui/gui.go | 1 + pkg/gui/layout.go | 11 ++-- pkg/gui/main_panel.go | 4 +- pkg/gui/menu_panel.go | 3 +- pkg/gui/subprocess.go | 2 +- pkg/gui/view_helpers.go | 98 +++++++++++++++-------------------- 8 files changed, 63 insertions(+), 93 deletions(-) diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go index b24c82af..6b0250e5 100644 --- a/pkg/gui/arrangement.go +++ b/pkg/gui/arrangement.go @@ -204,30 +204,15 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { } } -// TODO: reintroduce -// func (gui *Gui) currentSideWindowName() string { -// // there is always one and only one cyclable context in the context stack. We'll look from top to bottom -// gui.State.ContextManager.RLock() -// defer gui.State.ContextManager.RUnlock() - -// for idx := range gui.State.ContextManager.ContextStack { -// reversedIdx := len(gui.State.ContextManager.ContextStack) - 1 - idx -// context := gui.State.ContextManager.ContextStack[reversedIdx] - -// if context.GetKind() == types.SIDE_CONTEXT { -// return context.GetWindowName() -// } -// } - -// return "files" // default -// } - -// TODO: do this better. func (gui *Gui) currentSideWindowName() string { - windowName := gui.currentWindow() - if !lo.Contains(gui.sideViewNames(), windowName) { - return gui.peekPreviousView() + // we expect that there is a side window somewhere in the view stack, so we will search from top to bottom + for idx := range gui.State.ViewStack { + reversedIdx := len(gui.State.ViewStack) - 1 - idx + viewName := gui.State.ViewStack[reversedIdx] + if lo.Contains(gui.sideViewNames(), viewName) { + return viewName + } } - return windowName + return gui.initiallyFocusedViewName() } diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index 4c3b761a..85f3fe85 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -86,7 +86,7 @@ func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt confirmationView.Title = title confirmationView.Visible = true gui.g.Update(func(g *gocui.Gui) error { - return gui.switchFocus(gui.g, currentView, confirmationView, false) + return gui.switchFocus(confirmationView) }) return nil } @@ -96,8 +96,8 @@ func (gui *Gui) onNewPopupPanel() { gui.Views.Confirmation.Visible = false } -// it is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password -// unparam complains that handleClose is alwans nil but one day it won't be nil. +// It is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password. +// The golangcilint unparam linter complains that handleClose is alwans nil but one day it won't be nil. // nolint:unparam func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { return gui.createPopupPanel(g, currentView, title, prompt, false, handleConfirm, handleClose) diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index c664edb0..1b2199a7 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -78,6 +78,7 @@ type Gui struct { type Mutexes struct { SubprocessMutex sync.Mutex + ViewStackMutex sync.Mutex } type servicePanelState struct { diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index 22f5fd89..11e86758 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -117,16 +117,13 @@ func (gui *Gui) layout(g *gocui.Gui) error { } if gui.g.CurrentView() == nil { - v, err := gui.g.View(gui.peekPreviousView()) + viewName := gui.initiallyFocusedViewName() + view, err := gui.g.View(viewName) if err != nil { - viewName := gui.initiallyFocusedViewName() - v, err = gui.g.View(viewName) - if err != nil { - return err - } + return err } - if err := gui.switchFocus(gui.g, nil, v, false); err != nil { + if err := gui.switchFocus(view); err != nil { return err } } diff --git a/pkg/gui/main_panel.go b/pkg/gui/main_panel.go index 6b59ef73..c8f13721 100644 --- a/pkg/gui/main_panel.go +++ b/pkg/gui/main_panel.go @@ -101,7 +101,7 @@ func (gui *Gui) handleEnterMain(g *gocui.Gui, v *gocui.View) error { mainView := gui.getMainView() mainView.ParentView = v - return gui.switchFocus(gui.g, v, mainView, false) + return gui.switchFocus(mainView) } func (gui *Gui) handleExitMain(g *gocui.Gui, v *gocui.View) error { @@ -122,5 +122,5 @@ func (gui *Gui) handleMainClick(g *gocui.Gui, v *gocui.View) error { v.ParentView = currentView } - return gui.switchFocus(gui.g, currentView, v, false) + return gui.switchFocus(v) } diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index adb35818..f6e5e74b 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -100,8 +100,7 @@ func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handl gui.g.Update(func(g *gocui.Gui) error { menuView.Visible = true - currentView := gui.g.CurrentView() - return gui.switchFocus(gui.g, currentView, menuView, false) + return gui.switchFocus(menuView) }) return nil } diff --git a/pkg/gui/subprocess.go b/pkg/gui/subprocess.go index 6d114bc8..54af269d 100644 --- a/pkg/gui/subprocess.go +++ b/pkg/gui/subprocess.go @@ -22,7 +22,7 @@ func (gui *Gui) runSubprocess(cmd *exec.Cmd) error { gui.PauseBackgroundThreads = true - cmdErr := gui.runSubprocess(cmd) + cmdErr := gui.runCommand(cmd) if err := gui.g.Resume(); err != nil { return gui.createErrorPanel(gui.g, err.Error()) diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index a585d807..2ab39f19 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -33,8 +33,7 @@ func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error { panic(err) } gui.resetMainView() - gui.popView() - return gui.switchFocus(g, v, focusedView, false) + return gui.switchFocus(focusedView) } func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error { @@ -59,8 +58,7 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error { panic(err) } gui.resetMainView() - gui.popView() - return gui.switchFocus(g, v, focusedView, false) + return gui.switchFocus(focusedView) } func (gui *Gui) resetMainView() { @@ -96,24 +94,48 @@ func (gui *Gui) newLineFocused(v *gocui.View) error { } } -func (gui *Gui) popView() string { - if len(gui.State.ViewStack) > 0 { - value := gui.State.ViewStack[len(gui.State.ViewStack)-1] - gui.State.ViewStack = gui.State.ViewStack[:len(gui.State.ViewStack)-1] - return value - } +// TODO: move some of this logic into our onFocusLost and onFocus hooks +func (gui *Gui) switchFocus(newView *gocui.View) error { + gui.Mutexes.ViewStackMutex.Lock() + defer gui.Mutexes.ViewStackMutex.Unlock() - return "" + return gui.switchFocusAux(newView) } -func (gui *Gui) peekPreviousView() string { - if len(gui.State.ViewStack) > 0 { - return gui.State.ViewStack[len(gui.State.ViewStack)-1] +func (gui *Gui) switchFocusAux(newView *gocui.View) error { + gui.pushView(newView.Name()) + gui.Log.Info("setting highlight to true for view " + newView.Name()) + gui.Log.Info("new focused view is " + newView.Name()) + if _, err := gui.g.SetCurrentView(newView.Name()); err != nil { + return err } - return "" + gui.g.Cursor = newView.Editable + + if err := gui.renderPanelOptions(); err != nil { + return err + } + + return gui.newLineFocused(newView) } +func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error { + gui.Mutexes.ViewStackMutex.Lock() + defer gui.Mutexes.ViewStackMutex.Unlock() + + if len(gui.State.ViewStack) <= 1 { + return nil + } + + previousViewName := gui.State.ViewStack[len(gui.State.ViewStack)-2] + previousView, err := g.View(previousViewName) + if err != nil { + return err + } + return gui.switchFocusAux(previousView) +} + +// Not to be called directly. Use `switchFocus` instead func (gui *Gui) pushView(name string) { // No matter what view we're pushing, we first remove all popup panels from the stack gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool { @@ -122,51 +144,17 @@ func (gui *Gui) pushView(name string) { // If we're pushing a side panel, we remove all other panels if lo.Contains(gui.sideViewNames(), name) { - gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool { - return viewName != "main" - }) + gui.State.ViewStack = []string{} } + // If we're pushing a panel that's already in the stack, we remove it + gui.State.ViewStack = lo.Filter(gui.State.ViewStack, func(viewName string, _ int) bool { + return viewName != name + }) + gui.State.ViewStack = append(gui.State.ViewStack, name) } -func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error { - previousViewName := gui.popView() - previousView, err := g.View(previousViewName) - if err != nil { - // always fall back to services view if there's no 'previous' view stored - previousView, err = g.View(gui.initiallyFocusedViewName()) - if err != nil { - gui.Log.Error(err) - } - } - return gui.switchFocus(g, v, previousView, true) -} - -// pass in oldView = nil if you don't want to be able to return to your old view -// TODO: move some of this logic into our onFocusLost and onFocus hooks -func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View, returning bool) error { - // we assume we'll never want to return focus to a popup panel i.e. - // we should never stack popup panels - if oldView != nil && !gui.isPopupPanel(oldView.Name()) && !returning { - gui.pushView(oldView.Name()) - } - - gui.Log.Info("setting highlight to true for view " + newView.Name()) - gui.Log.Info("new focused view is " + newView.Name()) - if _, err := g.SetCurrentView(newView.Name()); err != nil { - return err - } - - g.Cursor = newView.Editable - - if err := gui.renderPanelOptions(); err != nil { - return err - } - - return gui.newLineFocused(newView) -} - // if the cursor down past the last item, move it to the last line // nolint:unparam func (gui *Gui) focusPoint(selectedX int, selectedY int, lineCount int, v *gocui.View) { From 540c592b0459dcc263d00f9b2fef1167afe3eead Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 17:48:42 -0700 Subject: [PATCH 06/10] prevent issue where opening popup in maximised main view shows popup in front of a side view --- pkg/gui/arrangement.go | 2 +- pkg/gui/confirmation_panel.go | 18 +++++++++--------- pkg/gui/containers_panel.go | 10 +++++----- pkg/gui/gui.go | 2 +- pkg/gui/images_panel.go | 4 ++-- pkg/gui/main_panel.go | 2 +- pkg/gui/menu_panel.go | 12 +++++++++--- pkg/gui/options_menu_panel.go | 5 +---- pkg/gui/services_panel.go | 2 +- pkg/gui/view_helpers.go | 19 +++++++++++++++++-- pkg/gui/views.go | 4 ++++ pkg/gui/volumes_panel.go | 2 +- pkg/gui/window.go | 5 +++++ 13 files changed, 57 insertions(+), 30 deletions(-) diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go index 6b0250e5..df717341 100644 --- a/pkg/gui/arrangement.go +++ b/pkg/gui/arrangement.go @@ -75,7 +75,7 @@ func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V { } func (gui *Gui) getMidSectionWeights() (int, int) { - currentWindow := gui.currentWindow() + currentWindow := gui.currentStaticWindowName() // we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4 sidePanelWidthRatio := gui.Config.UserConfig.Gui.SidePanelWidth diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index 85f3fe85..c515fd54 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -30,7 +30,7 @@ func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.Vie } func (gui *Gui) closeConfirmationPrompt() error { - if err := gui.returnFocus(gui.g, gui.Views.Confirmation); err != nil { + if err := gui.returnFocus(); err != nil { return err } gui.g.DeleteViewKeybindings("confirmation") @@ -64,7 +64,7 @@ func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt s func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error { gui.onNewPopupPanel() - err := gui.prepareConfirmationPanel(currentView, title, "", false) + err := gui.prepareConfirmationPanel(title, "", false) if err != nil { return err } @@ -72,7 +72,7 @@ func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title s return gui.setKeyBindings(g, handleConfirm, nil) } -func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string, hasLoader bool) error { +func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool) error { x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, true, prompt) confirmationView := gui.Views.Confirmation _, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0) @@ -99,19 +99,19 @@ func (gui *Gui) onNewPopupPanel() { // It is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password. // The golangcilint unparam linter complains that handleClose is alwans nil but one day it won't be nil. // nolint:unparam -func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { - return gui.createPopupPanel(g, currentView, title, prompt, false, handleConfirm, handleClose) +func (gui *Gui) createConfirmationPanel(title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { + return gui.createPopupPanel(title, prompt, false, handleConfirm, handleClose) } -func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, hasLoader bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { +func (gui *Gui) createPopupPanel(title, prompt string, hasLoader bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { gui.onNewPopupPanel() - g.Update(func(g *gocui.Gui) error { + gui.g.Update(func(g *gocui.Gui) error { if gui.currentViewName() == "confirmation" { if err := gui.closeConfirmationPrompt(); err != nil { gui.Log.Error(err.Error()) } } - err := gui.prepareConfirmationPanel(currentView, title, prompt, hasLoader) + err := gui.prepareConfirmationPanel(title, prompt, hasLoader) if err != nil { return err } @@ -146,7 +146,7 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error { colorFunction := color.New(color.FgRed).SprintFunc() coloredMessage := colorFunction(strings.TrimSpace(message)) - return gui.createConfirmationPanel(gui.g, g.CurrentView(), gui.Tr.ErrorTitle, coloredMessage, nil, nil) + return gui.createConfirmationPanel(gui.Tr.ErrorTitle, coloredMessage, nil, nil) } func (gui *Gui) renderConfirmationOptions() error { diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index e4309e72..3dc9bd2f 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -408,7 +408,7 @@ func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error { if err := container.Remove(configOptions); err != nil { if commands.HasErrorCode(err, commands.MustStopContainer) { - return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.MustForceToRemoveContainer, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.MustForceToRemoveContainer, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error { configOptions.Force = true return container.Remove(configOptions) @@ -455,7 +455,7 @@ func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error { return nil } - return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.StopContainer, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopContainer, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error { if err := container.Stop(); err != nil { return gui.createErrorPanel(gui.g, err.Error()) @@ -496,7 +496,7 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handlePruneContainers() error { - return gui.createConfirmationPanel(gui.g, gui.getContainersView(), gui.Tr.Confirm, gui.Tr.ConfirmPruneContainers, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneContainers, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error { err := gui.DockerCommand.PruneContainers() if err != nil { @@ -555,7 +555,7 @@ func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error } func (gui *Gui) handleStopContainers() error { - return gui.createConfirmationPanel(gui.g, gui.getContainersView(), gui.Tr.Confirm, gui.Tr.ConfirmStopContainers, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmStopContainers, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error { for _, container := range gui.DockerCommand.Containers { _ = container.Stop() @@ -567,7 +567,7 @@ func (gui *Gui) handleStopContainers() error { } func (gui *Gui) handleRemoveContainers() error { - return gui.createConfirmationPanel(gui.g, gui.getContainersView(), gui.Tr.Confirm, gui.Tr.ConfirmRemoveContainers, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmRemoveContainers, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error { for _, container := range gui.DockerCommand.Containers { _ = container.Remove(types.ContainerRemoveOptions{Force: true}) diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 1b2199a7..94aa4c0d 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -385,7 +385,7 @@ func (gui *Gui) reRenderMain() error { func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error { if gui.Config.UserConfig.ConfirmOnQuit { - return gui.createConfirmationPanel(g, v, "", gui.Tr.ConfirmQuit, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel("", gui.Tr.ConfirmQuit, func(g *gocui.Gui, v *gocui.View) error { return gocui.ErrQuit }, nil) } diff --git a/pkg/gui/images_panel.go b/pkg/gui/images_panel.go index da69c93f..2f513ab5 100644 --- a/pkg/gui/images_panel.go +++ b/pkg/gui/images_panel.go @@ -116,7 +116,7 @@ func (gui *Gui) refreshImages() error { gui.g.Update(func(g *gocui.Gui) error { ImagesView.Clear() - isFocused := gui.g.CurrentView().Name() == "Images" + isFocused := gui.g.CurrentView() == gui.Views.Images list, err := utils.RenderList(gui.DockerCommand.Images, utils.IsFocused(isFocused)) if err != nil { return err @@ -260,7 +260,7 @@ func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handlePruneImages() error { - return gui.createConfirmationPanel(gui.g, gui.getImagesView(), gui.Tr.Confirm, gui.Tr.ConfirmPruneImages, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneImages, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error { err := gui.DockerCommand.PruneImages() if err != nil { diff --git a/pkg/gui/main_panel.go b/pkg/gui/main_panel.go index c8f13721..e8675df4 100644 --- a/pkg/gui/main_panel.go +++ b/pkg/gui/main_panel.go @@ -106,7 +106,7 @@ func (gui *Gui) handleEnterMain(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleExitMain(g *gocui.Gui, v *gocui.View) error { v.ParentView = nil - return gui.returnFocus(gui.g, v) + return gui.returnFocus() } func (gui *Gui) handleMainClick(g *gocui.Gui, v *gocui.View) error { diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index f6e5e74b..66e0d316 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -58,7 +58,7 @@ func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error { } } gui.Views.Menu.Visible = false - return gui.returnFocus(g, v) + return gui.returnFocus() } func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handlePress func(int) error) error { @@ -80,12 +80,18 @@ func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handl wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error { selectedLine := gui.State.Panels.Menu.SelectedLine + + menuView.Visible = false + err := gui.returnFocus() + if err != nil { + return err + } + if err := handlePress(selectedLine); err != nil { return err } - menuView.Visible = false - return gui.returnFocus(gui.g, menuView) + return nil } gui.State.Panels.Menu.OnPress = wrappedHandlePress diff --git a/pkg/gui/options_menu_panel.go b/pkg/gui/options_menu_panel.go index 24dd9681..c5a4c2b6 100644 --- a/pkg/gui/options_menu_panel.go +++ b/pkg/gui/options_menu_panel.go @@ -64,10 +64,7 @@ func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error { if index >= len(bindings) { return errors.New("Index is greater than size of bindings") } - err := gui.handleMenuClose(g, v) - if err != nil { - return err - } + return bindings[index].Handler(g, v) } diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index 09a4dafb..16accf28 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -237,7 +237,7 @@ func (gui *Gui) handleServiceStop(g *gocui.Gui, v *gocui.View) error { return nil } - return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.StopService, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopService, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error { if err := service.Stop(); err != nil { return gui.createErrorPanel(gui.g, err.Error()) diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 2ab39f19..740ccbc1 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -119,7 +119,7 @@ func (gui *Gui) switchFocusAux(newView *gocui.View) error { return gui.newLineFocused(newView) } -func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error { +func (gui *Gui) returnFocus() error { gui.Mutexes.ViewStackMutex.Lock() defer gui.Mutexes.ViewStackMutex.Unlock() @@ -128,7 +128,7 @@ func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error { } previousViewName := gui.State.ViewStack[len(gui.State.ViewStack)-2] - previousView, err := g.View(previousViewName) + previousView, err := gui.g.View(previousViewName) if err != nil { return err } @@ -153,6 +153,21 @@ func (gui *Gui) pushView(name string) { }) gui.State.ViewStack = append(gui.State.ViewStack, name) + gui.Log.Warnf("Just pushed %s. View stack is now "+strings.Join(gui.State.ViewStack, ", "), name) +} + +// excludes popups +func (gui *Gui) currentStaticViewName() string { + gui.Mutexes.ViewStackMutex.Lock() + defer gui.Mutexes.ViewStackMutex.Unlock() + + for i := len(gui.State.ViewStack) - 1; i >= 0; i-- { + if !lo.Contains(gui.popupViewNames(), gui.State.ViewStack[i]) { + return gui.State.ViewStack[i] + } + } + + return gui.initiallyFocusedViewName() } // if the cursor down past the last item, move it to the last line diff --git a/pkg/gui/views.go b/pkg/gui/views.go index 3de54c26..6a7ed3e6 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -127,3 +127,7 @@ func (gui *Gui) getInformationContent() string { donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.Donate) return donate + " " + informationStr } + +func (gui *Gui) popupViewNames() []string { + return []string{"confirmation", "menu"} +} diff --git a/pkg/gui/volumes_panel.go b/pkg/gui/volumes_panel.go index 6d69e95a..d052856d 100644 --- a/pkg/gui/volumes_panel.go +++ b/pkg/gui/volumes_panel.go @@ -239,7 +239,7 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handlePruneVolumes() error { - return gui.createConfirmationPanel(gui.g, gui.getVolumesView(), gui.Tr.Confirm, gui.Tr.ConfirmPruneVolumes, func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmPruneVolumes, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error { err := gui.DockerCommand.PruneVolumes() if err != nil { diff --git a/pkg/gui/window.go b/pkg/gui/window.go index 42c12dd5..f596a2b2 100644 --- a/pkg/gui/window.go +++ b/pkg/gui/window.go @@ -5,3 +5,8 @@ func (gui *Gui) currentWindow() string { // are using the view name as the window name return gui.currentViewName() } + +// excludes popups +func (gui *Gui) currentStaticWindowName() string { + return gui.currentStaticViewName() +} From 5988ec574597daab68eaeb8db070b36bbf55e748 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 18:01:38 -0700 Subject: [PATCH 07/10] display ports against container and service --- pkg/commands/container.go | 11 ++++++++++- pkg/commands/service.go | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pkg/commands/container.go b/pkg/commands/container.go index 9b56664d..2cb60854 100644 --- a/pkg/commands/container.go +++ b/pkg/commands/container.go @@ -9,6 +9,7 @@ import ( "sync" "github.com/docker/docker/api/types/container" + "github.com/samber/lo" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" @@ -51,7 +52,15 @@ type Container struct { func (c *Container) GetDisplayStrings(isFocused bool) []string { image := strings.TrimPrefix(c.Container.Image, "sha256:") - return []string{c.GetDisplayStatus(), c.GetDisplaySubstatus(), c.Name, c.GetDisplayCPUPerc(), utils.ColoredString(image, color.FgMagenta)} + return []string{c.GetDisplayStatus(), c.GetDisplaySubstatus(), c.Name, c.GetDisplayCPUPerc(), utils.ColoredString(image, color.FgMagenta), c.displayPorts()} +} + +func (c *Container) displayPorts() string { + portStrings := lo.Map(c.Container.Ports, func(port types.Port, _ int) string { + return fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type) + }) + + return strings.Join(portStrings, ", ") } // GetDisplayStatus returns the colored status of the container diff --git a/pkg/commands/service.go b/pkg/commands/service.go index 269e119f..f45146bc 100644 --- a/pkg/commands/service.go +++ b/pkg/commands/service.go @@ -24,11 +24,11 @@ type Service struct { // GetDisplayStrings returns the dispaly string of Container func (s *Service) GetDisplayStrings(isFocused bool) []string { if s.Container == nil { - return []string{utils.ColoredString("none", color.FgBlue), "", s.Name, ""} + return []string{utils.ColoredString("none", color.FgBlue), "", s.Name, "", ""} } cont := s.Container - return []string{cont.GetDisplayStatus(), cont.GetDisplaySubstatus(), s.Name, cont.GetDisplayCPUPerc()} + return []string{cont.GetDisplayStatus(), cont.GetDisplaySubstatus(), s.Name, cont.GetDisplayCPUPerc(), utils.ColoredString(cont.displayPorts(), color.FgYellow)} } // Remove removes the service's containers From a9d1694853e2e354e2c1e863630552d2a0052fa9 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 18:10:45 -0700 Subject: [PATCH 08/10] more concise showing of ports --- pkg/commands/container.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/commands/container.go b/pkg/commands/container.go index 2cb60854..c239640d 100644 --- a/pkg/commands/container.go +++ b/pkg/commands/container.go @@ -57,7 +57,14 @@ func (c *Container) GetDisplayStrings(isFocused bool) []string { func (c *Container) displayPorts() string { portStrings := lo.Map(c.Container.Ports, func(port types.Port, _ int) string { - return fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type) + // docker ps will show '0.0.0.0:80->80/tcp' but we'll show + // '80->80/tcp' instead to save space (unless the IP is something other than + // 0.0.0.0) + ipString := "" + if port.IP != "0.0.0.0" { + ipString = port.IP + ":" + } + return fmt.Sprintf("%s%d->%d/%s", ipString, port.PublicPort, port.PrivatePort, port.Type) }) return strings.Join(portStrings, ", ") From 46de6b365d2a2f79912062d8e641751d21e7e0bd Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 18:21:33 -0700 Subject: [PATCH 09/10] clean up some things remove redundant g argument --- docs/Config.md | 8 ++++++++ pkg/config/app_config.go | 3 ++- pkg/gui/app_status_manager.go | 2 +- pkg/gui/arrangement.go | 24 ------------------------ pkg/gui/confirmation_panel.go | 6 +++--- pkg/gui/containers_panel.go | 14 +++++++------- pkg/gui/custom_commands.go | 2 +- pkg/gui/gui.go | 8 ++++---- pkg/gui/images_panel.go | 4 ++-- pkg/gui/layout.go | 2 +- pkg/gui/project_panel.go | 2 +- pkg/gui/services_panel.go | 22 +++++++++++----------- pkg/gui/subprocess.go | 14 ++++---------- pkg/gui/view_helpers.go | 17 ++++++++++++++++- pkg/gui/views.go | 12 +++++------- pkg/gui/volumes_panel.go | 4 ++-- pkg/gui/window.go | 14 +++++++++----- 17 files changed, 77 insertions(+), 81 deletions(-) diff --git a/docs/Config.md b/docs/Config.md index 92a8a3ed..fecb7d3c 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -28,6 +28,14 @@ gui: - blue returnImmediately: false wrapMainPanel: true + # Side panel width as a ratio of the screen's width + sidePanelWidth: 0.333 + # Determines whether we show the bottom line (the one containing keybinding + # info and the status of the app). + showBottomLine: true + # When true, increases vertical space used by focused side panel, + # creating an accordion effect + expandFocusedSidePanel: false logs: timestamps: false since: '60m' diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index 9094104f..78064067 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -122,7 +122,8 @@ type GuiConfig struct { // info and the status of the app). ShowBottomLine bool `yaml:"showBottomLine"` - // When true, ensures that the focused side panel always takes up the full vertical space + // When true, increases vertical space used by focused side panel, + // creating an accordion effect ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"` } diff --git a/pkg/gui/app_status_manager.go b/pkg/gui/app_status_manager.go index 20a81120..f806f72c 100644 --- a/pkg/gui/app_status_manager.go +++ b/pkg/gui/app_status_manager.go @@ -73,7 +73,7 @@ func (gui *Gui) WithWaitingStatus(name string, f func() error) error { if err := f(); err != nil { gui.g.Update(func(g *gocui.Gui) error { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) }) } }() diff --git a/pkg/gui/arrangement.go b/pkg/gui/arrangement.go index df717341..f507685b 100644 --- a/pkg/gui/arrangement.go +++ b/pkg/gui/arrangement.go @@ -63,17 +63,6 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map return boxlayout.ArrangeWindows(root, 0, 0, width, height) } -func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V { - result := map[K]V{} - for _, currMap := range maps { - for key, value := range currMap { - result[key] = value - } - } - - return result -} - func (gui *Gui) getMidSectionWeights() (int, int) { currentWindow := gui.currentStaticWindowName() @@ -203,16 +192,3 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { }) } } - -func (gui *Gui) currentSideWindowName() string { - // we expect that there is a side window somewhere in the view stack, so we will search from top to bottom - for idx := range gui.State.ViewStack { - reversedIdx := len(gui.State.ViewStack) - 1 - idx - viewName := gui.State.ViewStack[reversedIdx] - if lo.Contains(gui.sideViewNames(), viewName) { - return viewName - } - } - - return gui.initiallyFocusedViewName() -} diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index c515fd54..f9e1ec74 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -62,14 +62,14 @@ func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt s height/2 + panelHeight/2 } -func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error { +func (gui *Gui) createPromptPanel(title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error { gui.onNewPopupPanel() err := gui.prepareConfirmationPanel(title, "", false) if err != nil { return err } gui.Views.Confirmation.Editable = true - return gui.setKeyBindings(g, handleConfirm, nil) + return gui.setKeyBindings(gui.g, handleConfirm, nil) } func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool) error { @@ -143,7 +143,7 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go return nil } -func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error { +func (gui *Gui) createErrorPanel(message string) error { colorFunction := color.New(color.FgRed).SprintFunc() coloredMessage := colorFunction(strings.TrimSpace(message)) return gui.createConfirmationPanel(gui.Tr.ErrorTitle, coloredMessage, nil, nil) diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 3dc9bd2f..4f6bb941 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -197,7 +197,7 @@ func (gui *Gui) renderContainerStats(container *commands.Container) error { contents, err := container.RenderStats(width) if err != nil { - _ = gui.createErrorPanel(gui.g, err.Error()) + _ = gui.createErrorPanel(err.Error()) } gui.reRenderStringMain(contents) @@ -415,7 +415,7 @@ func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error { }) }, nil) } - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil }) @@ -433,7 +433,7 @@ func (gui *Gui) PauseContainer(container *commands.Container) error { } if err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return gui.refreshContainersAndServices() @@ -458,7 +458,7 @@ func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopContainer, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error { if err := container.Stop(); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil @@ -474,7 +474,7 @@ func (gui *Gui) handleContainerRestart(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { if err := container.Restart(); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil @@ -489,7 +489,7 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error { c, err := container.Attach() if err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return gui.runSubprocess(c) @@ -500,7 +500,7 @@ func (gui *Gui) handlePruneContainers() error { return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error { err := gui.DockerCommand.PruneContainers() if err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil }) diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index da03eed9..6038b6b2 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -58,7 +58,7 @@ func (gui *Gui) createCommandMenu(customCommands []config.CustomCommand, command return gui.WithWaitingStatus(waitingStatus, func() error { if err := gui.OSCommand.RunCommand(option.command); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil }) diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 94aa4c0d..69245453 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -269,7 +269,7 @@ func (gui *Gui) Run() error { gui.Log.Warn(err) continue } - _ = gui.createErrorPanel(gui.g, err.Error()) + _ = gui.createErrorPanel(err.Error()) } }() @@ -407,7 +407,7 @@ func (gui *Gui) handleDonate(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) editFile(filename string) error { cmd, err := gui.OSCommand.EditFile(filename) if err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return gui.runSubprocess(cmd) @@ -415,13 +415,13 @@ func (gui *Gui) editFile(filename string) error { func (gui *Gui) openFile(filename string) error { if err := gui.OSCommand.OpenFile(filename); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil } func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error { - return gui.createPromptPanel(g, v, gui.Tr.CustomCommandTitle, func(g *gocui.Gui, v *gocui.View) error { + return gui.createPromptPanel(gui.Tr.CustomCommandTitle, func(g *gocui.Gui, v *gocui.View) error { command := gui.trimmedContent(v) return gui.runSubprocess(gui.OSCommand.RunCustomCommand(command)) }) diff --git a/pkg/gui/images_panel.go b/pkg/gui/images_panel.go index 2f513ab5..9e2bd8cb 100644 --- a/pkg/gui/images_panel.go +++ b/pkg/gui/images_panel.go @@ -250,7 +250,7 @@ func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error { } configOptions := options[index].configOptions if cerr := Image.Remove(configOptions); cerr != nil { - return gui.createErrorPanel(gui.g, cerr.Error()) + return gui.createErrorPanel(cerr.Error()) } return nil @@ -264,7 +264,7 @@ func (gui *Gui) handlePruneImages() error { return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error { err := gui.DockerCommand.PruneImages() if err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return gui.refreshImages() }) diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index 11e86758..134076ce 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -109,7 +109,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { return view, err } - for _, viewName := range []string{"project", "services", "containers", "images", "volumes", "options", "information", "appStatus", "main", "limit"} { + for _, viewName := range gui.controlledBoundsViewNames() { _, err := setViewFromDimensions(viewName, viewName) if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG { return err diff --git a/pkg/gui/project_panel.go b/pkg/gui/project_panel.go index 3dd14175..0ae98e9d 100644 --- a/pkg/gui/project_panel.go +++ b/pkg/gui/project_panel.go @@ -221,7 +221,7 @@ func (gui *Gui) handleProjectPrevContext(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleViewAllLogs(g *gocui.Gui, v *gocui.View) error { c, err := gui.DockerCommand.ViewAllLogs() if err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return gui.runSubprocess(c) diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index 16accf28..76d0116b 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -240,7 +240,7 @@ func (gui *Gui) handleServiceStop(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.StopService, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error { if err := service.Stop(); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil @@ -256,7 +256,7 @@ func (gui *Gui) handleServiceRestart(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { if err := service.Restart(); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil @@ -271,7 +271,7 @@ func (gui *Gui) handleServiceStart(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.StartingStatus, func() error { if err := service.Start(); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil @@ -285,12 +285,12 @@ func (gui *Gui) handleServiceAttach(g *gocui.Gui, v *gocui.View) error { } if service.Container == nil { - return gui.createErrorPanel(gui.g, gui.Tr.NoContainers) + return gui.createErrorPanel(gui.Tr.NoContainers) } c, err := service.Attach() if err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return gui.runSubprocess(c) @@ -304,7 +304,7 @@ func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error c, err := service.ViewLogs() if err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return gui.runSubprocess(c) @@ -336,7 +336,7 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error { f: func() error { return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { if err := service.Restart(); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil }) @@ -351,7 +351,7 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error { f: func() error { return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { if err := gui.OSCommand.RunCommand(recreateCommand); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil }) @@ -385,7 +385,7 @@ func (gui *Gui) createServiceCommandMenu(options []*commandOption, status string } return gui.WithWaitingStatus(status, func() error { if err := gui.OSCommand.RunCommand(options[index].command); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil @@ -447,7 +447,7 @@ func (gui *Gui) handleServicesExecShell(g *gocui.Gui, v *gocui.View) error { container := service.Container if container == nil { - return gui.createErrorPanel(gui.g, gui.Tr.NoContainers) + return gui.createErrorPanel(gui.Tr.NoContainers) } return gui.containerExecShell(container) @@ -461,7 +461,7 @@ func (gui *Gui) handleServicesOpenInBrowserCommand(g *gocui.Gui, v *gocui.View) container := service.Container if container == nil { - return gui.createErrorPanel(gui.g, gui.Tr.NoContainers) + return gui.createErrorPanel(gui.Tr.NoContainers) } return gui.openContainerInBrowser(container) diff --git a/pkg/gui/subprocess.go b/pkg/gui/subprocess.go index 54af269d..8366393c 100644 --- a/pkg/gui/subprocess.go +++ b/pkg/gui/subprocess.go @@ -17,27 +17,23 @@ func (gui *Gui) runSubprocess(cmd *exec.Cmd) error { defer gui.Mutexes.SubprocessMutex.Unlock() if err := gui.g.Suspend(); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } gui.PauseBackgroundThreads = true - cmdErr := gui.runCommand(cmd) + gui.runCommand(cmd) if err := gui.g.Resume(); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } gui.PauseBackgroundThreads = false - if cmdErr != nil { - return gui.createErrorPanel(gui.g, cmdErr.Error()) - } - return nil } -func (gui *Gui) runCommand(cmd *exec.Cmd) error { +func (gui *Gui) runCommand(cmd *exec.Cmd) { cmd.Stdout = os.Stdout cmd.Stderr = os.Stdout cmd.Stdin = os.Stdin @@ -67,6 +63,4 @@ func (gui *Gui) runCommand(cmd *exec.Cmd) error { cmd.Stderr = ioutil.Discard gui.promptToReturn() - - return nil } diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 740ccbc1..000118d3 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -153,7 +153,6 @@ func (gui *Gui) pushView(name string) { }) gui.State.ViewStack = append(gui.State.ViewStack, name) - gui.Log.Warnf("Just pushed %s. View stack is now "+strings.Join(gui.State.ViewStack, ", "), name) } // excludes popups @@ -170,6 +169,22 @@ func (gui *Gui) currentStaticViewName() string { return gui.initiallyFocusedViewName() } +func (gui *Gui) currentSideViewName() string { + gui.Mutexes.ViewStackMutex.Lock() + defer gui.Mutexes.ViewStackMutex.Unlock() + + // we expect that there is a side window somewhere in the view stack, so we will search from top to bottom + for idx := range gui.State.ViewStack { + reversedIdx := len(gui.State.ViewStack) - 1 - idx + viewName := gui.State.ViewStack[reversedIdx] + if lo.Contains(gui.sideViewNames(), viewName) { + return viewName + } + } + + return gui.initiallyFocusedViewName() +} + // if the cursor down past the last item, move it to the last line // nolint:unparam func (gui *Gui) focusPoint(selectedX int, selectedY int, lineCount int, v *gocui.View) { diff --git a/pkg/gui/views.go b/pkg/gui/views.go index 6a7ed3e6..4b568ed0 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -3,7 +3,6 @@ package gui import ( "github.com/fatih/color" "github.com/jesseduffield/gocui" - "github.com/samber/lo" ) type Views struct { @@ -28,12 +27,6 @@ type viewNameMapping struct { name string } -func (gui *Gui) orderedViews() []*gocui.View { - return lo.Map(gui.orderedViewNameMappings(), func(v viewNameMapping, _ int) *gocui.View { - return *v.viewPtr - }) -} - func (gui *Gui) orderedViewNameMappings() []viewNameMapping { return []viewNameMapping{ // first layer. Ordering within this layer does not matter because there are @@ -131,3 +124,8 @@ func (gui *Gui) getInformationContent() string { func (gui *Gui) popupViewNames() []string { return []string{"confirmation", "menu"} } + +// these views have their position and size determined by arrangement.go +func (gui *Gui) controlledBoundsViewNames() []string { + return []string{"project", "services", "containers", "images", "volumes", "options", "information", "appStatus", "main", "limit"} +} diff --git a/pkg/gui/volumes_panel.go b/pkg/gui/volumes_panel.go index d052856d..7d2cf936 100644 --- a/pkg/gui/volumes_panel.go +++ b/pkg/gui/volumes_panel.go @@ -229,7 +229,7 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error { } return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error { if cerr := volume.Remove(options[index].force); cerr != nil { - return gui.createErrorPanel(gui.g, cerr.Error()) + return gui.createErrorPanel(cerr.Error()) } return nil }) @@ -243,7 +243,7 @@ func (gui *Gui) handlePruneVolumes() error { return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error { err := gui.DockerCommand.PruneVolumes() if err != nil { - return gui.createErrorPanel(gui.g, err.Error()) + return gui.createErrorPanel(err.Error()) } return nil }) diff --git a/pkg/gui/window.go b/pkg/gui/window.go index f596a2b2..d58d132f 100644 --- a/pkg/gui/window.go +++ b/pkg/gui/window.go @@ -1,12 +1,16 @@ package gui -func (gui *Gui) currentWindow() string { - // at the moment, we only have one view per window in lazydocker, so we - // are using the view name as the window name - return gui.currentViewName() -} +// func (gui *Gui) currentWindow() string { +// // at the moment, we only have one view per window in lazydocker, so we +// // are using the view name as the window name +// return gui.currentViewName() +// } // excludes popups func (gui *Gui) currentStaticWindowName() string { return gui.currentStaticViewName() } + +func (gui *Gui) currentSideWindowName() string { + return gui.currentSideViewName() +} From a614bd9a1d860be55518b8c3e5945edb4ba5d059 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 9 Oct 2022 18:43:19 -0700 Subject: [PATCH 10/10] show selected line in blue --- pkg/gui/views.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/gui/views.go b/pkg/gui/views.go index 4b568ed0..bf417cbe 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -63,6 +63,8 @@ func (gui *Gui) createAllViews() error { (*mapping.viewPtr).FgColor = gocui.ColorDefault } + selectedLineBgColor := gocui.ColorBlue + gui.Views.Main.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel // when you run a docker container with the -it flags (interactive mode) it adds carriage returns for some reason. This is not docker's fault, it's an os-level default. gui.Views.Main.IgnoreCarriageReturns = true @@ -71,8 +73,10 @@ func (gui *Gui) createAllViews() error { gui.Views.Services.Highlight = true gui.Views.Services.Title = gui.Tr.ServicesTitle + gui.Views.Services.SelBgColor = selectedLineBgColor gui.Views.Containers.Highlight = true + gui.Views.Containers.SelBgColor = selectedLineBgColor if gui.Config.UserConfig.Gui.ShowAllContainers || !gui.DockerCommand.InDockerComposeProject { gui.Views.Containers.Title = gui.Tr.ContainersTitle } else { @@ -81,9 +85,11 @@ func (gui *Gui) createAllViews() error { gui.Views.Images.Highlight = true gui.Views.Images.Title = gui.Tr.ImagesTitle + gui.Views.Images.SelBgColor = selectedLineBgColor gui.Views.Volumes.Highlight = true gui.Views.Volumes.Title = gui.Tr.VolumesTitle + gui.Views.Volumes.SelBgColor = selectedLineBgColor gui.Views.Options.Frame = false gui.Views.Options.FgColor = gui.GetOptionsPanelTextColor() @@ -101,6 +107,7 @@ func (gui *Gui) createAllViews() error { gui.Views.Confirmation.Visible = false gui.Views.Confirmation.Wrap = true gui.Views.Menu.Visible = false + gui.Views.Menu.SelBgColor = selectedLineBgColor gui.Views.Limit.Visible = false gui.Views.Limit.Title = gui.Tr.NotEnoughSpace