package presentation import ( "fmt" "sort" "strconv" "strings" "github.com/docker/docker/api/types/container" "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(guiConfig *config.GuiConfig, container *commands.Container) []string { return []string{ getContainerDisplayStatus(guiConfig, container), getContainerDisplaySubstatus(guiConfig, container), container.Name, getDisplayCPUPerc(container), utils.ColoredString(displayPorts(container), color.FgYellow), utils.ColoredString(displayContainerImage(container), color.FgMagenta), } } func displayContainerImage(container *commands.Container) string { return strings.TrimPrefix(container.Container.Image, "sha256:") } func displayPorts(c *commands.Container) string { portStrings := lo.Map(c.Container.Ports, func(port container.Port, _ int) string { if port.PublicPort == 0 { return fmt.Sprintf("%d/%s", port.PrivatePort, port.Type) } // docker ps will show '0.0.0.0:80->80/tcp' but we'll show // '80->80/tcp' instead to save space (unless the IP is something other than // 0.0.0.0) ipString := "" if port.IP != "0.0.0.0" { ipString = port.IP + ":" } return fmt.Sprintf("%s%d->%d/%s", ipString, port.PublicPort, port.PrivatePort, port.Type) }) // sorting because the order of the ports is not deterministic // and we don't want to have them constantly swapping sort.Strings(portStrings) return strings.Join(portStrings, ", ") } // getContainerDisplayStatus returns the colored status of the container 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(guiConfig *config.GuiConfig, c *commands.Container) string { if !c.DetailsLoaded() { return "" } switch c.Container.State { case "exited": return utils.ColoredString( fmt.Sprintf("(%s)", strconv.Itoa(c.Details.State.ExitCode)), getContainerColor(c), ) case "running": return getHealthStatus(guiConfig, c) default: return "" } } func getHealthStatus(guiConfig *config.GuiConfig, c *commands.Container) string { if !c.DetailsLoaded() { return "" } healthStatusColorMap := map[string]color.Attribute{ "healthy": color.FgGreen, "unhealthy": color.FgRed, "starting": color.FgYellow, } if c.Details.State.Health == nil { return "" } 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 "" } // getDisplayCPUPerc colors the cpu percentage based on how extreme it is func getDisplayCPUPerc(c *commands.Container) string { stats, ok := c.GetLastStats() if !ok { return "" } percentage := stats.DerivedStats.CPUPercentage formattedPercentage := fmt.Sprintf("%.2f%%", stats.DerivedStats.CPUPercentage) var clr color.Attribute if percentage > 90 { clr = color.FgRed } else if percentage > 50 { clr = color.FgYellow } else { clr = color.FgWhite } return utils.ColoredString(formattedPercentage, clr) } // getContainerColor Container color func getContainerColor(c *commands.Container) color.Attribute { switch c.Container.State { case "exited": // This means the colour may be briefly yellow and then switch to red upon starting // Not sure what a better alternative is. if !c.DetailsLoaded() || c.Details.State.ExitCode == 0 { return color.FgYellow } return color.FgRed case "created": return color.FgCyan case "running": return color.FgGreen case "paused": return color.FgYellow case "dead": return color.FgRed case "restarting": return color.FgBlue case "removing": return color.FgMagenta default: return color.FgWhite } }