diff --git a/pkg/commands/docker.go b/pkg/commands/docker.go index 9b4e825f..e0accbfe 100644 --- a/pkg/commands/docker.go +++ b/pkg/commands/docker.go @@ -258,6 +258,7 @@ func (c *DockerCommand) RefreshContainersAndServices() error { c.Containers = containers c.Services = services c.DisplayContainers = c.filterOutExited(displayContainers) + c.DisplayContainers = c.sortedContainers(c.DisplayContainers) return nil } @@ -289,6 +290,27 @@ func (c *DockerCommand) filterOutExited(containers []*Container) []*Container { return toReturn } +// sortedContainers returns containers sorted by state if c.SortContainersByState is true (follows 1- running, 2- exited, 3- created) +// and sorted by name if c.SortContainersByState is false +func (c *DockerCommand) sortedContainers(containers []*Container) []*Container { + if !c.Config.UserConfig.Gui.LegacySortContainers { + states := map[string]int{ + "running": 1, + "exited": 2, + "created": 3, + } + sort.Slice(containers, func(i, j int) bool { + stateLeft := states[containers[i].Container.State] + stateRight := states[containers[j].Container.State] + if stateLeft == stateRight { + return containers[i].Name < containers[j].Name + } + return states[containers[i].Container.State] < states[containers[j].Container.State] + }) + } + return containers +} + // obtainStandaloneContainers returns standalone containers. Standalone containers are containers which are either one-off containers, or whose service is not part of this docker-compose context func (c *DockerCommand) obtainStandaloneContainers(containers []*Container, services []*Service) []*Container { standaloneContainers := []*Container{} diff --git a/pkg/commands/sort_container_test.go b/pkg/commands/sort_container_test.go new file mode 100644 index 00000000..5a8cb2a1 --- /dev/null +++ b/pkg/commands/sort_container_test.go @@ -0,0 +1,171 @@ +package commands + +import ( + "github.com/docker/docker/api/types" + "github.com/jesseduffield/lazydocker/pkg/config" + "github.com/stretchr/testify/assert" + "testing" +) + +func sampleContainers(userConfig *config.AppConfig) []*Container { + return []*Container{ + { + ID: "1", + Name: "1", + Container: types.Container{ + State: "exited", + }, + Config: userConfig, + }, + { + ID: "2", + Name: "2", + Container: types.Container{ + State: "running", + }, + Config: userConfig, + }, + { + ID: "3", + Name: "3", + Container: types.Container{ + State: "running", + }, + Config: userConfig, + }, + { + ID: "4", + Name: "4", + Container: types.Container{ + State: "created", + }, + Config: userConfig, + }, + } +} + +func expectedPerStatusContainers(appConfig *config.AppConfig) []*Container { + return []*Container{ + { + ID: "2", + Name: "2", + Container: types.Container{ + State: "running", + }, + Config: appConfig, + }, + { + ID: "3", + Name: "3", + Container: types.Container{ + State: "running", + }, + Config: appConfig, + }, + { + ID: "1", + Name: "1", + Container: types.Container{ + State: "exited", + }, + Config: appConfig, + }, + { + ID: "4", + Name: "4", + Container: types.Container{ + State: "created", + }, + Config: appConfig, + }, + } +} + +func expectedLegacySortedContainers(appConfig *config.AppConfig) []*Container { + return []*Container{ + { + ID: "1", + Name: "1", + Container: types.Container{ + State: "exited", + }, + Config: appConfig, + }, + { + ID: "2", + Name: "2", + Container: types.Container{ + State: "running", + }, + Config: appConfig, + }, + { + ID: "3", + Name: "3", + Container: types.Container{ + State: "running", + }, + Config: appConfig, + }, + { + ID: "4", + Name: "4", + Container: types.Container{ + State: "created", + }, + Config: appConfig, + }, + } +} + +func assertEqualContainers(t *testing.T, left *Container, right *Container) { + assert.Equal(t, left.Container.State, right.Container.State) + assert.Equal(t, left.Container.ID, right.Container.ID) + assert.Equal(t, left.Name, right.Name) +} + +func TestSortContainers(t *testing.T) { + appConfig := NewDummyAppConfig() + appConfig.UserConfig = &config.UserConfig{ + Gui: config.GuiConfig{ + LegacySortContainers: false, + }, + } + command := &DockerCommand{ + Config: appConfig, + } + + containers := sampleContainers(appConfig) + + sorted := expectedPerStatusContainers(appConfig) + + ct := command.sortedContainers(containers) + + assert.Equal(t, len(ct), len(sorted)) + + for i := 0; i < len(ct); i++ { + assertEqualContainers(t, sorted[i], ct[i]) + } +} + +func TestLegacySortedContainers(t *testing.T) { + appConfig := NewDummyAppConfig() + appConfig.UserConfig = &config.UserConfig{ + Gui: config.GuiConfig{ + LegacySortContainers: true, + }, + } + command := &DockerCommand{ + Config: appConfig, + } + + containers := sampleContainers(appConfig) + + sorted := expectedLegacySortedContainers(appConfig) + + ct := command.sortedContainers(containers) + + for i := 0; i < len(ct); i++ { + assertEqualContainers(t, sorted[i], ct[i]) + } +} diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index 0ede0554..3b65f827 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -20,7 +20,7 @@ import ( "time" "github.com/OpenPeeDeeP/xdg" - yaml "github.com/jesseduffield/yaml" + "github.com/jesseduffield/yaml" ) // UserConfig holds all of the user-configurable options @@ -106,6 +106,11 @@ type GuiConfig struct { // WrapMainPanel determines whether we use word wrap on the main panel WrapMainPanel bool `yaml:"wrapMainPanel,omitempty"` + + // LegacySortContainers determines if containers should be sorted using legacy approach. + // By default, containers are now sorted by status. This setting allows users to + // use legacy behaviour instead. + LegacySortContainers bool `yaml:"legacySortContainers,omitempty"` } // CommandTemplatesConfig determines what commands actually get called when we @@ -316,9 +321,10 @@ func GetDefaultConfig() UserConfig { InactiveBorderColor: []string{"default"}, OptionsTextColor: []string{"blue"}, }, - ShowAllContainers: false, - ReturnImmediately: false, - WrapMainPanel: false, + ShowAllContainers: false, + ReturnImmediately: false, + WrapMainPanel: false, + LegacySortContainers: false, }, ConfirmOnQuit: false, CommandTemplates: CommandTemplatesConfig{ diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 1e8d8a76..d875b768 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -82,6 +82,7 @@ type TranslationSet struct { RunCustomCommand string ViewBulkCommands string OpenInBrowser string + SortContainersByState string LogsTitle string ConfigTitle string @@ -120,42 +121,43 @@ func englishSet() TranslationSet { Donate: "Donate", Confirm: "Confirm", - Return: "return", - FocusMain: "focus main panel", - Navigate: "navigate", - Execute: "execute", - Close: "close", - Menu: "menu", - Scroll: "scroll", - OpenConfig: "open lazydocker config", - EditConfig: "edit lazydocker config", - Cancel: "cancel", - Remove: "remove", - HideStopped: "Hide/Show stopped containers", - ForceRemove: "force remove", - RemoveWithVolumes: "remove with volumes", - RemoveService: "remove containers", - Stop: "stop", - Restart: "restart", - Rebuild: "rebuild", - Recreate: "recreate", - PreviousContext: "previous tab", - NextContext: "next tab", - Attach: "attach", - ViewLogs: "view logs", - RemoveImage: "remove image", - RemoveVolume: "remove volume", - RemoveWithoutPrune: "remove without deleting untagged parents", - PruneContainers: "prune exited containers", - PruneVolumes: "prune unused volumes", - PruneImages: "prune unused images", - StopAllContainers: "stop all containers", - RemoveAllContainers: "remove all containers (forced)", - ViewRestartOptions: "view restart options", - ExecShell: "exec shell", - RunCustomCommand: "run predefined custom command", - ViewBulkCommands: "view bulk commands", - OpenInBrowser: "open in browser (first port is http)", + Return: "return", + FocusMain: "focus main panel", + Navigate: "navigate", + Execute: "execute", + Close: "close", + Menu: "menu", + Scroll: "scroll", + OpenConfig: "open lazydocker config", + EditConfig: "edit lazydocker config", + Cancel: "cancel", + Remove: "remove", + HideStopped: "Hide/Show stopped containers", + ForceRemove: "force remove", + RemoveWithVolumes: "remove with volumes", + RemoveService: "remove containers", + Stop: "stop", + Restart: "restart", + Rebuild: "rebuild", + Recreate: "recreate", + PreviousContext: "previous tab", + NextContext: "next tab", + Attach: "attach", + ViewLogs: "view logs", + RemoveImage: "remove image", + RemoveVolume: "remove volume", + RemoveWithoutPrune: "remove without deleting untagged parents", + PruneContainers: "prune exited containers", + PruneVolumes: "prune unused volumes", + PruneImages: "prune unused images", + StopAllContainers: "stop all containers", + RemoveAllContainers: "remove all containers (forced)", + ViewRestartOptions: "view restart options", + ExecShell: "exec shell", + RunCustomCommand: "run predefined custom command", + ViewBulkCommands: "view bulk commands", + OpenInBrowser: "open in browser (first port is http)", + SortContainersByState: "sort containers by state", GlobalTitle: "Global", MainTitle: "Main",