mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-21 23:01:03 +00:00
move things into presentation package
This commit is contained in:
parent
fc592b5806
commit
a87b698761
24 changed files with 311 additions and 254 deletions
|
|
@ -4,18 +4,14 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
||||
|
|
@ -34,13 +30,13 @@ type Container struct {
|
|||
OneOff bool
|
||||
ProjectName string
|
||||
ID string
|
||||
Container types.Container
|
||||
Container dockerTypes.Container
|
||||
Client *client.Client
|
||||
OSCommand *OSCommand
|
||||
Config *config.AppConfig
|
||||
Log *logrus.Entry
|
||||
StatHistory []*RecordedStats
|
||||
Details types.ContainerJSON
|
||||
Details dockerTypes.ContainerJSON
|
||||
MonitoringStats bool
|
||||
DockerCommand LimitedDockerCommand
|
||||
Tr *i18n.TranslationSet
|
||||
|
|
@ -48,126 +44,8 @@ type Container struct {
|
|||
StatsMutex deadlock.Mutex
|
||||
}
|
||||
|
||||
// TODO: move this stuff into a presentation layer
|
||||
|
||||
func (c *Container) DisplayPorts() string {
|
||||
portStrings := lo.Map(c.Container.Ports, func(port types.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, ", ")
|
||||
}
|
||||
|
||||
// GetDisplayStatus returns the colored status of the container
|
||||
func (c *Container) GetDisplayStatus() string {
|
||||
return utils.ColoredString(c.Container.State, c.GetColor())
|
||||
}
|
||||
|
||||
// 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 (c *Container) GetDisplaySubstatus() string {
|
||||
if !c.DetailsLoaded() {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch c.Container.State {
|
||||
case "exited":
|
||||
return utils.ColoredString(
|
||||
fmt.Sprintf("(%s)", strconv.Itoa(c.Details.State.ExitCode)), c.GetColor(),
|
||||
)
|
||||
case "running":
|
||||
return c.getHealthStatus()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) getHealthStatus() 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 ""
|
||||
}
|
||||
healthStatus := c.Details.State.Health.Status
|
||||
if healthStatusColor, ok := healthStatusColorMap[healthStatus]; ok {
|
||||
return utils.ColoredString(fmt.Sprintf("(%s)", healthStatus), healthStatusColor)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetDisplayCPUPerc colors the cpu percentage based on how extreme it is
|
||||
func (c *Container) GetDisplayCPUPerc() 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)
|
||||
}
|
||||
|
||||
// GetColor Container color
|
||||
func (c *Container) GetColor() 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
|
||||
}
|
||||
}
|
||||
|
||||
// Remove removes the container
|
||||
func (c *Container) Remove(options types.ContainerRemoveOptions) error {
|
||||
func (c *Container) Remove(options dockerTypes.ContainerRemoveOptions) error {
|
||||
c.Log.Warn(fmt.Sprintf("removing container %s", c.Name))
|
||||
if err := c.Client.ContainerRemove(context.Background(), c.ID, options); err != nil {
|
||||
if strings.Contains(err.Error(), "Stop the container before attempting removal or force remove") {
|
||||
|
|
@ -250,7 +128,7 @@ func (c *DockerCommand) PruneContainers() error {
|
|||
}
|
||||
|
||||
// Inspect returns details about the container
|
||||
func (c *Container) Inspect() (types.ContainerJSON, error) {
|
||||
func (c *Container) Inspect() (dockerTypes.ContainerJSON, error) {
|
||||
return c.Client.ContainerInspect(context.Background(), c.ID)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ func (s *ContainerStats) CalculateContainerMemoryUsage() float64 {
|
|||
|
||||
// RenderStats returns a string containing the rendered stats of the container
|
||||
func (c *Container) RenderStats(viewWidth int) (string, error) {
|
||||
stats, ok := c.getLastStats()
|
||||
stats, ok := c.GetLastStats()
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
|
|
@ -225,7 +225,7 @@ func (c *Container) eraseOldHistory() {
|
|||
}
|
||||
}
|
||||
|
||||
func (c *Container) getLastStats() (*RecordedStats, bool) {
|
||||
func (c *Container) GetLastStats() (*RecordedStats, bool) {
|
||||
c.StatsMutex.Lock()
|
||||
defer c.StatsMutex.Unlock()
|
||||
history := c.StatHistory
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands/ssh"
|
||||
|
|
@ -189,7 +189,7 @@ func (c *DockerCommand) GetContainers(existingContainers []*Container) ([]*Conta
|
|||
c.ContainerMutex.Lock()
|
||||
defer c.ContainerMutex.Unlock()
|
||||
|
||||
containers, err := c.Client.ContainerList(context.Background(), types.ContainerListOptions{All: true})
|
||||
containers, err := c.Client.ContainerList(context.Background(), dockerTypes.ContainerListOptions{All: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/fatih/color"
|
||||
|
|
@ -20,7 +20,7 @@ type Image struct {
|
|||
Name string
|
||||
Tag string
|
||||
ID string
|
||||
Image types.ImageSummary
|
||||
Image dockerTypes.ImageSummary
|
||||
Client *client.Client
|
||||
OSCommand *OSCommand
|
||||
Log *logrus.Entry
|
||||
|
|
@ -28,7 +28,7 @@ type Image struct {
|
|||
}
|
||||
|
||||
// Remove removes the image
|
||||
func (i *Image) Remove(options types.ImageRemoveOptions) error {
|
||||
func (i *Image) Remove(options dockerTypes.ImageRemoveOptions) error {
|
||||
if _, err := i.Client.ImageRemove(context.Background(), i.ID, options); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -94,7 +94,7 @@ func (i *Image) RenderHistory() (string, error) {
|
|||
|
||||
// RefreshImages returns a slice of docker images
|
||||
func (c *DockerCommand) RefreshImages() ([]*Image, error) {
|
||||
images, err := c.Client.ImageList(context.Background(), types.ImageListOptions{})
|
||||
images, err := c.Client.ImageList(context.Background(), dockerTypes.ImageListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import (
|
|||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
|
@ -21,7 +21,7 @@ type Service struct {
|
|||
}
|
||||
|
||||
// Remove removes the service's containers
|
||||
func (s *Service) Remove(options types.ContainerRemoveOptions) error {
|
||||
func (s *Service) Remove(options dockerTypes.ContainerRemoveOptions) error {
|
||||
return s.Container.Remove(options)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package commands
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
|
@ -12,7 +12,7 @@ import (
|
|||
// Volume : A docker Volume
|
||||
type Volume struct {
|
||||
Name string
|
||||
Volume *types.Volume
|
||||
Volume *dockerTypes.Volume
|
||||
Client *client.Client
|
||||
OSCommand *OSCommand
|
||||
Log *logrus.Entry
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"os/signal"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
|
|
@ -107,7 +107,7 @@ func (gui *Gui) promptToReturn() {
|
|||
}
|
||||
|
||||
func (gui *Gui) writeContainerLogs(container *commands.Container, ctx context.Context, writer io.Writer) error {
|
||||
readCloser, err := gui.DockerCommand.Client.ContainerLogs(ctx, container.ID, types.ContainerLogsOptions{
|
||||
readCloser, err := gui.DockerCommand.Client.ContainerLogs(ctx, container.ID, dockerTypes.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Timestamps: gui.Config.UserConfig.Logs.Timestamps,
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
|
@ -88,18 +90,7 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container]
|
|||
|
||||
return true
|
||||
},
|
||||
GetDisplayStrings: func(container *commands.Container) []string {
|
||||
image := strings.TrimPrefix(container.Container.Image, "sha256:")
|
||||
|
||||
return []string{
|
||||
container.GetDisplayStatus(),
|
||||
container.GetDisplaySubstatus(),
|
||||
container.Name,
|
||||
container.GetDisplayCPUPerc(),
|
||||
utils.ColoredString(image, color.FgMagenta),
|
||||
utils.ColoredString(container.DisplayPorts(), color.FgYellow),
|
||||
}
|
||||
},
|
||||
GetDisplayStrings: presentation.GetContainerDisplayStrings,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -318,7 +309,7 @@ func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
handleMenuPress := func(configOptions types.ContainerRemoveOptions) error {
|
||||
handleMenuPress := func(configOptions dockerTypes.ContainerRemoveOptions) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
||||
if err := container.Remove(configOptions); err != nil {
|
||||
if commands.HasErrorCode(err, commands.MustStopContainer) {
|
||||
|
|
@ -335,14 +326,14 @@ func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
|||
})
|
||||
}
|
||||
|
||||
menuItems := []*MenuItem{
|
||||
menuItems := []*types.MenuItem{
|
||||
{
|
||||
LabelColumns: []string{gui.Tr.Remove, "docker rm " + container.ID[1:10]},
|
||||
OnPress: func() error { return handleMenuPress(types.ContainerRemoveOptions{}) },
|
||||
OnPress: func() error { return handleMenuPress(dockerTypes.ContainerRemoveOptions{}) },
|
||||
},
|
||||
{
|
||||
LabelColumns: []string{gui.Tr.RemoveWithVolumes, "docker rm --volumes " + container.ID[1:10]},
|
||||
OnPress: func() error { return handleMenuPress(types.ContainerRemoveOptions{RemoveVolumes: true}) },
|
||||
OnPress: func() error { return handleMenuPress(dockerTypes.ContainerRemoveOptions{RemoveVolumes: true}) },
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -500,7 +491,7 @@ func (gui *Gui) handleRemoveContainers() error {
|
|||
return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmRemoveContainers, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
||||
for _, container := range gui.Panels.Containers.List.GetAllItems() {
|
||||
if err := container.Remove(types.ContainerRemoveOptions{Force: true}); err != nil {
|
||||
if err := container.Remove(dockerTypes.ContainerRemoveOptions{Force: true}); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ import (
|
|||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func (gui *Gui) createCommandMenu(customCommands []config.CustomCommand, commandObject commands.CommandObject, title string, waitingStatus string) error {
|
||||
menuItems := lo.Map(customCommands, func(command config.CustomCommand, _ int) *MenuItem {
|
||||
menuItems := lo.Map(customCommands, func(command config.CustomCommand, _ int) *types.MenuItem {
|
||||
resolvedCommand := utils.ApplyTemplate(command.Command, commandObject)
|
||||
|
||||
onPress := func() error {
|
||||
|
|
@ -31,7 +32,7 @@ func (gui *Gui) createCommandMenu(customCommands []config.CustomCommand, command
|
|||
})
|
||||
}
|
||||
|
||||
return &MenuItem{
|
||||
return &types.MenuItem{
|
||||
LabelColumns: []string{
|
||||
command.Name,
|
||||
utils.ColoredString(utils.WithShortSha(resolvedCommand), color.FgCyan),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/i18n"
|
||||
"github.com/jesseduffield/lazydocker/pkg/tasks"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
|
|
@ -55,7 +56,7 @@ type Panels struct {
|
|||
Containers *panels.SideListPanel[*commands.Container]
|
||||
Images *panels.SideListPanel[*commands.Image]
|
||||
Volumes *panels.SideListPanel[*commands.Volume]
|
||||
Menu *panels.SideListPanel[*MenuItem]
|
||||
Menu *panels.SideListPanel[*types.MenuItem]
|
||||
}
|
||||
|
||||
type Mutexes struct {
|
||||
|
|
@ -309,7 +310,7 @@ func (gui *Gui) listenForEvents(ctx context.Context, refresh func()) {
|
|||
|
||||
outer:
|
||||
for {
|
||||
messageChan, errChan := gui.DockerCommand.Client.Events(context.Background(), types.EventsOptions{})
|
||||
messageChan, errChan := gui.DockerCommand.Client.Events(context.Background(), dockerTypes.EventsOptions{})
|
||||
|
||||
if errorCount > 0 {
|
||||
select {
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
|
@ -60,13 +62,7 @@ func (gui *Gui) getImagesPanel() *panels.SideListPanel[*commands.Image] {
|
|||
|
||||
return a.ID < b.ID
|
||||
},
|
||||
GetDisplayStrings: func(image *commands.Image) []string {
|
||||
return []string{
|
||||
image.Name,
|
||||
image.Tag,
|
||||
utils.FormatDecimalBytes(int(image.Image.Size)),
|
||||
}
|
||||
},
|
||||
GetDisplayStrings: presentation.GetImageDisplayStrings,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -126,7 +122,7 @@ func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
|||
type removeImageOption struct {
|
||||
description string
|
||||
command string
|
||||
configOptions types.ImageRemoveOptions
|
||||
configOptions dockerTypes.ImageRemoveOptions
|
||||
}
|
||||
|
||||
image, err := gui.Panels.Images.GetSelectedItem()
|
||||
|
|
@ -141,27 +137,27 @@ func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
|||
{
|
||||
description: gui.Tr.Remove,
|
||||
command: "docker image rm " + shortSha,
|
||||
configOptions: types.ImageRemoveOptions{PruneChildren: true, Force: false},
|
||||
configOptions: dockerTypes.ImageRemoveOptions{PruneChildren: true, Force: false},
|
||||
},
|
||||
{
|
||||
description: gui.Tr.RemoveWithoutPrune,
|
||||
command: "docker image rm --no-prune " + shortSha,
|
||||
configOptions: types.ImageRemoveOptions{PruneChildren: false, Force: false},
|
||||
configOptions: dockerTypes.ImageRemoveOptions{PruneChildren: false, Force: false},
|
||||
},
|
||||
{
|
||||
description: gui.Tr.RemoveWithForce,
|
||||
command: "docker image rm --force " + shortSha,
|
||||
configOptions: types.ImageRemoveOptions{PruneChildren: true, Force: true},
|
||||
configOptions: dockerTypes.ImageRemoveOptions{PruneChildren: true, Force: true},
|
||||
},
|
||||
{
|
||||
description: gui.Tr.RemoveWithoutPruneWithForce,
|
||||
command: "docker image rm --no-prune --force " + shortSha,
|
||||
configOptions: types.ImageRemoveOptions{PruneChildren: false, Force: true},
|
||||
configOptions: dockerTypes.ImageRemoveOptions{PruneChildren: false, Force: true},
|
||||
},
|
||||
}
|
||||
|
||||
menuItems := lo.Map(options, func(option *removeImageOption, _ int) *MenuItem {
|
||||
return &MenuItem{
|
||||
menuItems := lo.Map(options, func(option *removeImageOption, _ int) *types.MenuItem {
|
||||
return &types.MenuItem{
|
||||
LabelColumns: []string{
|
||||
option.description,
|
||||
color.New(color.FgRed).Sprint(option.command),
|
||||
|
|
|
|||
|
|
@ -2,40 +2,28 @@ package gui
|
|||
|
||||
import (
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
)
|
||||
|
||||
type MenuItem struct {
|
||||
Label string
|
||||
|
||||
// alternative to Label. Allows specifying columns which will be auto-aligned
|
||||
LabelColumns []string
|
||||
|
||||
OnPress func() error
|
||||
|
||||
// Only applies when Label is used
|
||||
OpensMenu bool
|
||||
}
|
||||
|
||||
type CreateMenuOptions struct {
|
||||
Title string
|
||||
Items []*MenuItem
|
||||
Items []*types.MenuItem
|
||||
HideCancel bool
|
||||
}
|
||||
|
||||
func (gui *Gui) getMenuPanel() *panels.SideListPanel[*MenuItem] {
|
||||
return &panels.SideListPanel[*MenuItem]{
|
||||
ListPanel: panels.ListPanel[*MenuItem]{
|
||||
List: panels.NewFilteredList[*MenuItem](),
|
||||
func (gui *Gui) getMenuPanel() *panels.SideListPanel[*types.MenuItem] {
|
||||
return &panels.SideListPanel[*types.MenuItem]{
|
||||
ListPanel: panels.ListPanel[*types.MenuItem]{
|
||||
List: panels.NewFilteredList[*types.MenuItem](),
|
||||
View: gui.Views.Menu,
|
||||
},
|
||||
NoItemsMessage: "",
|
||||
Gui: gui.intoInterface(),
|
||||
OnClick: gui.onMenuPress,
|
||||
Sort: nil,
|
||||
GetDisplayStrings: func(menuItem *MenuItem) []string {
|
||||
return menuItem.LabelColumns
|
||||
},
|
||||
NoItemsMessage: "",
|
||||
Gui: gui.intoInterface(),
|
||||
OnClick: gui.onMenuPress,
|
||||
Sort: nil,
|
||||
GetDisplayStrings: presentation.GetMenuItemDisplayStrings,
|
||||
OnRerender: func() error {
|
||||
return gui.resizePopupPanel(gui.Views.Menu)
|
||||
},
|
||||
|
|
@ -47,7 +35,7 @@ func (gui *Gui) getMenuPanel() *panels.SideListPanel[*MenuItem] {
|
|||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) onMenuPress(menuItem *MenuItem) error {
|
||||
func (gui *Gui) onMenuPress(menuItem *types.MenuItem) error {
|
||||
if err := gui.handleMenuClose(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -71,7 +59,7 @@ func (gui *Gui) handleMenuPress() error {
|
|||
func (gui *Gui) Menu(opts CreateMenuOptions) error {
|
||||
if !opts.HideCancel {
|
||||
// this is mutative but I'm okay with that for now
|
||||
opts.Items = append(opts.Items, &MenuItem{
|
||||
opts.Items = append(opts.Items, &types.MenuItem{
|
||||
LabelColumns: []string{gui.Tr.Cancel},
|
||||
OnPress: func() error {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"github.com/samber/lo"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
)
|
||||
|
||||
func (gui *Gui) getBindings(v *gocui.View) []*Binding {
|
||||
|
|
@ -55,8 +56,8 @@ func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
menuItems := lo.Map(gui.getBindings(v), func(binding *Binding, _ int) *MenuItem {
|
||||
return &MenuItem{
|
||||
menuItems := lo.Map(gui.getBindings(v), func(binding *Binding, _ int) *types.MenuItem {
|
||||
return &types.MenuItem{
|
||||
LabelColumns: []string{binding.GetKey(), binding.Description},
|
||||
OnPress: func() error {
|
||||
if binding.Key == nil {
|
||||
|
|
|
|||
143
pkg/gui/presentation/containers.go
Normal file
143
pkg/gui/presentation/containers.go
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
package presentation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func GetContainerDisplayStrings(container *commands.Container) []string {
|
||||
image := strings.TrimPrefix(container.Container.Image, "sha256:")
|
||||
|
||||
return []string{
|
||||
getContainerDisplayStatus(container),
|
||||
getContainerDisplaySubstatus(container),
|
||||
container.Name,
|
||||
getDisplayCPUPerc(container),
|
||||
utils.ColoredString(image, color.FgMagenta),
|
||||
utils.ColoredString(displayPorts(container), color.FgYellow),
|
||||
}
|
||||
}
|
||||
|
||||
func displayPorts(c *commands.Container) string {
|
||||
portStrings := lo.Map(c.Container.Ports, func(port dockerTypes.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(c *commands.Container) string {
|
||||
return utils.ColoredString(c.Container.State, 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 {
|
||||
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(c)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func getHealthStatus(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 ""
|
||||
}
|
||||
healthStatus := c.Details.State.Health.Status
|
||||
if healthStatusColor, ok := healthStatusColorMap[healthStatus]; 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
|
||||
}
|
||||
}
|
||||
14
pkg/gui/presentation/images.go
Normal file
14
pkg/gui/presentation/images.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package presentation
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
)
|
||||
|
||||
func GetImageDisplayStrings(image *commands.Image) []string {
|
||||
return []string{
|
||||
image.Name,
|
||||
image.Tag,
|
||||
utils.FormatDecimalBytes(int(image.Image.Size)),
|
||||
}
|
||||
}
|
||||
7
pkg/gui/presentation/menu_items.go
Normal file
7
pkg/gui/presentation/menu_items.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package presentation
|
||||
|
||||
import "github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
|
||||
func GetMenuItemDisplayStrings(menuItem *types.MenuItem) []string {
|
||||
return menuItem.LabelColumns
|
||||
}
|
||||
7
pkg/gui/presentation/projects.go
Normal file
7
pkg/gui/presentation/projects.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package presentation
|
||||
|
||||
import "github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
|
||||
func GetProjectDisplayStrings(project *commands.Project) []string {
|
||||
return []string{project.Name}
|
||||
}
|
||||
28
pkg/gui/presentation/services.go
Normal file
28
pkg/gui/presentation/services.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package presentation
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
)
|
||||
|
||||
func GetServiceDisplayStrings(service *commands.Service) []string {
|
||||
if service.Container == nil {
|
||||
return []string{
|
||||
utils.ColoredString("none", color.FgBlue),
|
||||
"",
|
||||
service.Name,
|
||||
"",
|
||||
"",
|
||||
}
|
||||
}
|
||||
|
||||
container := service.Container
|
||||
return []string{
|
||||
getContainerDisplayStatus(container),
|
||||
getContainerDisplaySubstatus(container),
|
||||
service.Name,
|
||||
getDisplayCPUPerc(container),
|
||||
utils.ColoredString(displayPorts(container), color.FgYellow),
|
||||
}
|
||||
}
|
||||
7
pkg/gui/presentation/volumes.go
Normal file
7
pkg/gui/presentation/volumes.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package presentation
|
||||
|
||||
import "github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
|
||||
func GetVolumeDisplayStrings(volume *commands.Volume) []string {
|
||||
return []string{volume.Volume.Driver, volume.Name}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/jesseduffield/yaml"
|
||||
)
|
||||
|
|
@ -63,9 +64,7 @@ func (gui *Gui) getProjectPanel() *panels.SideListPanel[*commands.Project] {
|
|||
Sort: func(a *commands.Project, b *commands.Project) bool {
|
||||
return false
|
||||
},
|
||||
GetDisplayStrings: func(project *commands.Project) []string {
|
||||
return []string{project.Name}
|
||||
},
|
||||
GetDisplayStrings: presentation.GetProjectDisplayStrings,
|
||||
// It doesn't make sense to filter a list of only one item.
|
||||
DisableFilter: true,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import (
|
|||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
|
@ -70,26 +72,7 @@ func (gui *Gui) getServicesPanel() *panels.SideListPanel[*commands.Service] {
|
|||
|
||||
return a.Name < b.Name
|
||||
},
|
||||
GetDisplayStrings: func(service *commands.Service) []string {
|
||||
if service.Container == nil {
|
||||
return []string{
|
||||
utils.ColoredString("none", color.FgBlue),
|
||||
"",
|
||||
service.Name,
|
||||
"",
|
||||
"",
|
||||
}
|
||||
}
|
||||
|
||||
cont := service.Container
|
||||
return []string{
|
||||
cont.GetDisplayStatus(),
|
||||
cont.GetDisplaySubstatus(),
|
||||
service.Name,
|
||||
cont.GetDisplayCPUPerc(),
|
||||
utils.ColoredString(cont.DisplayPorts(), color.FgYellow),
|
||||
}
|
||||
},
|
||||
GetDisplayStrings: presentation.GetServiceDisplayStrings,
|
||||
Hide: func() bool {
|
||||
return !gui.DockerCommand.InDockerComposeProject
|
||||
},
|
||||
|
|
@ -174,8 +157,8 @@ func (gui *Gui) handleServiceRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
|||
},
|
||||
}
|
||||
|
||||
menuItems := lo.Map(options, func(option *commandOption, _ int) *MenuItem {
|
||||
return &MenuItem{
|
||||
menuItems := lo.Map(options, func(option *commandOption, _ int) *types.MenuItem {
|
||||
return &types.MenuItem{
|
||||
LabelColumns: option.getDisplayStrings(),
|
||||
OnPress: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
||||
|
|
@ -355,8 +338,8 @@ func (gui *Gui) handleProjectDown(g *gocui.Gui, v *gocui.View) error {
|
|||
},
|
||||
}
|
||||
|
||||
menuItems := lo.Map(options, func(option *commandOption, _ int) *MenuItem {
|
||||
return &MenuItem{
|
||||
menuItems := lo.Map(options, func(option *commandOption, _ int) *types.MenuItem {
|
||||
return &types.MenuItem{
|
||||
LabelColumns: option.getDisplayStrings(),
|
||||
OnPress: option.onPress,
|
||||
}
|
||||
|
|
@ -427,8 +410,8 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
|
|||
},
|
||||
}
|
||||
|
||||
menuItems := lo.Map(options, func(option *commandOption, _ int) *MenuItem {
|
||||
return &MenuItem{
|
||||
menuItems := lo.Map(options, func(option *commandOption, _ int) *types.MenuItem {
|
||||
return &types.MenuItem{
|
||||
LabelColumns: option.getDisplayStrings(),
|
||||
OnPress: option.onPress,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
|
@ -14,28 +14,28 @@ func sampleContainers() []*commands.Container {
|
|||
{
|
||||
ID: "1",
|
||||
Name: "1",
|
||||
Container: types.Container{
|
||||
Container: dockerTypes.Container{
|
||||
State: "exited",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Name: "2",
|
||||
Container: types.Container{
|
||||
Container: dockerTypes.Container{
|
||||
State: "running",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Name: "3",
|
||||
Container: types.Container{
|
||||
Container: dockerTypes.Container{
|
||||
State: "running",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "4",
|
||||
Name: "4",
|
||||
Container: types.Container{
|
||||
Container: dockerTypes.Container{
|
||||
State: "created",
|
||||
},
|
||||
},
|
||||
|
|
@ -47,28 +47,28 @@ func expectedPerStatusContainers() []*commands.Container {
|
|||
{
|
||||
ID: "2",
|
||||
Name: "2",
|
||||
Container: types.Container{
|
||||
Container: dockerTypes.Container{
|
||||
State: "running",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Name: "3",
|
||||
Container: types.Container{
|
||||
Container: dockerTypes.Container{
|
||||
State: "running",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "1",
|
||||
Name: "1",
|
||||
Container: types.Container{
|
||||
Container: dockerTypes.Container{
|
||||
State: "exited",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "4",
|
||||
Name: "4",
|
||||
Container: types.Container{
|
||||
Container: dockerTypes.Container{
|
||||
State: "created",
|
||||
},
|
||||
},
|
||||
|
|
@ -80,28 +80,28 @@ func expectedLegacySortedContainers() []*commands.Container {
|
|||
{
|
||||
ID: "1",
|
||||
Name: "1",
|
||||
Container: types.Container{
|
||||
Container: dockerTypes.Container{
|
||||
State: "exited",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Name: "2",
|
||||
Container: types.Container{
|
||||
Container: dockerTypes.Container{
|
||||
State: "running",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Name: "3",
|
||||
Container: types.Container{
|
||||
Container: dockerTypes.Container{
|
||||
State: "running",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "4",
|
||||
Name: "4",
|
||||
Container: types.Container{
|
||||
Container: dockerTypes.Container{
|
||||
State: "created",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
13
pkg/gui/types/types.go
Normal file
13
pkg/gui/types/types.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package types
|
||||
|
||||
type MenuItem struct {
|
||||
Label string
|
||||
|
||||
// alternative to Label. Allows specifying columns which will be auto-aligned
|
||||
LabelColumns []string
|
||||
|
||||
OnPress func() error
|
||||
|
||||
// Only applies when Label is used
|
||||
OpensMenu bool
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ import (
|
|||
"github.com/jesseduffield/lazydocker/pkg/commands"
|
||||
"github.com/jesseduffield/lazydocker/pkg/config"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/panels"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazydocker/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazydocker/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
|
@ -46,9 +48,7 @@ func (gui *Gui) getVolumesPanel() *panels.SideListPanel[*commands.Volume] {
|
|||
}
|
||||
return a.Name < b.Name
|
||||
},
|
||||
GetDisplayStrings: func(volume *commands.Volume) []string {
|
||||
return []string{volume.Volume.Driver, volume.Name}
|
||||
},
|
||||
GetDisplayStrings: presentation.GetVolumeDisplayStrings,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -130,8 +130,8 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
|
|||
},
|
||||
}
|
||||
|
||||
menuItems := lo.Map(options, func(option *removeVolumeOption, _ int) *MenuItem {
|
||||
return &MenuItem{
|
||||
menuItems := lo.Map(options, func(option *removeVolumeOption, _ int) *types.MenuItem {
|
||||
return &types.MenuItem{
|
||||
LabelColumns: []string{option.description, color.New(color.FgRed).Sprint(option.command)},
|
||||
OnPress: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue