Merge pull request #372 from jesseduffield/resizable-windows

This commit is contained in:
Jesse Duffield 2022-10-09 19:07:25 -07:00 committed by GitHub
commit 8670bc06e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 3696 additions and 931 deletions

View file

@ -28,6 +28,14 @@ gui:
- blue - blue
returnImmediately: false returnImmediately: false
wrapMainPanel: true 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: logs:
timestamps: false timestamps: false
since: '60m' since: '60m'

9
go.mod
View file

@ -16,13 +16,15 @@ require (
github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee github.com/jesseduffield/asciigraph v0.0.0-20190605104717-6d88e39309ee
github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6 github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 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/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/mcuadros/go-lookup v0.0.0-20171110082742-5650f26be767
github.com/mgutz/str v1.2.0 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/sirupsen/logrus v1.4.2
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad 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 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/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.4 // indirect github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.11 // 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/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect
github.com/onsi/ginkgo v1.8.0 // 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/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
gopkg.in/yaml.v2 v2.2.2 // 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 gotest.tools/v3 v3.2.0 // indirect
) )

17
go.sum
View file

@ -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/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 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo= 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 h1:33wSxJWU/f2TAozHYtJ8zqBxEnEVYM+22moLoiAkxvg=
github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56/go.mod h1:FZJBwOhE+RXz8EVZfY+xnbCw2cVOwxlK3/aIi581z/s= 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= 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.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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.31.0 h1:Sfa+/064Tdo4SvlohQUQzBhgSer9v/coGvKQI/XLWAM=
github.com/samber/lo v1.20.0/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A= 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.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 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 h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= 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.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.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.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.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/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 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= 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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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-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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I=
gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=

View file

@ -56,8 +56,7 @@ func NewApp(config *config.AppConfig) (*App, error) {
} }
func (app *App) Run() error { func (app *App) Run() error {
err := app.Gui.RunWithSubprocesses() return app.Gui.Run()
return err
} }
func (app *App) Close() error { func (app *App) Close() error {

View file

@ -9,6 +9,7 @@ import (
"sync" "sync"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/samber/lo"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
@ -51,7 +52,22 @@ type Container struct {
func (c *Container) GetDisplayStrings(isFocused bool) []string { func (c *Container) GetDisplayStrings(isFocused bool) []string {
image := strings.TrimPrefix(c.Container.Image, "sha256:") 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 {
// 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, ", ")
} }
// GetDisplayStatus returns the colored status of the container // GetDisplayStatus returns the colored status of the container

View file

@ -24,11 +24,11 @@ type Service struct {
// GetDisplayStrings returns the dispaly string of Container // GetDisplayStrings returns the dispaly string of Container
func (s *Service) GetDisplayStrings(isFocused bool) []string { func (s *Service) GetDisplayStrings(isFocused bool) []string {
if s.Container == nil { 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 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 // Remove removes the service's containers

View file

@ -114,6 +114,17 @@ type GuiConfig struct {
// By default, containers are now sorted by status. This setting allows users to // By default, containers are now sorted by status. This setting allows users to
// use legacy behaviour instead. // use legacy behaviour instead.
LegacySortContainers bool `yaml:"legacySortContainers,omitempty"` 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, increases vertical space used by focused side panel,
// creating an accordion effect
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
} }
// CommandTemplatesConfig determines what commands actually get called when we // CommandTemplatesConfig determines what commands actually get called when we
@ -320,10 +331,13 @@ func GetDefaultConfig() UserConfig {
InactiveBorderColor: []string{"default"}, InactiveBorderColor: []string{"default"},
OptionsTextColor: []string{"blue"}, OptionsTextColor: []string{"blue"},
}, },
ShowAllContainers: false, ShowAllContainers: false,
ReturnImmediately: false, ReturnImmediately: false,
WrapMainPanel: true, WrapMainPanel: true,
LegacySortContainers: false, LegacySortContainers: false,
SidePanelWidth: 0.3333,
ShowBottomLine: true,
ExpandFocusedSidePanel: false,
}, },
ConfirmOnQuit: false, ConfirmOnQuit: false,
Logs: LogsConfig{ Logs: LogsConfig{

View file

@ -73,7 +73,7 @@ func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
if err := f(); err != nil { if err := f(); err != nil {
gui.g.Update(func(g *gocui.Gui) error { gui.g.Update(func(g *gocui.Gui) error {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
}) })
} }
}() }()

194
pkg/gui/arrangement.go Normal file
View file

@ -0,0 +1,194 @@
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 (gui *Gui) getMidSectionWeights() (int, int) {
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
// 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)
})
}
}

View file

@ -15,25 +15,27 @@ import (
func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error { 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 { return func(g *gocui.Gui, v *gocui.View) error {
if err := gui.closeConfirmationPrompt(); err != nil {
return err
}
if function != nil { if function != nil {
if err := function(g, v); err != nil { if err := function(g, v); err != nil {
return err return err
} }
} }
return gui.closeConfirmationPrompt()
return nil
} }
} }
func (gui *Gui) closeConfirmationPrompt() error { func (gui *Gui) closeConfirmationPrompt() error {
view, err := gui.g.View("confirmation") if err := gui.returnFocus(); err != nil {
if err != nil {
return nil // if it's already been closed we can just return
}
if err := gui.returnFocus(gui.g, view); err != nil {
return err return err
} }
gui.g.DeleteViewKeybindings("confirmation") 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 { func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
@ -60,67 +62,60 @@ func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt s
height/2 + panelHeight/2 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() gui.onNewPopupPanel()
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, "", false) err := gui.prepareConfirmationPanel(title, "", false)
if err != nil { if err != nil {
return err return err
} }
confirmationView.Editable = true gui.Views.Confirmation.Editable = true
return gui.setKeyBindings(g, handleConfirm, nil) return gui.setKeyBindings(gui.g, handleConfirm, nil)
} }
func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string, hasLoader bool) (*gocui.View, error) { func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool) error {
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, true, prompt) 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 != nil {
if err.Error() != "unknown view" { return err
return nil, err
}
confirmationView.HasLoader = hasLoader
gui.g.StartTicking()
confirmationView.Title = title
confirmationView.Wrap = true
confirmationView.FgColor = gocui.ColorDefault
} }
confirmationView.HasLoader = hasLoader
if hasLoader {
gui.g.StartTicking()
}
confirmationView.Title = title
confirmationView.Visible = true
gui.g.Update(func(g *gocui.Gui) error { gui.g.Update(func(g *gocui.Gui) error {
return gui.switchFocus(gui.g, currentView, confirmationView, false) return gui.switchFocus(confirmationView)
}) })
return confirmationView, nil return nil
} }
func (gui *Gui) onNewPopupPanel() { func (gui *Gui) onNewPopupPanel() {
viewNames := []string{ gui.Views.Menu.Visible = false
"commitMessage", gui.Views.Confirmation.Visible = false
"credentials",
"menu",
}
for _, viewName := range viewNames {
_, _ = gui.g.SetViewOnBottom(viewName)
}
} }
// 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 // 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. // The golangcilint unparam linter complains that handleClose is alwans nil but one day it won't be nil.
// nolint:unparam // nolint:unparam
func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { func (gui *Gui) createConfirmationPanel(title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
return gui.createPopupPanel(g, currentView, title, prompt, false, handleConfirm, handleClose) 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() gui.onNewPopupPanel()
g.Update(func(g *gocui.Gui) error { gui.g.Update(func(g *gocui.Gui) error {
// delete the existing confirmation panel if it exists if gui.currentViewName() == "confirmation" {
if view, _ := g.View("confirmation"); view != nil {
if err := gui.closeConfirmationPrompt(); err != nil { if err := gui.closeConfirmationPrompt(); err != nil {
gui.Log.Error(err.Error()) gui.Log.Error(err.Error())
} }
} }
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, prompt, hasLoader) err := gui.prepareConfirmationPanel(title, prompt, hasLoader)
if err != nil { if err != nil {
return err return err
} }
confirmationView.Editable = false gui.Views.Confirmation.Editable = false
if err := gui.renderString(g, "confirmation", prompt); err != nil { if err := gui.renderString(g, "confirmation", prompt); err != nil {
return err return err
} }
@ -148,10 +143,10 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go
return nil 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() colorFunction := color.New(color.FgRed).SprintFunc()
coloredMessage := colorFunction(strings.TrimSpace(message)) 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 { func (gui *Gui) renderConfirmationOptions() error {

View file

@ -197,7 +197,7 @@ func (gui *Gui) renderContainerStats(container *commands.Container) error {
contents, err := container.RenderStats(width) contents, err := container.RenderStats(width)
if err != nil { if err != nil {
_ = gui.createErrorPanel(gui.g, err.Error()) _ = gui.createErrorPanel(err.Error())
} }
gui.reRenderStringMain(contents) gui.reRenderStringMain(contents)
@ -408,14 +408,14 @@ func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
if err := container.Remove(configOptions); err != nil { if err := container.Remove(configOptions); err != nil {
if commands.HasErrorCode(err, commands.MustStopContainer) { 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 { return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
configOptions.Force = true configOptions.Force = true
return container.Remove(configOptions) return container.Remove(configOptions)
}) })
}, nil) }, nil)
} }
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
} }
return nil return nil
}) })
@ -433,7 +433,7 @@ func (gui *Gui) PauseContainer(container *commands.Container) error {
} }
if err != nil { if err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
} }
return gui.refreshContainersAndServices() return gui.refreshContainersAndServices()
@ -455,10 +455,10 @@ func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error {
return nil 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 { return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
if err := container.Stop(); err != nil { if err := container.Stop(); err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
} }
return nil 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 { return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
if err := container.Restart(); err != nil { if err := container.Restart(); err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
} }
return nil return nil
@ -489,19 +489,18 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error {
c, err := container.Attach() c, err := container.Attach()
if err != nil { if err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
} }
gui.SubProcess = c return gui.runSubprocess(c)
return gui.Errors.ErrSubProcess
} }
func (gui *Gui) handlePruneContainers() 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 { return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneContainers() err := gui.DockerCommand.PruneContainers()
if err != nil { if err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
} }
return nil return nil
}) })
@ -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) 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 // attach and return the subprocess error
cmd := gui.OSCommand.ExecutableFromString(resolvedCommand) cmd := gui.OSCommand.ExecutableFromString(resolvedCommand)
gui.SubProcess = cmd return gui.runSubprocess(cmd)
return gui.Errors.ErrSubProcess
} }
func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error {
@ -557,7 +555,7 @@ func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error
} }
func (gui *Gui) handleStopContainers() 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 { return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
for _, container := range gui.DockerCommand.Containers { for _, container := range gui.DockerCommand.Containers {
_ = container.Stop() _ = container.Stop()
@ -569,7 +567,7 @@ func (gui *Gui) handleStopContainers() error {
} }
func (gui *Gui) handleRemoveContainers() 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 { return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
for _, container := range gui.DockerCommand.Containers { for _, container := range gui.DockerCommand.Containers {
_ = container.Remove(types.ContainerRemoveOptions{Force: true}) _ = container.Remove(types.ContainerRemoveOptions{Force: true})

View file

@ -53,13 +53,12 @@ func (gui *Gui) createCommandMenu(customCommands []config.CustomCommand, command
// if we have a command for attaching, we attach and return the subprocess error // if we have a command for attaching, we attach and return the subprocess error
if option.customCommand.Attach { if option.customCommand.Attach {
cmd := gui.OSCommand.ExecutableFromString(option.command) cmd := gui.OSCommand.ExecutableFromString(option.command)
gui.SubProcess = cmd return gui.runSubprocess(cmd)
return gui.Errors.ErrSubProcess
} }
return gui.WithWaitingStatus(waitingStatus, func() error { return gui.WithWaitingStatus(waitingStatus, func() error {
if err := gui.OSCommand.RunCommand(option.command); err != nil { if err := gui.OSCommand.RunCommand(option.command); err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
} }
return nil return nil
}) })

View file

@ -2,21 +2,14 @@ package gui
import ( import (
"context" "context"
"os/exec"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/golang-collections/collections/stack"
// "io"
// "io/ioutil"
"github.com/go-errors/errors" "github.com/go-errors/errors"
// "strings"
throttle "github.com/boz/go-throttle" throttle "github.com/boz/go-throttle"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/commands" "github.com/jesseduffield/lazydocker/pkg/commands"
@ -32,7 +25,6 @@ var OverlappingEdges = false
// SentinelErrors are the errors that have special meaning and need to be checked // SentinelErrors are the errors that have special meaning and need to be checked
// by calling functions. The less of these, the better // by calling functions. The less of these, the better
type SentinelErrors struct { type SentinelErrors struct {
ErrSubProcess error
ErrNoContainers error ErrNoContainers error
ErrNoImages error ErrNoImages error
ErrNoVolumes error ErrNoVolumes error
@ -50,7 +42,6 @@ type SentinelErrors struct {
// localising things in the code. // localising things in the code.
func (gui *Gui) GenerateSentinelErrors() { func (gui *Gui) GenerateSentinelErrors() {
gui.Errors = SentinelErrors{ gui.Errors = SentinelErrors{
ErrSubProcess: errors.New(gui.Tr.RunningSubprocess),
ErrNoContainers: errors.New(gui.Tr.NoContainers), ErrNoContainers: errors.New(gui.Tr.NoContainers),
ErrNoImages: errors.New(gui.Tr.NoImages), ErrNoImages: errors.New(gui.Tr.NoImages),
ErrNoVolumes: errors.New(gui.Tr.NoVolumes), ErrNoVolumes: errors.New(gui.Tr.NoVolumes),
@ -63,7 +54,6 @@ type Gui struct {
Log *logrus.Entry Log *logrus.Entry
DockerCommand *commands.DockerCommand DockerCommand *commands.DockerCommand
OSCommand *commands.OSCommand OSCommand *commands.OSCommand
SubProcess *exec.Cmd
State guiState State guiState
Config *config.AppConfig Config *config.AppConfig
Tr *i18n.TranslationSet Tr *i18n.TranslationSet
@ -73,6 +63,22 @@ type Gui struct {
T *tasks.TaskManager T *tasks.TaskManager
ErrorChan chan error ErrorChan chan error
CyclableViews []string 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
ViewStackMutex sync.Mutex
} }
type servicePanelState struct { type servicePanelState struct {
@ -120,19 +126,29 @@ type panelStates struct {
} }
type guiState struct { type guiState struct {
MenuItemCount int // can't store the actual list because it's of interface{} type MenuItemCount int // can't store the actual list because it's of interface{} type
PreviousViews *stack.Stack // the names of views in the current focus stack (last item is the current view)
ViewStack []string
Platform commands.Platform Platform commands.Platform
Panels *panelStates Panels *panelStates
SubProcessOutput string SubProcessOutput string
Stats map[string]commands.ContainerStats Stats map[string]commands.ContainerStats
// SessionIndex tells us how many times we've come back from a subprocess. ScreenMode WindowMaximisation
// 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
} }
// 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 // 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) { func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand *commands.OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*Gui, error) {
initialState := guiState{ initialState := guiState{
@ -148,8 +164,7 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
}, },
Project: &projectState{ContextIndex: 0}, Project: &projectState{ContextIndex: 0},
}, },
SessionIndex: 0, ViewStack: []string{},
PreviousViews: stack.New(),
} }
cyclableViews := []string{"project", "containers", "images", "volumes"} cyclableViews := []string{"project", "containers", "images", "volumes"}
@ -176,13 +191,6 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
return gui, nil return gui, nil
} }
func max(a, b int) int {
if a > b {
return a
}
return b
}
func (gui *Gui) renderGlobalOptions() error { func (gui *Gui) renderGlobalOptions() error {
return gui.renderOptionsMap(map[string]string{ return gui.renderOptionsMap(map[string]string{
"PgUp/PgDn": gui.Tr.Scroll, "PgUp/PgDn": gui.Tr.Scroll,
@ -194,15 +202,15 @@ func (gui *Gui) renderGlobalOptions() error {
} }
func (gui *Gui) goEvery(interval time.Duration, function func() 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 _ = function() // time.Tick doesn't run immediately so we'll do that here // TODO: maybe change
go func() { go func() {
ticker := time.NewTicker(interval) ticker := time.NewTicker(interval)
defer ticker.Stop() defer ticker.Stop()
for range ticker.C { for range ticker.C {
if gui.State.SessionIndex > currentSessionIndex { if gui.PauseBackgroundThreads {
return return
} }
_ = function() _ = function()
} }
}() }()
@ -261,7 +269,7 @@ func (gui *Gui) Run() error {
gui.Log.Warn(err) gui.Log.Warn(err)
continue continue
} }
_ = gui.createErrorPanel(gui.g, err.Error()) _ = gui.createErrorPanel(err.Error())
} }
}() }()
@ -272,6 +280,9 @@ func (gui *Gui) Run() error {
} }
err = g.MainLoop() err = g.MainLoop()
if err == gocui.ErrQuit {
return nil
}
return err return err
} }
@ -374,7 +385,7 @@ func (gui *Gui) reRenderMain() error {
func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
if gui.Config.UserConfig.ConfirmOnQuit { 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 return gocui.ErrQuit
}, nil) }, nil)
} }
@ -394,39 +405,25 @@ func (gui *Gui) handleDonate(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) editFile(filename string) error { func (gui *Gui) editFile(filename string) error {
_, err := gui.runSyncOrAsyncCommand(gui.OSCommand.EditFile(filename)) cmd, err := gui.OSCommand.EditFile(filename)
return err if err != nil {
return gui.createErrorPanel(err.Error())
}
return gui.runSubprocess(cmd)
} }
func (gui *Gui) openFile(filename string) error { func (gui *Gui) openFile(filename string) error {
if err := gui.OSCommand.OpenFile(filename); err != nil { if err := gui.OSCommand.OpenFile(filename); err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
} }
return nil 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 { 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) command := gui.trimmedContent(v)
gui.SubProcess = gui.OSCommand.RunCustomCommand(command) return gui.runSubprocess(gui.OSCommand.RunCustomCommand(command))
return gui.Errors.ErrSubProcess
}) })
} }

View file

@ -116,7 +116,7 @@ func (gui *Gui) refreshImages() error {
gui.g.Update(func(g *gocui.Gui) error { gui.g.Update(func(g *gocui.Gui) error {
ImagesView.Clear() 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)) list, err := utils.RenderList(gui.DockerCommand.Images, utils.IsFocused(isFocused))
if err != nil { if err != nil {
return err return err
@ -250,7 +250,7 @@ func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
} }
configOptions := options[index].configOptions configOptions := options[index].configOptions
if cerr := Image.Remove(configOptions); cerr != nil { if cerr := Image.Remove(configOptions); cerr != nil {
return gui.createErrorPanel(gui.g, cerr.Error()) return gui.createErrorPanel(cerr.Error())
} }
return nil return nil
@ -260,11 +260,11 @@ func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handlePruneImages() 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 { return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneImages() err := gui.DockerCommand.PruneImages()
if err != nil { if err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
} }
return gui.refreshImages() return gui.refreshImages()
}) })

View file

@ -506,6 +506,18 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.scrollRightMain, 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 // TODO: add more views here
@ -573,3 +585,9 @@ func (gui *Gui) keybindings(g *gocui.Gui) error {
return nil return nil
} }
func wrappedHandler(f func() error) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
return f()
}
}

View file

@ -1,11 +1,11 @@
package gui package gui
import ( import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui" "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 // getFocusLayout returns a manager function for when view gain and lose focus
func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error { func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
var previousView *gocui.View var previousView *gocui.View
@ -59,217 +59,71 @@ func (gui *Gui) onFocus(v *gocui.View) {
// layout is called for every screen re-render e.g. when the screen is resized // layout is called for every screen re-render e.g. when the screen is resized
func (gui *Gui) layout(g *gocui.Gui) error { 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 g.Highlight = true
width, height := g.Size() 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() appStatus := gui.statusManager.getStatusString()
appStatusOptionsBoundary := 0
if appStatus != "" {
appStatusOptionsBoundary = len(appStatus) + 2
}
_, _ = g.SetViewOnBottom("limit") viewDimensions := gui.getWindowDimensions(gui.getInformationContent(), appStatus)
_ = g.DeleteView("limit") // 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) view, err := g.View(viewName)
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"])
if err != nil { if err != nil {
if err.Error() != "unknown view" { return nil, err
return err
}
servicesView.Highlight = true
servicesView.Title = gui.Tr.ServicesTitle
servicesView.FgColor = gocui.ColorDefault
} }
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"]) for _, viewName := range gui.controlledBoundsViewNames() {
if err != nil { _, err := setViewFromDimensions(viewName, viewName)
if err.Error() != "unknown view" { if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err 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 { if gui.g.CurrentView() == nil {
v, err := gui.g.View(gui.peekPreviousView()) viewName := gui.initiallyFocusedViewName()
view, err := gui.g.View(viewName)
if err != nil { if err != nil {
viewName := gui.initiallyFocusedViewName() return err
v, err = gui.g.View(viewName)
if err != nil {
return err
}
} }
if err := gui.switchFocus(gui.g, nil, v, false); err != nil { if err := gui.switchFocus(view); err != nil {
return err return err
} }
} }
@ -303,3 +157,9 @@ func (gui *Gui) focusPointInView(view *gocui.View) {
gui.focusY(state.selectedLine, state.lineCount, 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)
}

View file

@ -101,12 +101,12 @@ func (gui *Gui) handleEnterMain(g *gocui.Gui, v *gocui.View) error {
mainView := gui.getMainView() mainView := gui.getMainView()
mainView.ParentView = v 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 { func (gui *Gui) handleExitMain(g *gocui.Gui, v *gocui.View) error {
v.ParentView = nil v.ParentView = nil
return gui.returnFocus(gui.g, v) return gui.returnFocus()
} }
func (gui *Gui) handleMainClick(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleMainClick(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 v.ParentView = currentView
} }
return gui.switchFocus(gui.g, currentView, v, false) return gui.switchFocus(v)
} }

View file

@ -57,11 +57,8 @@ func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
return err return err
} }
} }
err := g.DeleteView("menu") gui.Views.Menu.Visible = false
if err != nil { return gui.returnFocus()
return err
}
return gui.returnFocus(g, v)
} }
func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handlePress func(int) error) error { func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handlePress func(int) error) error {
@ -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) 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.Title = title
menuView.FgColor = gocui.ColorDefault menuView.FgColor = gocui.ColorDefault
menuView.Clear() menuView.Clear()
@ -82,16 +80,18 @@ func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handl
wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error { wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error {
selectedLine := gui.State.Panels.Menu.SelectedLine selectedLine := gui.State.Panels.Menu.SelectedLine
menuView.Visible = false
err := gui.returnFocus()
if err != nil {
return err
}
if err := handlePress(selectedLine); err != nil { if err := handlePress(selectedLine); err != nil {
return err return err
} }
if _, err := gui.g.View("menu"); err == nil {
if _, err := gui.g.SetViewOnBottom("menu"); err != nil {
return err
}
}
return gui.returnFocus(gui.g, menuView) return nil
} }
gui.State.Panels.Menu.OnPress = wrappedHandlePress gui.State.Panels.Menu.OnPress = wrappedHandlePress
@ -105,13 +105,8 @@ func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handl
} }
gui.g.Update(func(g *gocui.Gui) error { gui.g.Update(func(g *gocui.Gui) error {
if _, err := gui.g.View("menu"); err == nil { menuView.Visible = true
if _, err := g.SetViewOnTop("menu"); err != nil { return gui.switchFocus(menuView)
return err
}
}
currentView := gui.g.CurrentView()
return gui.switchFocus(gui.g, currentView, menuView, false)
}) })
return nil return nil
} }

