From dffe6970aa259d6d3fb11fda32b6dbdfb506bf39 Mon Sep 17 00:00:00 2001 From: Martin Cross <22754479+martini1992@users.noreply.github.com> Date: Fri, 21 Jul 2023 09:22:18 +0100 Subject: [PATCH] Short status (#419) Co-authored-by: Martin Cross --- docs/Config.md | 4 ++ pkg/config/app_config.go | 22 +++++---- pkg/gui/containers_panel.go | 4 +- pkg/gui/presentation/containers.go | 77 ++++++++++++++++++++++++++---- pkg/gui/presentation/services.go | 21 ++++++-- pkg/gui/services_panel.go | 4 +- pkg/utils/utils.go | 55 +++++++++++---------- 7 files changed, 135 insertions(+), 52 deletions(-) diff --git a/docs/Config.md b/docs/Config.md index 6e030dbca..07daa8f0e 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -38,6 +38,10 @@ gui: expandFocusedSidePanel: false # Determines which screen mode will be used on startup screenMode: "normal" # one of 'normal' | 'half' | 'fullscreen' + # Determines the style of the container status and container health display in the + # containers panel. "long": full words (default), "short": one or two characters, + # "icon": unicode emoji. + containerStatusHealthStyle: "long" logs: timestamps: false since: '60m' # set to '' to show all logs diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index d48399170..ff8fcbb0a 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -133,6 +133,11 @@ type GuiConfig struct { // ScreenMode allow user to specify which screen mode will be used on startup ScreenMode string `yaml:"screenMode,omitempty"` + + // Determines the style of the container status and container health display in the + // containers panel. "long": full words (default), "short": one or two characters, + // "icon": unicode emoji. + ContainerStatusHealthStyle string `yaml:"containerStatusHealthStyle"` } // CommandTemplatesConfig determines what commands actually get called when we @@ -358,14 +363,15 @@ func GetDefaultConfig() UserConfig { InactiveBorderColor: []string{"default"}, OptionsTextColor: []string{"blue"}, }, - ShowAllContainers: false, - ReturnImmediately: false, - WrapMainPanel: true, - LegacySortContainers: false, - SidePanelWidth: 0.3333, - ShowBottomLine: true, - ExpandFocusedSidePanel: false, - ScreenMode: "normal", + ShowAllContainers: false, + ReturnImmediately: false, + WrapMainPanel: true, + LegacySortContainers: false, + SidePanelWidth: 0.3333, + ShowBottomLine: true, + ExpandFocusedSidePanel: false, + ScreenMode: "normal", + ContainerStatusHealthStyle: "long", }, ConfirmOnQuit: false, Logs: LogsConfig{ diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index eaa222879..24965c6c0 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -96,7 +96,9 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container] return true }, - GetTableCells: presentation.GetContainerDisplayStrings, + GetTableCells: func(container *commands.Container) []string { + return presentation.GetContainerDisplayStrings(&gui.Config.UserConfig.Gui, container) + }, } } diff --git a/pkg/gui/presentation/containers.go b/pkg/gui/presentation/containers.go index e1a9d3098..f58fc706c 100644 --- a/pkg/gui/presentation/containers.go +++ b/pkg/gui/presentation/containers.go @@ -9,14 +9,15 @@ import ( dockerTypes "github.com/docker/docker/api/types" "github.com/fatih/color" "github.com/jesseduffield/lazydocker/pkg/commands" + "github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/utils" "github.com/samber/lo" ) -func GetContainerDisplayStrings(container *commands.Container) []string { +func GetContainerDisplayStrings(guiConfig *config.GuiConfig, container *commands.Container) []string { return []string{ - getContainerDisplayStatus(container), - getContainerDisplaySubstatus(container), + getContainerDisplayStatus(guiConfig, container), + getContainerDisplaySubstatus(guiConfig, container), container.Name, getDisplayCPUPerc(container), utils.ColoredString(displayPorts(container), color.FgYellow), @@ -52,12 +53,44 @@ func displayPorts(c *commands.Container) string { } // getContainerDisplayStatus returns the colored status of the container -func getContainerDisplayStatus(c *commands.Container) string { - return utils.ColoredString(c.Container.State, getContainerColor(c)) +func getContainerDisplayStatus(guiConfig *config.GuiConfig, c *commands.Container) string { + shortStatusMap := map[string]string{ + "paused": "P", + "exited": "X", + "created": "C", + "removing": "RM", + "restarting": "RS", + "running": "R", + "dead": "D", + } + + iconStatusMap := map[string]rune{ + "paused": '◫', + "exited": '⨯', + "created": '+', + "removing": '−', + "restarting": '⟳', + "running": '▶', + "dead": '!', + } + + var containerState string + switch guiConfig.ContainerStatusHealthStyle { + case "short": + containerState = shortStatusMap[c.Container.State] + case "icon": + containerState = string(iconStatusMap[c.Container.State]) + case "long": + fallthrough + default: + containerState = c.Container.State + } + + return utils.ColoredString(containerState, getContainerColor(c)) } // GetDisplayStatus returns the exit code if the container has exited, and the health status if the container is running (and has a health check) -func getContainerDisplaySubstatus(c *commands.Container) string { +func getContainerDisplaySubstatus(guiConfig *config.GuiConfig, c *commands.Container) string { if !c.DetailsLoaded() { return "" } @@ -68,13 +101,13 @@ func getContainerDisplaySubstatus(c *commands.Container) string { fmt.Sprintf("(%s)", strconv.Itoa(c.Details.State.ExitCode)), getContainerColor(c), ) case "running": - return getHealthStatus(c) + return getHealthStatus(guiConfig, c) default: return "" } } -func getHealthStatus(c *commands.Container) string { +func getHealthStatus(guiConfig *config.GuiConfig, c *commands.Container) string { if !c.DetailsLoaded() { return "" } @@ -88,8 +121,32 @@ func getHealthStatus(c *commands.Container) string { if c.Details.State.Health == nil { return "" } - healthStatus := c.Details.State.Health.Status - if healthStatusColor, ok := healthStatusColorMap[healthStatus]; ok { + + shortHealthStatusMap := map[string]string{ + "healthy": "H", + "unhealthy": "U", + "starting": "S", + } + + iconHealthStatusMap := map[string]rune{ + "healthy": '✔', + "unhealthy": '?', + "starting": '…', + } + + var healthStatus string + switch guiConfig.ContainerStatusHealthStyle { + case "short": + healthStatus = shortHealthStatusMap[c.Details.State.Health.Status] + case "icon": + healthStatus = string(iconHealthStatusMap[c.Details.State.Health.Status]) + case "long": + fallthrough + default: + healthStatus = c.Details.State.Health.Status + } + + if healthStatusColor, ok := healthStatusColorMap[c.Details.State.Health.Status]; ok { return utils.ColoredString(fmt.Sprintf("(%s)", healthStatus), healthStatusColor) } return "" diff --git a/pkg/gui/presentation/services.go b/pkg/gui/presentation/services.go index c99f1586c..7e781ec16 100644 --- a/pkg/gui/presentation/services.go +++ b/pkg/gui/presentation/services.go @@ -3,13 +3,26 @@ package presentation import ( "github.com/fatih/color" "github.com/jesseduffield/lazydocker/pkg/commands" + "github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/utils" ) -func GetServiceDisplayStrings(service *commands.Service) []string { +func GetServiceDisplayStrings(guiConfig *config.GuiConfig, service *commands.Service) []string { if service.Container == nil { + var containerState string + switch guiConfig.ContainerStatusHealthStyle { + case "short": + containerState = "n" + case "icon": + containerState = "." + case "long": + fallthrough + default: + containerState = "none" + } + return []string{ - utils.ColoredString("none", color.FgBlue), + utils.ColoredString(containerState, color.FgBlue), "", service.Name, "", @@ -20,8 +33,8 @@ func GetServiceDisplayStrings(service *commands.Service) []string { container := service.Container return []string{ - getContainerDisplayStatus(container), - getContainerDisplaySubstatus(container), + getContainerDisplayStatus(guiConfig, container), + getContainerDisplaySubstatus(guiConfig, container), service.Name, getDisplayCPUPerc(container), utils.ColoredString(displayPorts(container), color.FgYellow), diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index 2c1caaec7..c9dd4d1d8 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -74,7 +74,9 @@ func (gui *Gui) getServicesPanel() *panels.SideListPanel[*commands.Service] { return a.Name < b.Name }, - GetTableCells: presentation.GetServiceDisplayStrings, + GetTableCells: func(service *commands.Service) []string { + return presentation.GetServiceDisplayStrings(&gui.Config.UserConfig.Gui, service) + }, Hide: func() bool { return !gui.DockerCommand.InDockerComposeProject }, diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index b9eb801f5..0cd9a7cad 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -14,6 +14,7 @@ import ( "github.com/go-errors/errors" "github.com/jesseduffield/gocui" + "github.com/mattn/go-runewidth" // "github.com/jesseduffield/yaml" @@ -41,10 +42,10 @@ func SplitLines(multilineString string) []string { // WithPadding pads a string as much as you want func WithPadding(str string, padding int) string { uncoloredStr := Decolorise(str) - if padding < len(uncoloredStr) { + if padding < runewidth.StringWidth(uncoloredStr) { return str } - return str + strings.Repeat(" ", padding-len(uncoloredStr)) + return str + strings.Repeat(" ", padding-runewidth.StringWidth(uncoloredStr)) } // ColoredString takes a string and a colour attribute and returns a colored @@ -143,54 +144,52 @@ func Max(x, y int) int { } // RenderTable takes an array of string arrays and returns a table containing the values -func RenderTable(stringArrays [][]string) (string, error) { - if len(stringArrays) == 0 { +func RenderTable(rows [][]string) (string, error) { + if len(rows) == 0 { return "", nil } - if !displayArraysAligned(stringArrays) { + if !displayArraysAligned(rows) { return "", errors.New("Each item must return the same number of strings to display") } - padWidths := getPadWidths(stringArrays) - paddedDisplayStrings := getPaddedDisplayStrings(stringArrays, padWidths) + columnPadWidths := getPadWidths(rows) + paddedDisplayRows := getPaddedDisplayStrings(rows, columnPadWidths) - return strings.Join(paddedDisplayStrings, "\n"), nil + return strings.Join(paddedDisplayRows, "\n"), nil } // Decolorise strips a string of color func Decolorise(str string) string { - re := regexp.MustCompile(`\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]`) + re := regexp.MustCompile(`\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mK]`) return re.ReplaceAllString(str, "") } -func getPadWidths(stringArrays [][]string) []int { - if len(stringArrays[0]) <= 1 { +func getPadWidths(rows [][]string) []int { + if len(rows[0]) <= 1 { return []int{} } - padWidths := make([]int, len(stringArrays[0])-1) - for i := range padWidths { - for _, strings := range stringArrays { - uncoloredString := Decolorise(strings[i]) - if len(uncoloredString) > padWidths[i] { - padWidths[i] = len(uncoloredString) + columnPadWidths := make([]int, len(rows[0])-1) + for i := range columnPadWidths { + for _, cells := range rows { + uncoloredCell := Decolorise(cells[i]) + + if runewidth.StringWidth(uncoloredCell) > columnPadWidths[i] { + columnPadWidths[i] = runewidth.StringWidth(uncoloredCell) } } } - return padWidths + return columnPadWidths } -func getPaddedDisplayStrings(stringArrays [][]string, padWidths []int) []string { - paddedDisplayStrings := make([]string, len(stringArrays)) - for i, stringArray := range stringArrays { - if len(stringArray) == 0 { - continue - } - for j, padWidth := range padWidths { - paddedDisplayStrings[i] += WithPadding(stringArray[j], padWidth) + " " +func getPaddedDisplayStrings(rows [][]string, columnPadWidths []int) []string { + paddedDisplayRows := make([]string, len(rows)) + for i, cells := range rows { + for j, columnPadWidth := range columnPadWidths { + paddedDisplayRows[i] += WithPadding(cells[j], columnPadWidth) + " " } - paddedDisplayStrings[i] += stringArray[len(padWidths)] + paddedDisplayRows[i] += cells[len(columnPadWidths)] } - return paddedDisplayStrings + return paddedDisplayRows } // displayArraysAligned returns true if every string array returned from our