View file

@ -64,10 +64,7 @@ func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
if index >= len(bindings) { if index >= len(bindings) {
return errors.New("Index is greater than size of 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) return bindings[index].Handler(g, v)
} }

View file

@ -221,9 +221,8 @@ func (gui *Gui) handleProjectPrevContext(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleViewAllLogs(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleViewAllLogs(g *gocui.Gui, v *gocui.View) error {
c, err := gui.DockerCommand.ViewAllLogs() c, err := gui.DockerCommand.ViewAllLogs()
if err != nil { if err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
} }
gui.SubProcess = c return gui.runSubprocess(c)
return gui.Errors.ErrSubProcess
} }

View file

@ -237,10 +237,10 @@ func (gui *Gui) handleServiceStop(g *gocui.Gui, v *gocui.View) error {
return nil 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 { return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
if err := service.Stop(); err != nil { if err := service.Stop(); err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
} }
return nil 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 { return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
if err := service.Restart(); err != nil { if err := service.Restart(); err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
} }
return nil 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 { return gui.WithWaitingStatus(gui.Tr.StartingStatus, func() error {
if err := service.Start(); err != nil { if err := service.Start(); err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
} }
return nil return nil
@ -285,16 +285,15 @@ func (gui *Gui) handleServiceAttach(g *gocui.Gui, v *gocui.View) error {
} }
if service.Container == nil { if service.Container == nil {
return gui.createErrorPanel(gui.g, gui.Tr.NoContainers) return gui.createErrorPanel(gui.Tr.NoContainers)
} }
c, err := service.Attach() c, err := service.Attach()
if err != nil { if err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
} }
gui.SubProcess = c return gui.runSubprocess(c)
return gui.Errors.ErrSubProcess
} }
func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error {
@ -305,11 +304,10 @@ func (gui *Gui) handleServiceRenderLogsToMain(g *gocui.Gui, v *gocui.View) error
c, err := service.ViewLogs() c, err := service.ViewLogs()
if err != nil { if err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
} }
gui.SubProcess = c return gui.runSubprocess(c)
return gui.Errors.ErrSubProcess
} }
func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
@ -338,7 +336,7 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
f: func() error { f: func() error {
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
if err := service.Restart(); err != nil { if err := service.Restart(); err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
} }
return nil return nil
}) })
@ -353,7 +351,7 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
f: func() error { f: func() error {
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
if err := gui.OSCommand.RunCommand(recreateCommand); err != nil { if err := gui.OSCommand.RunCommand(recreateCommand); err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
} }
return nil return nil
}) })
@ -366,8 +364,7 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}), gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
), ),
f: func() error { f: func() error {
gui.SubProcess = gui.OSCommand.RunCustomCommand(rebuildCommand) return gui.runSubprocess(gui.OSCommand.RunCustomCommand(rebuildCommand))
return gui.Errors.ErrSubProcess
}, },
}, },
{ {
@ -388,7 +385,7 @@ func (gui *Gui) createServiceCommandMenu(options []*commandOption, status string
} }
return gui.WithWaitingStatus(status, func() error { return gui.WithWaitingStatus(status, func() error {
if err := gui.OSCommand.RunCommand(options[index].command); err != nil { if err := gui.OSCommand.RunCommand(options[index].command); err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
} }
return nil return nil
@ -450,7 +447,7 @@ func (gui *Gui) handleServicesExecShell(g *gocui.Gui, v *gocui.View) error {
container := service.Container container := service.Container
if container == nil { if container == nil {
return gui.createErrorPanel(gui.g, gui.Tr.NoContainers) return gui.createErrorPanel(gui.Tr.NoContainers)
} }
return gui.containerExecShell(container) return gui.containerExecShell(container)
@ -464,7 +461,7 @@ func (gui *Gui) handleServicesOpenInBrowserCommand(g *gocui.Gui, v *gocui.View)
container := service.Container container := service.Container
if container == nil { if container == nil {
return gui.createErrorPanel(gui.g, gui.Tr.NoContainers) return gui.createErrorPanel(gui.Tr.NoContainers)
} }
return gui.openContainerInBrowser(container) return gui.openContainerInBrowser(container)

View file

@ -4,48 +4,39 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec"
"os/signal" "os/signal"
"strings" "strings"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/lazydocker/pkg/utils"
) )
// RunWithSubprocesses loops, instantiating a new gocui.Gui with each iteration func (gui *Gui) runSubprocess(cmd *exec.Cmd) error {
// if the error returned from a run is a ErrSubProcess, it runs the subprocess gui.Mutexes.SubprocessMutex.Lock()
// otherwise it handles the error, possibly by quitting the application defer gui.Mutexes.SubprocessMutex.Unlock()
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.pushPreviousView(gui.currentViewName())
// giving goEvery goroutines time to finish
gui.State.SessionIndex++
if err := gui.runCommand(); err != nil { if err := gui.g.Suspend(); err != nil {
return err return gui.createErrorPanel(err.Error())
}
// pop here so we don't stack up view names
gui.popPreviousView()
// ensuring we render e.g. the logs of the currently selected item upon return
gui.State.Panels.Main.ObjectKey = ""
} else {
return err
}
}
} }
gui.PauseBackgroundThreads = true
gui.runCommand(cmd)
if err := gui.g.Resume(); err != nil {
return gui.createErrorPanel(err.Error())
}
gui.PauseBackgroundThreads = false
return nil return nil
} }
func (gui *Gui) runCommand() error { func (gui *Gui) runCommand(cmd *exec.Cmd) {
gui.SubProcess.Stdout = os.Stdout cmd.Stdout = os.Stdout
gui.SubProcess.Stderr = os.Stdout cmd.Stderr = os.Stdout
gui.SubProcess.Stdin = os.Stdin cmd.Stdin = os.Stdin
stop := make(chan os.Signal, 1) stop := make(chan os.Signal, 1)
defer signal.Stop(stop) defer signal.Stop(stop)
@ -54,25 +45,22 @@ func (gui *Gui) runCommand() error {
signal.Notify(stop, os.Interrupt) signal.Notify(stop, os.Interrupt)
<-stop <-stop
if err := gui.OSCommand.Kill(gui.SubProcess); err != nil { if err := gui.OSCommand.Kill(cmd); err != nil {
gui.Log.Error(err) 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 // not handling the error explicitly because usually we're going to see it
// in the output anyway // in the output anyway
gui.Log.Error(err) gui.Log.Error(err)
} }
gui.SubProcess.Stdin = nil cmd.Stdin = nil
gui.SubProcess.Stdout = ioutil.Discard cmd.Stdout = ioutil.Discard
gui.SubProcess.Stderr = ioutil.Discard cmd.Stderr = ioutil.Discard
gui.SubProcess = nil
gui.promptToReturn() gui.promptToReturn()
return nil
} }

View file

@ -5,8 +5,8 @@ import (
) )
// GetOptionsPanelTextColor gets the color of the options panel text // GetOptionsPanelTextColor gets the color of the options panel text
func (gui *Gui) GetOptionsPanelTextColor() (gocui.Attribute, error) { func (gui *Gui) GetOptionsPanelTextColor() gocui.Attribute {
return GetGocuiStyle(gui.Config.UserConfig.Gui.Theme.OptionsTextColor), nil return GetGocuiStyle(gui.Config.UserConfig.Gui.Theme.OptionsTextColor)
} }
// SetColorScheme sets the color scheme for the app based on the user config // SetColorScheme sets the color scheme for the app based on the user config

View file

@ -7,6 +7,7 @@ import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/samber/lo"
"github.com/spkg/bom" "github.com/spkg/bom"
) )
@ -32,8 +33,7 @@ func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
panic(err) panic(err)
} }
gui.resetMainView() gui.resetMainView()
gui.popPreviousView() return gui.switchFocus(focusedView)
return gui.switchFocus(g, v, focusedView, false)
} }
func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
@ -58,8 +58,7 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
panic(err) panic(err)
} }
gui.resetMainView() gui.resetMainView()
gui.popPreviousView() return gui.switchFocus(focusedView)
return gui.switchFocus(g, v, focusedView, false)
} }
func (gui *Gui) resetMainView() { func (gui *Gui) resetMainView() {
@ -95,58 +94,23 @@ 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)
}
return ""
}
func (gui *Gui) peekPreviousView() string {
if gui.State.PreviousViews.Len() > 0 {
return gui.State.PreviousViews.Peek().(string)
}
return ""
}
func (gui *Gui) pushPreviousView(name string) {
gui.State.PreviousViews.Push(name)
}
func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error {
previousViewName := gui.popPreviousView()
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 // 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 { func (gui *Gui) switchFocus(newView *gocui.View) error {
// we assume we'll never want to return focus to a popup panel i.e. gui.Mutexes.ViewStackMutex.Lock()
// we should never stack popup panels defer gui.Mutexes.ViewStackMutex.Unlock()
if oldView != nil && !gui.isPopupPanel(oldView.Name()) && !returning {
gui.pushPreviousView(oldView.Name())
}
return gui.switchFocusAux(newView)
}
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("setting highlight to true for view " + newView.Name())
gui.Log.Info("new focused view is " + newView.Name()) gui.Log.Info("new focused view is " + newView.Name())
if _, err := g.SetCurrentView(newView.Name()); err != nil { if _, err := gui.g.SetCurrentView(newView.Name()); err != nil {
return err
}
if _, err := g.SetViewOnTop(newView.Name()); err != nil {
return err return err
} }
g.Cursor = newView.Editable gui.g.Cursor = newView.Editable
if err := gui.renderPanelOptions(); err != nil { if err := gui.renderPanelOptions(); err != nil {
return err return err
@ -155,6 +119,72 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View, returnin
return gui.newLineFocused(newView) return gui.newLineFocused(newView)
} }
func (gui *Gui) returnFocus() 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 := gui.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 {
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 = []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)
}
// 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()
}
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 // if the cursor down past the last item, move it to the last line
// nolint:unparam // nolint:unparam
func (gui *Gui) focusPoint(selectedX int, selectedY int, lineCount int, v *gocui.View) { func (gui *Gui) focusPoint(selectedX int, selectedY int, lineCount int, v *gocui.View) {
@ -291,6 +321,10 @@ func (gui *Gui) trimmedContent(v *gocui.View) string {
func (gui *Gui) currentViewName() string { func (gui *Gui) currentViewName() string {
currentView := gui.g.CurrentView() currentView := gui.g.CurrentView()
// this can happen when the app is first starting up
if currentView == nil {
return gui.initiallyFocusedViewName()
}
return currentView.Name() return currentView.Name()
} }
@ -383,3 +417,51 @@ func (gui *Gui) handleClick(v *gocui.View, itemCount int, selectedLine *int, han
return handleSelect(gui.g, v) 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]
}

138
pkg/gui/views.go Normal file
View file

@ -0,0 +1,138 @@
package gui
import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
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) 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
}
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
gui.Views.Project.Title = gui.Tr.ProjectTitle
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 {
gui.Views.Containers.Title = gui.Tr.StandaloneContainersTitle
}
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()
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.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
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
}
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"}
}

View file

@ -229,7 +229,7 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
} }
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error { return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
if cerr := volume.Remove(options[index].force); cerr != nil { if cerr := volume.Remove(options[index].force); cerr != nil {
return gui.createErrorPanel(gui.g, cerr.Error()) return gui.createErrorPanel(cerr.Error())
} }
return nil return nil
}) })
@ -239,11 +239,11 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handlePruneVolumes() 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 { return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneVolumes() err := gui.DockerCommand.PruneVolumes()
if err != nil { if err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(err.Error())
} }
return nil return nil
}) })

16
pkg/gui/window.go Normal file
View file

@ -0,0 +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()
// }
// excludes popups
func (gui *Gui) currentStaticWindowName() string {
return gui.currentStaticViewName()
}
func (gui *Gui) currentSideWindowName() string {
return gui.currentSideViewName()
}

View file

@ -8,7 +8,6 @@ func dutchSet() TranslationSet {
StoppingStatus: "stoppen", StoppingStatus: "stoppen",
RunningCustomCommandStatus: "Aangepast commando draaien", RunningCustomCommandStatus: "Aangepast commando draaien",
RunningSubprocess: "subprocess draaien",
NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement", 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", ErrorOccurred: "Er is iets fout gegaan! Zou je hier een issue aan willen maken: https://github.com/jesseduffield/lazydocker/issues",

View file

@ -13,7 +13,6 @@ type TranslationSet struct {
Scroll string Scroll string
Close string Close string
ErrorTitle string ErrorTitle string
RunningSubprocess string
NoViewMachingNewLineFocusedSwitchStatement string NoViewMachingNewLineFocusedSwitchStatement string
OpenConfig string OpenConfig string
EditConfig string EditConfig string
@ -105,6 +104,9 @@ type TranslationSet struct {
No string No string
Yes string Yes string
LcNextScreenMode string
LcPrevScreenMode string
} }
func englishSet() TranslationSet { func englishSet() TranslationSet {
@ -118,7 +120,6 @@ func englishSet() TranslationSet {
RunningCustomCommandStatus: "running custom command", RunningCustomCommandStatus: "running custom command",
RunningBulkCommandStatus: "running bulk command", RunningBulkCommandStatus: "running bulk command",
RunningSubprocess: "running subprocess",
NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement", NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement",
ErrorOccurred: "An error occurred! Please create an issue at https://github.com/jesseduffield/lazydocker/issues", ErrorOccurred: "An error occurred! Please create an issue at https://github.com/jesseduffield/lazydocker/issues",
@ -217,5 +218,8 @@ func englishSet() TranslationSet {
No: "no", No: "no",
Yes: "yes", Yes: "yes",
LcNextScreenMode: "next screen mode (normal/half/fullscreen)",
LcPrevScreenMode: "prev screen mode",
} }
} }

View file

@ -8,7 +8,6 @@ func germanSet() TranslationSet {
StoppingStatus: "anhalten", StoppingStatus: "anhalten",
RunningCustomCommandStatus: "führt benutzerdefinierten Befehl aus", RunningCustomCommandStatus: "führt benutzerdefinierten Befehl aus",
RunningSubprocess: "Unterprozess ausführen",
NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement", NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement",
ErrorOccurred: "Es ist ein Fehler aufgetreten! Bitte erstelle ein Issue hier: https://github.com/jesseduffield/lazydocker/issues", ErrorOccurred: "Es ist ein Fehler aufgetreten! Bitte erstelle ein Issue hier: https://github.com/jesseduffield/lazydocker/issues",

View file

@ -8,7 +8,6 @@ func polishSet() TranslationSet {
StoppingStatus: "zatrzymywanie", StoppingStatus: "zatrzymywanie",
RunningCustomCommandStatus: "uruchamianie własnej komendty", RunningCustomCommandStatus: "uruchamianie własnej komendty",
RunningSubprocess: "uruchamianie podprocesu",
NoViewMachingNewLineFocusedSwitchStatement: "Żaden widok nie odpowiada instrukcji przełączenia newLineFocused", 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", ErrorOccurred: "Wystąpił błąd! Proszę go zgłosić na https://github.com/jesseduffield/lazydocker/issues",

View file

@ -8,7 +8,6 @@ func turkishSet() TranslationSet {
StoppingStatus: "durduruluyor", StoppingStatus: "durduruluyor",
RunningCustomCommandStatus: "özel komut çalıştır", 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", 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", ErrorOccurred: "Bir hata oluştu! Lütfen https://github.com/jesseduffield/lazydocker/issues adresinden bir hataya ilişkin konu oluşturun",

5
scripts/bump_gocui.sh Executable file
View file

@ -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)

5
scripts/bump_lazycore.sh Executable file
View file

@ -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)

21
vendor/github.com/jesseduffield/lazycore/LICENSE generated vendored Normal file
View file

@ -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.

View file

@ -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
}

View file

@ -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
}

View file

@ -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. @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) ## 1.20.0 (2022-05-02)
Adding: Adding:
@ -55,7 +177,7 @@ Improvement:
Adding: Adding:
- lo.Colaesce - lo.Coalesce
## 1.13.0 (2022-04-14) ## 1.13.0 (2022-04-14)

11
vendor/github.com/samber/lo/Makefile generated vendored
View file

@ -1,10 +1,5 @@
BIN=go BIN=go
# BIN=go1.18beta1
go1.18beta1:
go install golang.org/dl/go1.18beta1@latest
go1.18beta1 download
build: build:
${BIN} build -v ./... ${BIN} build -v ./...
@ -20,7 +15,7 @@ watch-bench:
reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...' reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...'
coverage: coverage:
${BIN} test -v -coverprofile cover.out . ${BIN} test -v -coverprofile=cover.out -covermode=atomic .
${BIN} tool cover -html=cover.out -o cover.html ${BIN} tool cover -html=cover.out -o cover.html
# tools # tools
@ -31,7 +26,7 @@ tools:
${BIN} install github.com/jondot/goweight@latest ${BIN} install github.com/jondot/goweight@latest
${BIN} install github.com/golangci/golangci-lint/cmd/golangci-lint@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 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 go mod tidy
lint: lint:
@ -40,11 +35,9 @@ lint-fix:
golangci-lint run --timeout 60s --max-same-issues 50 --fix ./... golangci-lint run --timeout 60s --max-same-issues 50 --fix ./...
audit: tools audit: tools
${BIN} mod tidy
${BIN} list -json -m all | nancy sleuth ${BIN} list -json -m all | nancy sleuth
outdated: tools outdated: tools
${BIN} mod tidy
${BIN} list -u -m -json all | go-mod-outdated -update -direct ${BIN} list -u -m -json all | go-mod-outdated -update -direct
weight: tools weight: tools

1196
vendor/github.com/samber/lo/README.md generated vendored

File diff suppressed because it is too large Load diff

228
vendor/github.com/samber/lo/channel.go generated vendored Normal file
View file

@ -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
}

View file

@ -1,6 +1,7 @@
package lo package lo
// Ternary is a 1 line if/else statement. // 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 { func Ternary[T any](condition bool, ifOutput T, elseOutput T) T {
if condition { if condition {
return ifOutput return ifOutput
@ -9,12 +10,23 @@ func Ternary[T any](condition bool, ifOutput T, elseOutput T) T {
return elseOutput 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 { type ifElse[T any] struct {
result T result T
done bool done bool
} }
// If. // If.
// Play: https://go.dev/play/p/WSw3ApMxhyW
func If[T any](condition bool, result T) *ifElse[T] { func If[T any](condition bool, result T) *ifElse[T] {
if condition { if condition {
return &ifElse[T]{result, true} return &ifElse[T]{result, true}
@ -25,6 +37,7 @@ func If[T any](condition bool, result T) *ifElse[T] {
} }
// IfF. // IfF.
// Play: https://go.dev/play/p/WSw3ApMxhyW
func IfF[T any](condition bool, resultF func() T) *ifElse[T] { func IfF[T any](condition bool, resultF func() T) *ifElse[T] {
if condition { if condition {
return &ifElse[T]{resultF(), true} return &ifElse[T]{resultF(), true}
@ -35,6 +48,7 @@ func IfF[T any](condition bool, resultF func() T) *ifElse[T] {
} }
// ElseIf. // ElseIf.
// Play: https://go.dev/play/p/WSw3ApMxhyW
func (i *ifElse[T]) ElseIf(condition bool, result T) *ifElse[T] { func (i *ifElse[T]) ElseIf(condition bool, result T) *ifElse[T] {
if !i.done && condition { if !i.done && condition {
i.result = result i.result = result
@ -45,6 +59,7 @@ func (i *ifElse[T]) ElseIf(condition bool, result T) *ifElse[T] {
} }
// ElseIfF. // ElseIfF.
// Play: https://go.dev/play/p/WSw3ApMxhyW
func (i *ifElse[T]) ElseIfF(condition bool, resultF func() T) *ifElse[T] { func (i *ifElse[T]) ElseIfF(condition bool, resultF func() T) *ifElse[T] {
if !i.done && condition { if !i.done && condition {
i.result = resultF() i.result = resultF()
@ -55,6 +70,7 @@ func (i *ifElse[T]) ElseIfF(condition bool, resultF func() T) *ifElse[T] {
} }
// Else. // Else.
// Play: https://go.dev/play/p/WSw3ApMxhyW
func (i *ifElse[T]) Else(result T) T { func (i *ifElse[T]) Else(result T) T {
if i.done { if i.done {
return i.result return i.result
@ -64,6 +80,7 @@ func (i *ifElse[T]) Else(result T) T {
} }
// ElseF. // ElseF.
// Play: https://go.dev/play/p/WSw3ApMxhyW
func (i *ifElse[T]) ElseF(resultF func() T) T { func (i *ifElse[T]) ElseF(resultF func() T) T {
if i.done { if i.done {
return i.result return i.result
@ -79,6 +96,7 @@ type switchCase[T comparable, R any] struct {
} }
// Switch is a pure functional switch/case/default statement. // 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] { func Switch[T comparable, R any](predicate T) *switchCase[T, R] {
var result R var result R
@ -90,6 +108,7 @@ func Switch[T comparable, R any](predicate T) *switchCase[T, R] {
} }
// Case. // Case.
// Play: https://go.dev/play/p/TGbKUMAeRUd
func (s *switchCase[T, R]) Case(val T, result R) *switchCase[T, R] { func (s *switchCase[T, R]) Case(val T, result R) *switchCase[T, R] {
if !s.done && s.predicate == val { if !s.done && s.predicate == val {
s.result = result s.result = result
@ -100,6 +119,7 @@ func (s *switchCase[T, R]) Case(val T, result R) *switchCase[T, R] {
} }
// CaseF. // CaseF.
// Play: https://go.dev/play/p/TGbKUMAeRUd
func (s *switchCase[T, R]) CaseF(val T, cb func() R) *switchCase[T, R] { func (s *switchCase[T, R]) CaseF(val T, cb func() R) *switchCase[T, R] {
if !s.done && s.predicate == val { if !s.done && s.predicate == val {
s.result = cb() s.result = cb()
@ -110,6 +130,7 @@ func (s *switchCase[T, R]) CaseF(val T, cb func() R) *switchCase[T, R] {
} }
// Default. // Default.
// Play: https://go.dev/play/p/TGbKUMAeRUd
func (s *switchCase[T, R]) Default(result R) R { func (s *switchCase[T, R]) Default(result R) R {
if !s.done { if !s.done {
s.result = result s.result = result
@ -119,6 +140,7 @@ func (s *switchCase[T, R]) Default(result R) R {
} }
// DefaultF. // DefaultF.
// Play: https://go.dev/play/p/TGbKUMAeRUd
func (s *switchCase[T, R]) DefaultF(cb func() R) R { func (s *switchCase[T, R]) DefaultF(cb func() R) R {
if !s.done { if !s.done {
s.result = cb() s.result = cb()

View file

@ -2,8 +2,8 @@ version: '3'
services: services:
dev: dev:
build: . image: golang:1.18-bullseye
volumes: volumes:
- ./:/go/src/github.com/samber/lo - ./:/go/src/github.com/samber/lo
working_dir: /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

236
vendor/github.com/samber/lo/errors.go generated vendored
View file

@ -1,62 +1,115 @@
package lo 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. // must panics if err is error or false.
func must(err any) { func must(err any, messageArgs ...interface{}) {
b, isBool := err.(bool) if err == nil {
if isBool && !b { return
panic("not ok")
} }
e, isError := err.(error) switch e := err.(type) {
if isError { case bool:
panic(e) 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 // 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. // and panics if err is error or false.
func Must[T any](val T, err any) T { // Play: https://go.dev/play/p/TMoWrRp3DyC
must(err) func Must[T any](val T, err any, messageArgs ...interface{}) T {
must(err, messageArgs...)
return val return val
} }
// Must0 has the same behavior than Must, but callback returns no variable. // Must0 has the same behavior than Must, but callback returns no variable.
func Must0(err any) { // Play: https://go.dev/play/p/TMoWrRp3DyC
must(err) func Must0(err any, messageArgs ...interface{}) {
must(err, messageArgs...)
} }
// Must1 is an alias to Must // Must1 is an alias to Must
func Must1[T any](val T, err any) T { // Play: https://go.dev/play/p/TMoWrRp3DyC
return Must(val, err) 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. // 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) { // Play: https://go.dev/play/p/TMoWrRp3DyC
must(err) func Must2[T1 any, T2 any](val1 T1, val2 T2, err any, messageArgs ...interface{}) (T1, T2) {
must(err, messageArgs...)
return val1, val2 return val1, val2
} }
// Must3 has the same behavior than Must, but callback returns 3 variables. // 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) { // Play: https://go.dev/play/p/TMoWrRp3DyC
must(err) 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 return val1, val2, val3
} }
// Must4 has the same behavior than Must, but callback returns 4 variables. // 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) { // Play: https://go.dev/play/p/TMoWrRp3DyC
must(err) 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 return val1, val2, val3, val4
} }
// Must5 has the same behavior than Must, but callback returns 5 variables. // 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) { // Play: https://go.dev/play/p/TMoWrRp3DyC
must(err) 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 return val1, val2, val3, val4, val5
} }
// Must6 has the same behavior than Must, but callback returns 6 variables. // 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) { // Play: https://go.dev/play/p/TMoWrRp3DyC
must(err) 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 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. // Try0 has the same behavior than Try, but callback returns no variable.
// Play: https://go.dev/play/p/mTyyWUvn9u4
func Try0(callback func()) bool { func Try0(callback func()) bool {
return Try(func() error { return Try(func() error {
callback() callback()
@ -87,11 +141,13 @@ func Try0(callback func()) bool {
} }
// Try1 is an alias to Try. // 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) return Try(callback)
} }
// Try2 has the same behavior than Try, but callback returns 2 variables. // 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 { func Try2[T any](callback func() (T, error)) bool {
return Try(func() error { return Try(func() error {
_, err := callback() _, 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. // 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 { func Try3[T, R any](callback func() (T, R, error)) bool {
return Try(func() error { return Try(func() error {
_, _, err := callback() _, _, 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. // 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 { func Try4[T, R, S any](callback func() (T, R, S, error)) bool {
return Try(func() error { return Try(func() error {
_, _, _, err := callback() _, _, _, 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. // 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 { func Try5[T, R, S, Q any](callback func() (T, R, S, Q, error)) bool {
return Try(func() error { return Try(func() error {
_, _, _, _, err := callback() _, _, _, _, 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. // 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 { func Try6[T, R, S, Q, U any](callback func() (T, R, S, Q, U, error)) bool {
return Try(func() error { return Try(func() error {
_, _, _, _, _, err := callback() _, _, _, _, _, 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. // 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) { func TryWithErrorValue(callback func() error) (errorValue any, ok bool) {
ok = true 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. // 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()) { func TryCatch(callback func() error, catch func()) {
if !Try(callback) { if !Try(callback) {
catch() 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. // 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)) { func TryCatchWithErrorValue(callback func() error, catch func(any)) {
if err, ok := TryWithErrorValue(callback); !ok { if err, ok := TryWithErrorValue(callback); !ok {
catch(err) 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
}

158
vendor/github.com/samber/lo/find.go generated vendored
View file

@ -2,7 +2,6 @@ package lo
import ( import (
"fmt" "fmt"
"math"
"math/rand" "math/rand"
"golang.org/x/exp/constraints" "golang.org/x/exp/constraints"
@ -87,6 +86,140 @@ func FindOrElse[T any](collection []T, fallback T, predicate func(T) bool) T {
return fallback 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. // Min search the minimum value of a collection.
func Min[T constraints.Ordered](collection []T) T { func Min[T constraints.Ordered](collection []T) T {
var min 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 // 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. // 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) { func Nth[T any, N constraints.Integer](collection []T, nth N) (T, error) {
if int(math.Abs(float64(nth))) >= len(collection) { n := int(nth)
l := len(collection)
if n >= l || -n > l {
var t T 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 n >= 0 {
return collection[n], nil
if nth >= 0 {
return collection[nth], nil
} }
return collection[l+n], nil
return collection[length+nth], nil
} }
// Sample returns a random item from collection. // 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 { func Samples[T any](collection []T, count int) []T {
size := len(collection) size := len(collection)
// put values into a map, for faster deletion cOpy := append([]T{}, collection...)
cOpy := make([]T, 0, size)
for _, v := range collection {
cOpy = append(cOpy, v)
}
results := []T{} results := []T{}

8
vendor/github.com/samber/lo/func.go generated vendored Normal file
View file

@ -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)
}
}

View file

@ -175,3 +175,28 @@ func Union[T comparable](list1 []T, list2 []T) []T {
return result 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
}

42
vendor/github.com/samber/lo/map.go generated vendored
View file

@ -1,6 +1,7 @@
package lo package lo
// Keys creates an array of the map keys. // 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 { func Keys[K comparable, V any](in map[K]V) []K {
result := make([]K, 0, len(in)) 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. // 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 { func Values[K comparable, V any](in map[K]V) []V {
result := make([]V, 0, len(in)) 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. // 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 { func PickBy[K comparable, V any](in map[K]V, predicate func(K, V) bool) map[K]V {
r := map[K]V{} r := map[K]V{}
for k, v := range in { 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. // 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 { func PickByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V {
r := map[K]V{} r := map[K]V{}
for k, v := range in { 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. // 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 { func PickByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V {
r := map[K]V{} r := map[K]V{}
for k, v := range in { 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 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 { func OmitBy[K comparable, V any](in map[K]V, predicate func(K, V) bool) map[K]V {
r := map[K]V{} r := map[K]V{}
for k, v := range in { 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. // 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 { func OmitByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V {
r := map[K]V{} r := map[K]V{}
for k, v := range in { 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. // 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 { func OmitByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V {
r := map[K]V{} r := map[K]V{}
for k, v := range in { 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. // Entries transforms a map into array of key/value pairs.
// Play:
func Entries[K comparable, V any](in map[K]V) []Entry[K, V] { func Entries[K comparable, V any](in map[K]V) []Entry[K, V] {
entries := make([]Entry[K, V], 0, len(in)) 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 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. // 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 { func FromEntries[K comparable, V any](entries []Entry[K, V]) map[K]V {
out := 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 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 // Invert creates a map composed of the inverted keys and values. If map
// contains duplicate values, subsequent values overwrite property assignments // contains duplicate values, subsequent values overwrite property assignments
// of previous values. // of previous values.
// Play: https://go.dev/play/p/rFQ4rak6iA1
func Invert[K comparable, V comparable](in map[K]V) map[V]K { func Invert[K comparable, V comparable](in map[K]V) map[V]K {
out := 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. // 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 { func Assign[K comparable, V any](maps ...map[K]V) map[K]V {
out := 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. // 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 { func MapKeys[K comparable, V any, R comparable](in map[K]V, iteratee func(V, K) R) map[R]V {
result := 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. // 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 { func MapValues[K comparable, V any, R any](in map[K]V, iteratee func(V, K) R) map[K]R {
result := 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 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
}

View file

@ -3,6 +3,7 @@ package lo
import "golang.org/x/exp/constraints" import "golang.org/x/exp/constraints"
// Range creates an array of numbers (positive and/or negative) with given length. // 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 { func Range(elementNum int) []int {
length := If(elementNum < 0, -elementNum).Else(elementNum) length := If(elementNum < 0, -elementNum).Else(elementNum)
result := make([]int, length) result := make([]int, length)
@ -14,6 +15,7 @@ func Range(elementNum int) []int {
} }
// RangeFrom creates an array of numbers from start with specified length. // 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 { func RangeFrom[T constraints.Integer | constraints.Float](start T, elementNum int) []T {
length := If(elementNum < 0, -elementNum).Else(elementNum) length := If(elementNum < 0, -elementNum).Else(elementNum)
result := make([]T, length) 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. // 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. // 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 { func RangeWithSteps[T constraints.Integer | constraints.Float](start, end, step T) []T {
result := []T{} result := []T{}
if start == end || step == 0 { 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. // 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 { func Clamp[T constraints.Ordered](value T, min T, max T) T {
if value < min { if value < min {
return min return min
@ -59,7 +63,8 @@ func Clamp[T constraints.Ordered](value T, min T, max T) T {
return value 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 { func SumBy[T any, R constraints.Float | constraints.Integer](collection []T, iteratee func(T) R) R {
var sum R = 0 var sum R = 0
for _, item := range collection { for _, item := range collection {

View file

@ -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
}

View file

@ -46,6 +46,7 @@ func (d *debounce) cancel() {
} }
// NewDebounce creates a debounced instance that delays invoking functions given until after wait milliseconds have elapsed. // 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()) { func NewDebounce(duration time.Duration, f ...func()) (func(), func()) {
d := &debounce{ d := &debounce{
after: duration, after: duration,
@ -60,7 +61,8 @@ func NewDebounce(duration time.Duration, f ...func()) (func(), func()) {
}, d.cancel }, 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) { func Attempt(maxIteration int, f func(int) error) (int, error) {
var err 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, // AttemptWithDelay invokes a function N times until it returns valid output,
// with a pause betwwen each call. Returning either the caught error or nil. // 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 // When first argument is less than `1`, the function runs until a successful
// response is returned. // 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) { func AttemptWithDelay(maxIteration int, delay time.Duration, f func(int, time.Duration) error) (int, time.Duration, error) {
var err error var err error

229
vendor/github.com/samber/lo/slice.go generated vendored
View file

@ -2,9 +2,12 @@ package lo
import ( import (
"math/rand" "math/rand"
"golang.org/x/exp/constraints"
) )
// Filter iterates over elements of collection, returning an array of all elements predicate returns truthy for. // 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 { func Filter[V any](collection []V, predicate func(V, int) bool) []V {
result := []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. // 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 { func Map[T any, R any](collection []T, iteratee func(T, int) R) []R {
result := make([]R, len(collection)) 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 callback function should return two values:
// - the result of the mapping operation and // - the result of the mapping operation and
// - whether the result element should be included or not. // - 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 { func FilterMap[T any, R any](collection []T, callback func(T, int) (R, bool)) []R {
result := []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. // 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 { func FlatMap[T any, R any](collection []T, iteratee func(T, int) []R) []R {
result := []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 // 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. // 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 { func Reduce[T any, R any](collection []T, accumulator func(R, T, int) R, initial R) R {
for i, item := range collection { for i, item := range collection {
initial = accumulator(initial, item, i) 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 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. // 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)) { func ForEach[T any](collection []T, iteratee func(T, int)) {
for i, item := range collection { for i, item := range collection {
iteratee(item, i) 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. // Times invokes the iteratee n times, returning an array of the results of each invocation.
// The iteratee is invoked with index as argument. // 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 { func Times[T any](count int, iteratee func(int) T) []T {
result := make([]T, count) 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. // 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. // 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 { func Uniq[T comparable](collection []T) []T {
result := make([]T, 0, len(collection)) result := make([]T, 0, len(collection))
seen := make(map[T]struct{}, 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. // 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 // 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. // 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 { func UniqBy[T any, U comparable](collection []T, iteratee func(T) U) []T {
result := make([]T, 0, len(collection)) result := make([]T, 0, len(collection))
seen := make(map[U]struct{}, 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. // 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 { func GroupBy[T any, U comparable](collection []T, iteratee func(T) U) map[U][]T {
result := 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, // 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. // the final chunk will be the remaining elements.
// Play: https://go.dev/play/p/EeKl0AuTehH
func Chunk[T any](collection []T, size int) [][]T { func Chunk[T any](collection []T, size int) [][]T {
if size <= 0 { if size <= 0 {
panic("Second parameter must be greater than 0") panic("Second parameter must be greater than 0")
} }
result := make([][]T, 0, len(collection)/2+1) chunksNum := len(collection) / size
length := len(collection) if len(collection)%size != 0 {
chunksNum += 1
}
for i := 0; i < length; i++ { result := make([][]T, 0, chunksNum)
chunk := i / size
if i%size == 0 { for i := 0; i < chunksNum; i++ {
result = append(result, make([]T, 0, size)) last := (i + 1) * size
if last > len(collection) {
last = len(collection)
} }
result = append(result, collection[i*size:last])
result[chunk] = append(result[chunk], collection[i])
} }
return result 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 // 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 // determined by the order they occur in collection. The grouping is generated from the results
// of running each element of collection through iteratee. // 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 { func PartitionBy[T any, K comparable](collection []T, iteratee func(x T) K) [][]T {
result := [][]T{} result := [][]T{}
seen := map[K]int{} 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. // Flatten returns an array a single level deep.
// Play: https://go.dev/play/p/rbp9ORaMpjw
func Flatten[T any](collection [][]T) []T { func Flatten[T any](collection [][]T) []T {
result := []T{} totalLen := 0
for i := range collection {
totalLen += len(collection[i])
}
for _, item := range collection { result := make([]T, 0, totalLen)
result = append(result, item...) for i := range collection {
result = append(result, collection[i]...)
} }
return result return result
} }
// Shuffle returns an array of shuffled values. Uses the Fisher-Yates shuffle algorithm. // 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 { func Shuffle[T any](collection []T) []T {
rand.Shuffle(len(collection), func(i, j int) { rand.Shuffle(len(collection), func(i, j int) {
collection[i], collection[j] = collection[j], collection[i] 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. // 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 { func Reverse[T any](collection []T) []T {
length := len(collection) length := len(collection)
half := length / 2 half := length / 2
@ -220,6 +254,7 @@ func Reverse[T any](collection []T) []T {
} }
// Fill fills elements of array with `initial` value. // 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 { func Fill[T Clonable[T]](collection []T, initial T) []T {
result := make([]T, 0, len(collection)) 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. // 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 { func Repeat[T Clonable[T]](count int, initial T) []T {
result := make([]T, 0, count) 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. // 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 { func RepeatBy[T any](count int, predicate func(int) T) []T {
result := make([]T, 0, count) 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. // 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 { func KeyBy[K comparable, V any](collection []V, iteratee func(V) K) map[K]V {
result := make(map[K]V, len(collection)) 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 return result
} }
// Drop drops n elements from the beginning of a slice or array. // Associate returns a map containing key-value pairs provided by transform function applied to elements of the given slice.
func Drop[T any](collection []T, n int) []T { // If any of two pairs would have the same key the last one gets added to the map.
if len(collection) <= n { // The order of keys in returned map is not specified and is not guaranteed to be the same from the original array.
return make([]T, 0) // 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 _, t := range collection {
for i := n; i < len(collection); i++ { k, v := transform(t)
result[i-n] = collection[i] result[k] = v
} }
return result 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. // 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 { func DropWhile[T any](collection []T, predicate func(T) bool) []T {
i := 0 i := 0
for ; i < len(collection); i++ { 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) result := make([]T, 0, len(collection)-i)
return append(result, 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
} }
// DropRightWhile drops elements from the end of a slice or array while the predicate returns true. // 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 { func DropRightWhile[T any](collection []T, predicate func(T) bool) []T {
i := len(collection) - 1 i := len(collection) - 1
for ; i >= 0; i-- { for ; i >= 0; i-- {
@ -319,16 +373,12 @@ func DropRightWhile[T any](collection []T, predicate func(T) bool) []T {
} }
} }
result := make([]T, i+1) result := make([]T, 0, i+1)
return append(result, collection[:i+1]...)
for ; i >= 0; i-- {
result[i] = collection[i]
}
return result
} }
// Reject is the opposite of Filter, this method returns the elements of collection that predicate does not return truthy for. // 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 { func Reject[V any](collection []V, predicate func(V, int) bool) []V {
result := []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. // 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) { func Count[T comparable](collection []T, value T) (count int) {
for _, item := range collection { for _, item := range collection {
if item == value { 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. // 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) { func CountBy[T any](collection []T, predicate func(T) bool) (count int) {
for _, item := range collection { for _, item := range collection {
if predicate(item) { if predicate(item) {
@ -363,7 +415,8 @@ func CountBy[T any](collection []T, predicate func(T) bool) (count int) {
return count 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 { func Subset[T any](collection []T, offset int, length uint) []T {
size := len(collection) size := len(collection)
@ -385,17 +438,36 @@ func Subset[T any](collection []T, offset int, length uint) []T {
return collection[offset : offset+int(length)] 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. // 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.
func Replace[T comparable](collection []T, old T, new T, n int) []T { // Play: https://go.dev/play/p/8XWYhfMMA1h
func Slice[T any](collection []T, start int, end int) []T {
size := len(collection) size := len(collection)
result := make([]T, 0, size)
for _, item := range collection { if start >= end {
if item == old && n != 0 { return []T{}
result = append(result, new) }
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-- 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. // 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 { 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
} }

View file

@ -1,8 +1,11 @@
package lo package lo
import "unicode/utf8" import (
"unicode/utf8"
)
// Substring return part of a string. // Substring return part of a string.
// Play: https://go.dev/play/p/TQlxQi82Lu1
func Substring[T ~string](str T, offset int, length uint) T { func Substring[T ~string](str T, offset int, length uint) T {
size := len(str) size := len(str)
@ -24,7 +27,39 @@ func Substring[T ~string](str T, offset int, length uint) T {
return str[offset : offset+int(length)] 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. // 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 { func RuneLength(str string) int {
return utf8.RuneCountInString(str) return utf8.RuneCountInString(str)
} }

32
vendor/github.com/samber/lo/test.go generated vendored Normal file
View file

@ -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}
}

136
vendor/github.com/samber/lo/tuples.go generated vendored
View file

@ -1,81 +1,97 @@
package lo package lo
// T2 creates a tuple from a list of values. // 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] { func T2[A any, B any](a A, b B) Tuple2[A, B] {
return Tuple2[A, B]{A: a, B: b} return Tuple2[A, B]{A: a, B: b}
} }
// T3 creates a tuple from a list of values. // 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] { 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} return Tuple3[A, B, C]{A: a, B: b, C: c}
} }
// T4 creates a tuple from a list of values. // 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] { 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} return Tuple4[A, B, C, D]{A: a, B: b, C: c, D: d}
} }
// T5 creates a tuple from a list of values. // 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] { 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} 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. // 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] { 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} 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. // 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] { 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} 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. // 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] { 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} 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. // 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] { 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} 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. // 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) { func Unpack2[A any, B any](tuple Tuple2[A, B]) (A, B) {
return tuple.A, tuple.B return tuple.A, tuple.B
} }
// Unpack3 returns values contained in tuple. // 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) { func Unpack3[A any, B any, C any](tuple Tuple3[A, B, C]) (A, B, C) {
return tuple.A, tuple.B, tuple.C return tuple.A, tuple.B, tuple.C
} }
// Unpack4 returns values contained in tuple. // 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) { 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 return tuple.A, tuple.B, tuple.C, tuple.D
} }
// Unpack5 returns values contained in tuple. // 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) { 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 return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E
} }
// Unpack6 returns values contained in tuple. // 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) { 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 return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F
} }
// Unpack7 returns values contained in tuple. // 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) { 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 return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F, tuple.G
} }
// Unpack8 returns values contained in tuple. // 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) { 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 return tuple.A, tuple.B, tuple.C, tuple.D, tuple.E, tuple.F, tuple.G, tuple.H
} }
// Unpack9 returns values contained in tuple. // 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) { 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 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 // 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. // 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. // 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] { 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) result := make([]Tuple2[A, B], 0, size)
for index := 0; index < size; index++ { for index := 0; index < size; index++ {
_a, _ := Nth[A](a, index) _a, _ := Nth(a, index)
_b, _ := Nth[B](b, index) _b, _ := Nth(b, index)
result = append(result, Tuple2[A, B]{ result = append(result, Tuple2[A, B]{
A: _a, 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 // 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. // 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. // 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] { 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) result := make([]Tuple3[A, B, C], 0, size)
for index := 0; index < size; index++ { for index := 0; index < size; index++ {
_a, _ := Nth[A](a, index) _a, _ := Nth(a, index)
_b, _ := Nth[B](b, index) _b, _ := Nth(b, index)
_c, _ := Nth[C](c, index) _c, _ := Nth(c, index)
result = append(result, Tuple3[A, B, C]{ result = append(result, Tuple3[A, B, C]{
A: _a, 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 // 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. // 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. // 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] { 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) result := make([]Tuple4[A, B, C, D], 0, size)
for index := 0; index < size; index++ { for index := 0; index < size; index++ {
_a, _ := Nth[A](a, index) _a, _ := Nth(a, index)
_b, _ := Nth[B](b, index) _b, _ := Nth(b, index)
_c, _ := Nth[C](c, index) _c, _ := Nth(c, index)
_d, _ := Nth[D](d, index) _d, _ := Nth(d, index)
result = append(result, Tuple4[A, B, C, D]{ result = append(result, Tuple4[A, B, C, D]{
A: _a, 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 // 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. // 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. // 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] { 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) result := make([]Tuple5[A, B, C, D, E], 0, size)
for index := 0; index < size; index++ { for index := 0; index < size; index++ {
_a, _ := Nth[A](a, index) _a, _ := Nth(a, index)
_b, _ := Nth[B](b, index) _b, _ := Nth(b, index)
_c, _ := Nth[C](c, index) _c, _ := Nth(c, index)
_d, _ := Nth[D](d, index) _d, _ := Nth(d, index)
_e, _ := Nth[E](e, index) _e, _ := Nth(e, index)
result = append(result, Tuple5[A, B, C, D, E]{ result = append(result, Tuple5[A, B, C, D, E]{
A: _a, 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 // 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. // 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. // 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] { 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) result := make([]Tuple6[A, B, C, D, E, F], 0, size)
for index := 0; index < size; index++ { for index := 0; index < size; index++ {
_a, _ := Nth[A](a, index) _a, _ := Nth(a, index)
_b, _ := Nth[B](b, index) _b, _ := Nth(b, index)
_c, _ := Nth[C](c, index) _c, _ := Nth(c, index)
_d, _ := Nth[D](d, index) _d, _ := Nth(d, index)
_e, _ := Nth[E](e, index) _e, _ := Nth(e, index)
_f, _ := Nth[F](f, index) _f, _ := Nth(f, index)
result = append(result, Tuple6[A, B, C, D, E, F]{ result = append(result, Tuple6[A, B, C, D, E, F]{
A: _a, 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 // 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. // 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. // 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] { 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) result := make([]Tuple7[A, B, C, D, E, F, G], 0, size)
for index := 0; index < size; index++ { for index := 0; index < size; index++ {
_a, _ := Nth[A](a, index) _a, _ := Nth(a, index)
_b, _ := Nth[B](b, index) _b, _ := Nth(b, index)
_c, _ := Nth[C](c, index) _c, _ := Nth(c, index)
_d, _ := Nth[D](d, index) _d, _ := Nth(d, index)
_e, _ := Nth[E](e, index) _e, _ := Nth(e, index)
_f, _ := Nth[F](f, index) _f, _ := Nth(f, index)
_g, _ := Nth[G](g, index) _g, _ := Nth(g, index)
result = append(result, Tuple7[A, B, C, D, E, F, G]{ result = append(result, Tuple7[A, B, C, D, E, F, G]{
A: _a, 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 // 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. // 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. // 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] { 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) result := make([]Tuple8[A, B, C, D, E, F, G, H], 0, size)
for index := 0; index < size; index++ { for index := 0; index < size; index++ {
_a, _ := Nth[A](a, index) _a, _ := Nth(a, index)
_b, _ := Nth[B](b, index) _b, _ := Nth(b, index)
_c, _ := Nth[C](c, index) _c, _ := Nth(c, index)
_d, _ := Nth[D](d, index) _d, _ := Nth(d, index)
_e, _ := Nth[E](e, index) _e, _ := Nth(e, index)
_f, _ := Nth[F](f, index) _f, _ := Nth(f, index)
_g, _ := Nth[G](g, index) _g, _ := Nth(g, index)
_h, _ := Nth[H](h, index) _h, _ := Nth(h, index)
result = append(result, Tuple8[A, B, C, D, E, F, G, H]{ result = append(result, Tuple8[A, B, C, D, E, F, G, H]{
A: _a, 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 // 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. // 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. // 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] { 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) result := make([]Tuple9[A, B, C, D, E, F, G, H, I], 0, size)
for index := 0; index < size; index++ { for index := 0; index < size; index++ {
_a, _ := Nth[A](a, index) _a, _ := Nth(a, index)
_b, _ := Nth[B](b, index) _b, _ := Nth(b, index)
_c, _ := Nth[C](c, index) _c, _ := Nth(c, index)
_d, _ := Nth[D](d, index) _d, _ := Nth(d, index)
_e, _ := Nth[E](e, index) _e, _ := Nth(e, index)
_f, _ := Nth[F](f, index) _f, _ := Nth(f, index)
_g, _ := Nth[G](g, index) _g, _ := Nth(g, index)
_h, _ := Nth[H](h, index) _h, _ := Nth(h, index)
_i, _ := Nth[I](i, index) _i, _ := Nth(i, index)
result = append(result, Tuple9[A, B, C, D, E, F, G, H, I]{ result = append(result, Tuple9[A, B, C, D, E, F, G, H, I]{
A: _a, 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 // Unzip2 accepts an array of grouped elements and creates an array regrouping the elements
// to their pre-zip configuration. // to their pre-zip configuration.
// Play: https://go.dev/play/p/ciHugugvaAW
func Unzip2[A any, B any](tuples []Tuple2[A, B]) ([]A, []B) { func Unzip2[A any, B any](tuples []Tuple2[A, B]) ([]A, []B) {
size := len(tuples) size := len(tuples)
r1 := make([]A, 0, size) 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 // Unzip3 accepts an array of grouped elements and creates an array regrouping the elements
// to their pre-zip configuration. // 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) { func Unzip3[A any, B any, C any](tuples []Tuple3[A, B, C]) ([]A, []B, []C) {
size := len(tuples) size := len(tuples)
r1 := make([]A, 0, size) 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 // Unzip4 accepts an array of grouped elements and creates an array regrouping the elements
// to their pre-zip configuration. // 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) { func Unzip4[A any, B any, C any, D any](tuples []Tuple4[A, B, C, D]) ([]A, []B, []C, []D) {
size := len(tuples) size := len(tuples)
r1 := make([]A, 0, size) 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 // Unzip5 accepts an array of grouped elements and creates an array regrouping the elements
// to their pre-zip configuration. // 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) { 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) size := len(tuples)
r1 := make([]A, 0, size) 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 // Unzip6 accepts an array of grouped elements and creates an array regrouping the elements
// to their pre-zip configuration. // 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) { 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) size := len(tuples)
r1 := make([]A, 0, size) 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 // Unzip7 accepts an array of grouped elements and creates an array regrouping the elements
// to their pre-zip configuration. // 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) { 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) size := len(tuples)
r1 := make([]A, 0, size) 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 // Unzip8 accepts an array of grouped elements and creates an array regrouping the elements
// to their pre-zip configuration. // 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) { 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) size := len(tuples)
r1 := make([]A, 0, size) 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 // Unzip9 accepts an array of grouped elements and creates an array regrouping the elements
// to their pre-zip configuration. // 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) { 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) size := len(tuples)
r1 := make([]A, 0, size) r1 := make([]A, 0, size)

88
vendor/github.com/samber/lo/type_manipulation.go generated vendored Normal file
View file

@ -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
}

View file

@ -1,8 +1,10 @@
package assert package assert
import ( import (
"bytes"
"fmt" "fmt"
"reflect" "reflect"
"time"
) )
type CompareType int type CompareType int
@ -30,6 +32,9 @@ var (
float64Type = reflect.TypeOf(float64(1)) float64Type = reflect.TypeOf(float64(1))
stringType = reflect.TypeOf("") stringType = reflect.TypeOf("")
timeType = reflect.TypeOf(time.Time{})
bytesType = reflect.TypeOf([]byte{})
) )
func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { 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 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 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, float64(2), float64(1))
// assert.Greater(t, "b", "a") // assert.Greater(t, "b", "a")
func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { 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 // 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", "a")
// assert.GreaterOrEqual(t, "b", "b") // assert.GreaterOrEqual(t, "b", "b")
func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { 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 // 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, float64(1), float64(2))
// assert.Less(t, "a", "b") // assert.Less(t, "a", "b")
func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { 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 // 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, "a", "b")
// assert.LessOrEqual(t, "b", "b") // assert.LessOrEqual(t, "b", "b")
func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { 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 // 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)
// assert.Positive(t, 1.23) // assert.Positive(t, 1.23)
func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
zero := reflect.Zero(reflect.TypeOf(e)) 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 // 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)
// assert.Negative(t, -1.23) // assert.Negative(t, -1.23)
func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
zero := reflect.Zero(reflect.TypeOf(e)) 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 { func compareTwoValues(t TestingT, e1 interface{}, e2 interface{}, allowedComparesResults []CompareType, failMessage string, msgAndArgs ...interface{}) bool {

View file

@ -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)
}

View file

@ -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
}

View file

@ -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...)...) 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. // ErrorIsf asserts that at least one of the errors in err's chain matches target.
// This is a wrapper for errors.Is. // This is a wrapper for errors.Is.
func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool { 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...)...) 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. // YAMLEqf asserts that two YAML strings are equivalent.
func YAMLEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) bool { func YAMLEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok { if h, ok := t.(tHelper); ok {

View file

@ -222,6 +222,30 @@ func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args ..
return ErrorAsf(a.t, err, target, msg, 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. // ErrorIs asserts that at least one of the errors in err's chain matches target.
// This is a wrapper for errors.Is. // This is a wrapper for errors.Is.
func (a *Assertions) ErrorIs(err error, target error, msgAndArgs ...interface{}) bool { 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...) 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. // YAMLEq asserts that two YAML strings are equivalent.
func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interface{}) bool { func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok { if h, ok := a.t.(tHelper); ok {

View file

@ -50,7 +50,7 @@ func isOrdered(t TestingT, object interface{}, allowedComparesResults []CompareT
// assert.IsIncreasing(t, []float{1, 2}) // assert.IsIncreasing(t, []float{1, 2})
// assert.IsIncreasing(t, []string{"a", "b"}) // assert.IsIncreasing(t, []string{"a", "b"})
func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { 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 // 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, []float{2, 1})
// assert.IsNonIncreasing(t, []string{"b", "a"}) // assert.IsNonIncreasing(t, []string{"b", "a"})
func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { 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 // 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, []float{2, 1})
// assert.IsDecreasing(t, []string{"b", "a"}) // assert.IsDecreasing(t, []string{"b", "a"})
func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { 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 // 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, []float{1, 2})
// assert.IsNonDecreasing(t, []string{"a", "b"}) // assert.IsNonDecreasing(t, []string{"a", "b"})
func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { 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...)
} }

View file

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"math" "math"
"os" "os"
"path/filepath"
"reflect" "reflect"
"regexp" "regexp"
"runtime" "runtime"
@ -144,7 +145,8 @@ func CallerInfo() []string {
if len(parts) > 1 { if len(parts) > 1 {
dir := parts[len(parts)-2] dir := parts[len(parts)-2]
if (dir != "assert" && dir != "mock" && dir != "require") || file == "mock_test.go" { 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() { switch objValue.Kind() {
// collection types are empty when they have no element // 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 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: case reflect.Ptr:
if objValue.IsNil() { if objValue.IsNil() {
return true return true
} }
deref := objValue.Elem().Interface() deref := objValue.Elem().Interface()
return isEmpty(deref) 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: default:
zero := reflect.Zero(objValue.Type()) zero := reflect.Zero(objValue.Type())
return reflect.DeepEqual(object, zero.Interface()) 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 (false, false) if impossible.
// return (true, false) if element was not found. // return (true, false) if element was not found.
// return (true, true) if element was 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) listValue := reflect.ValueOf(list)
listKind := reflect.TypeOf(list).Kind() listType := reflect.TypeOf(list)
if listType == nil {
return false, false
}
listKind := listType.Kind()
defer func() { defer func() {
if e := recover(); e != nil { if e := recover(); e != nil {
ok = false ok = false
@ -764,7 +771,7 @@ func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bo
h.Helper() h.Helper()
} }
ok, found := includeElement(s, contains) ok, found := containsElement(s, contains)
if !ok { if !ok {
return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", s), msgAndArgs...) 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() h.Helper()
} }
ok, found := includeElement(s, contains) ok, found := containsElement(s, contains)
if !ok { if !ok {
return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...) 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 return true // we consider nil to be equal to the nil set
} }
subsetValue := reflect.ValueOf(subset)
defer func() { defer func() {
if e := recover(); e != nil { if e := recover(); e != nil {
ok = false ok = false
@ -821,17 +827,35 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok
listKind := reflect.TypeOf(list).Kind() listKind := reflect.TypeOf(list).Kind()
subsetKind := reflect.TypeOf(subset).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...) 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...) 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++ { for i := 0; i < subsetValue.Len(); i++ {
element := subsetValue.Index(i).Interface() element := subsetValue.Index(i).Interface()
ok, found := includeElement(list, element) ok, found := containsElement(list, element)
if !ok { if !ok {
return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...) 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() h.Helper()
} }
if subset == nil { 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() { defer func() {
if e := recover(); e != nil { if e := recover(); e != nil {
ok = false ok = false
@ -865,17 +888,35 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{})
listKind := reflect.TypeOf(list).Kind() listKind := reflect.TypeOf(list).Kind()
subsetKind := reflect.TypeOf(subset).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...) 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...) 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++ { for i := 0; i < subsetValue.Len(); i++ {
element := subsetValue.Index(i).Interface() element := subsetValue.Index(i).Interface()
ok, found := includeElement(list, element) ok, found := containsElement(list, element)
if !ok { if !ok {
return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...) 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() type PanicTestFunc func()
// didPanic returns true if the function passed to it panics. Otherwise, it returns false. // didPanic returns true if the function passed to it panics. Otherwise, it returns false.
func didPanic(f PanicTestFunc) (bool, interface{}, string) { func didPanic(f PanicTestFunc) (didPanic bool, message interface{}, stack string) {
didPanic = true
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()
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. // 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 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) { func toFloat(x interface{}) (float64, bool) {
var xf float64 var xf float64
xok := true xok := true
@ -1161,11 +1217,15 @@ func InDelta(t TestingT, expected, actual interface{}, delta float64, msgAndArgs
bf, bok := toFloat(actual) bf, bok := toFloat(actual)
if !aok || !bok { 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) { 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) { if math.IsNaN(bf) {
@ -1188,7 +1248,7 @@ func InDeltaSlice(t TestingT, expected, actual interface{}, delta float64, msgAn
if expected == nil || actual == nil || if expected == nil || actual == nil ||
reflect.TypeOf(actual).Kind() != reflect.Slice || reflect.TypeOf(actual).Kind() != reflect.Slice ||
reflect.TypeOf(expected).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) 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) { func calcRelativeError(expected, actual interface{}) (float64, error) {
af, aok := toFloat(expected) af, aok := toFloat(expected)
if !aok { bf, bok := toFloat(actual)
return 0, fmt.Errorf("expected value %q cannot be converted to float", expected) 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) { if math.IsNaN(af) {
return 0, errors.New("expected value must not be NaN") return 0, errors.New("expected value must not be NaN")
@ -1259,10 +1323,6 @@ func calcRelativeError(expected, actual interface{}) (float64, error) {
if af == 0 { if af == 0 {
return 0, fmt.Errorf("expected value must have a value other than zero to calculate the relative error") 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) { if math.IsNaN(bf) {
return 0, errors.New("actual value must not be NaN") 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 || if expected == nil || actual == nil ||
reflect.TypeOf(actual).Kind() != reflect.Slice || reflect.TypeOf(actual).Kind() != reflect.Slice ||
reflect.TypeOf(expected).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) actualSlice := reflect.ValueOf(actual)
@ -1375,6 +1435,27 @@ func EqualError(t TestingT, theError error, errString string, msgAndArgs ...inte
return true 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. // matchRegexp return true if a specified regexp matches a string.
func matchRegexp(rx interface{}, str interface{}) bool { func matchRegexp(rx interface{}, str interface{}) bool {
@ -1588,12 +1669,17 @@ func diff(expected interface{}, actual interface{}) string {
} }
var e, a string var e, a string
if et != reflect.TypeOf("") {
e = spewConfig.Sdump(expected) switch et {
a = spewConfig.Sdump(actual) case reflect.TypeOf(""):
} else {
e = reflect.ValueOf(expected).String() e = reflect.ValueOf(expected).String()
a = reflect.ValueOf(actual).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{ diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
@ -1625,6 +1711,14 @@ var spewConfig = spew.ConfigState{
MaxDepth: 10, MaxDepth: 10,
} }
var spewConfigStringerEnabled = spew.ConfigState{
Indent: " ",
DisablePointerAddresses: true,
DisableCapacities: true,
SortKeys: true,
MaxDepth: 10,
}
type tHelper interface { type tHelper interface {
Helper() Helper()
} }

78
vendor/gopkg.in/yaml.v3/decode.go generated vendored
View file

@ -100,7 +100,10 @@ func (p *parser) peek() yaml_event_type_t {
if p.event.typ != yaml_NO_EVENT { if p.event.typ != yaml_NO_EVENT {
return p.event.typ 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() p.fail()
} }
return p.event.typ return p.event.typ
@ -320,6 +323,8 @@ type decoder struct {
decodeCount int decodeCount int
aliasCount int aliasCount int
aliasDepth int aliasDepth int
mergedFields map[interface{}]bool
} }
var ( 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 mapIsNew := false
if out.IsNil() { if out.IsNil() {
out.Set(reflect.MakeMap(outt)) 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 { for i := 0; i < l; i += 2 {
if isMerge(n.Content[i]) { if isMerge(n.Content[i]) {
d.merge(n.Content[i+1], out) mergeNode = n.Content[i+1]
continue continue
} }
k := reflect.New(kt).Elem() k := reflect.New(kt).Elem()
if d.unmarshal(n.Content[i], k) { if d.unmarshal(n.Content[i], k) {
if mergedFields != nil {
ki := k.Interface()
if mergedFields[ki] {
continue
}
mergedFields[ki] = true
}
kkind := k.Kind() kkind := k.Kind()
if kkind == reflect.Interface { if kkind == reflect.Interface {
kkind = k.Elem().Kind() 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.stringMapType = stringMapType
d.generalMapType = generalMapType d.generalMapType = generalMapType
return true return true
@ -844,7 +867,8 @@ func isStringMap(n *Node) bool {
} }
l := len(n.Content) l := len(n.Content)
for i := 0; i < l; i += 2 { 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 return false
} }
} }
@ -861,7 +885,6 @@ func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) {
var elemType reflect.Type var elemType reflect.Type
if sinfo.InlineMap != -1 { if sinfo.InlineMap != -1 {
inlineMap = out.Field(sinfo.InlineMap) inlineMap = out.Field(sinfo.InlineMap)
inlineMap.Set(reflect.New(inlineMap.Type()).Elem())
elemType = 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) d.prepare(n, field)
} }
mergedFields := d.mergedFields
d.mergedFields = nil
var mergeNode *Node
var doneFields []bool var doneFields []bool
if d.uniqueKeys { if d.uniqueKeys {
doneFields = make([]bool, len(sinfo.FieldsList)) 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 { for i := 0; i < l; i += 2 {
ni := n.Content[i] ni := n.Content[i]
if isMerge(ni) { if isMerge(ni) {
d.merge(n.Content[i+1], out) mergeNode = n.Content[i+1]
continue continue
} }
if !d.unmarshal(ni, name) { if !d.unmarshal(ni, name) {
continue 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 d.uniqueKeys {
if doneFields[info.Id] { 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())) 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.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 return true
} }
@ -918,19 +956,29 @@ func failWantMap() {
failf("map merge requires map or sequence of maps as the value") failf("map merge requires map or sequence of maps as the value")
} }
func (d *decoder) merge(n *Node, out reflect.Value) { func (d *decoder) merge(parent *Node, merge *Node, out reflect.Value) {
switch n.Kind { 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: case MappingNode:
d.unmarshal(n, out) d.unmarshal(merge, out)
case AliasNode: case AliasNode:
if n.Alias != nil && n.Alias.Kind != MappingNode { if merge.Alias != nil && merge.Alias.Kind != MappingNode {
failWantMap() failWantMap()
} }
d.unmarshal(n, out) d.unmarshal(merge, out)
case SequenceNode: case SequenceNode:
// Step backwards as earlier nodes take precedence. for i := 0; i < len(merge.Content); i++ {
for i := len(n.Content) - 1; i >= 0; i-- { ni := merge.Content[i]
ni := n.Content[i]
if ni.Kind == AliasNode { if ni.Kind == AliasNode {
if ni.Alias != nil && ni.Alias.Kind != MappingNode { if ni.Alias != nil && ni.Alias.Kind != MappingNode {
failWantMap() failWantMap()
@ -943,6 +991,8 @@ func (d *decoder) merge(n *Node, out reflect.Value) {
default: default:
failWantMap() failWantMap()
} }
d.mergedFields = mergedFields
} }
func isMerge(n *Node) bool { func isMerge(n *Node) bool {

11
vendor/gopkg.in/yaml.v3/parserc.go generated vendored
View file

@ -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 { func yaml_parser_parse_block_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
if first { if first {
token := peek_token(parser) token := peek_token(parser)
if token == nil {
return false
}
parser.marks = append(parser.marks, token.start_mark) parser.marks = append(parser.marks, token.start_mark)
skip_token(parser) skip_token(parser)
} }
@ -786,7 +789,7 @@ func yaml_parser_split_stem_comment(parser *yaml_parser_t, stem_len int) {
} }
token := peek_token(parser) 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 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 { func yaml_parser_parse_block_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
if first { if first {
token := peek_token(parser) token := peek_token(parser)
if token == nil {
return false
}
parser.marks = append(parser.marks, token.start_mark) parser.marks = append(parser.marks, token.start_mark)
skip_token(parser) 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 { func yaml_parser_parse_flow_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool {
if first { if first {
token := peek_token(parser) token := peek_token(parser)
if token == nil {
return false
}
parser.marks = append(parser.marks, token.start_mark) parser.marks = append(parser.marks, token.start_mark)
skip_token(parser) skip_token(parser)
} }

10
vendor/modules.txt vendored
View file

@ -121,6 +121,10 @@ github.com/jesseduffield/gocui
# github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 # github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
## explicit; go 1.18 ## explicit; go 1.18
github.com/jesseduffield/kill 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 # github.com/jesseduffield/yaml v0.0.0-20190702115811-b900b7e08b56
## explicit ## explicit
github.com/jesseduffield/yaml github.com/jesseduffield/yaml
@ -169,7 +173,7 @@ github.com/pmezard/go-difflib/difflib
# github.com/rivo/uniseg v0.2.0 # github.com/rivo/uniseg v0.2.0
## explicit; go 1.12 ## explicit; go 1.12
github.com/rivo/uniseg github.com/rivo/uniseg
# github.com/samber/lo v1.20.0 # github.com/samber/lo v1.31.0
## explicit; go 1.18 ## explicit; go 1.18
github.com/samber/lo github.com/samber/lo
# github.com/sirupsen/logrus v1.4.2 # github.com/sirupsen/logrus v1.4.2
@ -178,7 +182,7 @@ github.com/sirupsen/logrus
# github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad # github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
## explicit ## explicit
github.com/spkg/bom github.com/spkg/bom
# github.com/stretchr/testify v1.7.0 # github.com/stretchr/testify v1.8.0
## explicit; go 1.13 ## explicit; go 1.13
github.com/stretchr/testify/assert github.com/stretchr/testify/assert
# github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 # github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
@ -213,7 +217,7 @@ golang.org/x/xerrors
golang.org/x/xerrors/internal golang.org/x/xerrors/internal
# gopkg.in/yaml.v2 v2.2.2 # gopkg.in/yaml.v2 v2.2.2
## explicit ## explicit
# gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b # gopkg.in/yaml.v3 v3.0.1
## explicit ## explicit
gopkg.in/yaml.v3 gopkg.in/yaml.v3
# gotest.tools/v3 v3.2.0 # gotest.tools/v3 v3.2.0