diff --git a/pkg/commands/container.go b/pkg/commands/container.go index c8e03fc8..ef3df1a7 100644 --- a/pkg/commands/container.go +++ b/pkg/commands/container.go @@ -195,16 +195,10 @@ type Details struct { App struct { } `json:"/app"` } `json:"Volumes"` - WorkingDir string `json:"WorkingDir"` - Entrypoint interface{} `json:"Entrypoint"` - OnBuild interface{} `json:"OnBuild"` - Labels struct { - ComDockerComposeOneoff string `json:"com.docker.compose.oneoff"` - ComDockerComposeProject string `json:"com.docker.compose.project"` - ComDockerComposeService string `json:"com.docker.compose.service"` - ComDockerComposeSlug string `json:"com.docker.compose.slug"` - ComDockerComposeVersion string `json:"com.docker.compose.version"` - } `json:"Labels"` + WorkingDir string `json:"WorkingDir"` + Entrypoint interface{} `json:"Entrypoint"` + OnBuild interface{} `json:"OnBuild"` + Labels map[string]string `json:"Labels"` } `json:"Config"` NetworkSettings struct { Bridge string `json:"Bridge"` diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index 67553675..14a6e2b6 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -196,7 +196,7 @@ func GetDefaultConfig() UserConfig { Containers: []CustomCommand{ { Attach: true, - Command: "docker exec -it {{ .Container.ID }} /bin/bash", + Command: "docker exec -it {{ .Container.ID }} /bin/sh", }, }, Services: []CustomCommand{}, diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 6366d5da..0905bb56 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -3,6 +3,7 @@ package gui import ( "encoding/json" "fmt" + "strings" "time" "github.com/docker/docker/api/types" @@ -70,10 +71,8 @@ func (gui *Gui) handleContainerSelect(g *gocui.Gui, v *gocui.View) error { } key := "containers-" + container.ID + "-" + gui.getContainerContexts()[gui.State.Panels.Containers.ContextIndex] - if gui.State.Panels.Main.ObjectKey == key { + if !gui.shouldRefresh(key) { return nil - } else { - gui.State.Panels.Main.ObjectKey = key } if err := gui.focusPoint(0, gui.State.Panels.Containers.SelectedLine, len(gui.DockerCommand.DisplayContainers), v); err != nil { @@ -115,13 +114,36 @@ func (gui *Gui) renderContainerConfig(container *commands.Container) error { mainView.Autoscroll = false mainView.Wrap = true + padding := 15 + output := "" + output += utils.WithPadding("ID: ", padding) + container.ID + "\n" + output += utils.WithPadding("Name: ", padding) + container.Name + "\n" + output += utils.WithPadding("Command: ", padding) + strings.Join(append([]string{container.Details.Path}, container.Details.Args...), " ") + "\n" + output += utils.WithPadding("Labels: ", padding) + utils.FormatMap(padding, container.Details.Config.Labels) + output += "\n" + + output += utils.WithPadding("Mounts: ", padding) + if len(container.Details.Mounts) > 0 { + output += "\n" + for _, mount := range container.Details.Mounts { + if mount.Type == "volume" { + output += fmt.Sprintf("%s%s %s\n", strings.Repeat(" ", padding), utils.ColoredString(mount.Type+":", color.FgYellow), mount.Name) + } else { + output += fmt.Sprintf("%s%s %s:%s\n", strings.Repeat(" ", padding), utils.ColoredString(mount.Type+":", color.FgYellow), mount.Source, mount.Destination) + } + } + } else { + output += "none\n" + } + data, err := json.MarshalIndent(&container.Details, "", " ") if err != nil { return err } + output += fmt.Sprintf("\nFull details:\n\n%s", string(data)) return gui.T.NewTask(func(stop chan struct{}) { - gui.renderString(gui.g, "main", string(data)) + gui.renderString(gui.g, "main", output) }) } diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index b9c32e59..8a19d227 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -26,7 +26,7 @@ func (gui *Gui) createCustomCommandMenu(customCommands []config.CustomCommand, c options[i] = &customCommandOption{ customCommand: command, - description: resolvedCommand, + description: utils.WithShortSha(resolvedCommand), command: resolvedCommand, runCommand: true, attach: command.Attach, diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 67d4a2d9..f47c7a7a 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -90,6 +90,7 @@ type menuPanelState struct { } type mainPanelState struct { + // ObjectKey tells us what context we are in. For example, if we are looking at the logs of a particular service in the services panel this key might be 'services--logs'. The key is made so that if something changes which might require us to re-run the logs command or run a different command, the key will be different, and we'll then know to do whatever is required. Object key probably isn't the best name for this but Context is already used to refer to tabs. Maybe I should just call them tabs. ObjectKey string } @@ -278,6 +279,7 @@ func (gui *Gui) Run() error { if strings.Contains(err.Error(), "No such container") { // this happens all the time when e.g. restarting containers so we won't worry about it gui.Log.Warn(err) + continue } gui.createErrorPanel(gui.g, err.Error()) } @@ -368,3 +370,12 @@ func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error { return gui.Errors.ErrSubProcess }) } + +func (gui *Gui) shouldRefresh(key string) bool { + if gui.State.Panels.Main.ObjectKey == key { + return false + } + + gui.State.Panels.Main.ObjectKey = key + return true +} diff --git a/pkg/gui/images_panel.go b/pkg/gui/images_panel.go index 7179e702..0da107fc 100644 --- a/pkg/gui/images_panel.go +++ b/pkg/gui/images_panel.go @@ -70,10 +70,8 @@ func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error { } key := "images-" + Image.ID + "-" + gui.getImageContexts()[gui.State.Panels.Images.ContextIndex] - if gui.State.Panels.Main.ObjectKey == key { + if !gui.shouldRefresh(key) { return nil - } else { - gui.State.Panels.Main.ObjectKey = key } if err := gui.focusPoint(0, gui.State.Panels.Images.SelectedLine, len(gui.DockerCommand.Images), v); err != nil { @@ -234,16 +232,18 @@ func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error { return nil } + shortSha := Image.ID[7:17] + options := []*removeImageOption{ { description: gui.Tr.Remove, - command: "docker image rm " + Image.ID[1:20], + command: "docker image rm " + shortSha, configOptions: types.ImageRemoveOptions{PruneChildren: true}, runCommand: true, }, { description: gui.Tr.RemoveWithoutPrune, - command: "docker image rm --no-prune " + Image.ID[1:20], + command: "docker image rm --no-prune " + shortSha, configOptions: types.ImageRemoveOptions{PruneChildren: false}, runCommand: true, }, diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index 91c0450c..fc83d178 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -68,11 +68,10 @@ func (gui *Gui) handleServiceSelect(g *gocui.Gui, v *gocui.View) error { if service.Container != nil { containerID = service.Container.ID } + key := "services-" + service.ID + "-" + containerID + "-" + gui.getServiceContexts()[gui.State.Panels.Services.ContextIndex] - if gui.State.Panels.Main.ObjectKey == key { + if !gui.shouldRefresh(key) { return nil - } else { - gui.State.Panels.Main.ObjectKey = key } if err := gui.focusPoint(0, gui.State.Panels.Services.SelectedLine, len(gui.DockerCommand.Services), v); err != nil { diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go index 43294a7a..2f09371c 100644 --- a/pkg/gui/status_panel.go +++ b/pkg/gui/status_panel.go @@ -47,10 +47,8 @@ func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error { } key := gui.getStatusContexts()[gui.State.Panels.Status.ContextIndex] - if gui.State.Panels.Main.ObjectKey == key { + if !gui.shouldRefresh(key) { return nil - } else { - gui.State.Panels.Main.ObjectKey = key } gui.clearMainView() diff --git a/pkg/gui/subprocess.go b/pkg/gui/subprocess.go index c1545cd7..d6a29233 100644 --- a/pkg/gui/subprocess.go +++ b/pkg/gui/subprocess.go @@ -70,7 +70,9 @@ func (gui *Gui) runCommand() error { fmt.Fprintf(os.Stdout, "\n%s", utils.ColoredString(gui.Tr.PressEnterToReturn, color.FgGreen)) - fmt.Scanln() // wait for enter press + if _, err := fmt.Scanln(); err != nil { // wait for enter press + gui.Log.Error(err) + } return nil } diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 822e63d3..f42e13be 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -78,6 +78,10 @@ func (gui *Gui) resetMainView() { } func (gui *Gui) newLineFocused(v *gocui.View) error { + if v == nil { + return nil + } + switch v.Name() { case "menu": return gui.handleMenuSelect(gui.g, v) diff --git a/pkg/gui/volumes_panel.go b/pkg/gui/volumes_panel.go index 0532ebb0..bc9d6594 100644 --- a/pkg/gui/volumes_panel.go +++ b/pkg/gui/volumes_panel.go @@ -2,7 +2,6 @@ package gui import ( "fmt" - "strings" "github.com/fatih/color" "github.com/go-errors/errors" @@ -68,10 +67,8 @@ func (gui *Gui) handleVolumeSelect(g *gocui.Gui, v *gocui.View) error { } key := "volumes-" + volume.Name + "-" + gui.getVolumeContexts()[gui.State.Panels.Volumes.ContextIndex] - if gui.State.Panels.Main.ObjectKey == key { + if !gui.shouldRefresh(key) { return nil - } else { - gui.State.Panels.Main.ObjectKey = key } if err := gui.focusPoint(0, gui.State.Panels.Volumes.SelectedLine, len(gui.DockerCommand.Volumes), v); err != nil { @@ -105,31 +102,14 @@ func (gui *Gui) renderVolumeConfig(mainView *gocui.View, volume *commands.Volume output += utils.WithPadding("Driver: ", padding) + volume.Volume.Driver + "\n" output += utils.WithPadding("Scope: ", padding) + volume.Volume.Scope + "\n" output += utils.WithPadding("Mountpoint: ", padding) + volume.Volume.Mountpoint + "\n" - output += utils.WithPadding("Labels: ", padding) - if len(volume.Volume.Labels) > 0 { - output += "\n" - for k, v := range volume.Volume.Labels { - output += formatMapItem(padding, k, v) - } - } else { - output += "none\n" - } + output += utils.WithPadding("Labels: ", padding) + utils.FormatMap(padding, volume.Volume.Labels) + "\n" + output += utils.WithPadding("Options: ", padding) + utils.FormatMap(padding, volume.Volume.Options) + "\n" - output += "\n" + utils.WithPadding("Options: ", padding) - if len(volume.Volume.Options) > 0 { - output += "\n" - for k, v := range volume.Volume.Options { - output += formatMapItem(padding, k, v) - } - } else { - output += "none\n" - } - - output += "\n" + utils.WithPadding("Status: ", padding) + output += utils.WithPadding("Status: ", padding) if volume.Volume.Status != nil { output += "\n" for k, v := range volume.Volume.Status { - output += formatMapItem(padding, k, v) + output += utils.FormatMapItem(padding, k, v) } } else { output += "n/a" @@ -144,10 +124,6 @@ func (gui *Gui) renderVolumeConfig(mainView *gocui.View, volume *commands.Volume }) } -func formatMapItem(padding int, k string, v interface{}) string { - return fmt.Sprintf("%s%s %v\n", strings.Repeat(" ", padding), utils.ColoredString(k+":", color.FgYellow), utils.ColoredString(fmt.Sprintf("%v", v), color.FgMagenta)) -} - func (gui *Gui) refreshVolumes() error { volumesView := gui.getVolumesView() if volumesView == nil { @@ -252,13 +228,13 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error { options := []*removeVolumeOption{ { description: gui.Tr.Remove, - command: "docker volume rm " + volume.Name, + command: utils.WithShortSha("docker volume rm " + volume.Name), force: false, runCommand: true, }, { description: gui.Tr.ForceRemove, - command: "docker volume rm --force " + volume.Name, + command: utils.WithShortSha("docker volume rm --force " + volume.Name), force: true, runCommand: true, }, diff --git a/pkg/tasks/tasks.go b/pkg/tasks/tasks.go index 1d84c4ac..3de3e9ca 100644 --- a/pkg/tasks/tasks.go +++ b/pkg/tasks/tasks.go @@ -87,7 +87,7 @@ func (t *TaskManager) NewTickerTask(duration time.Duration, before func(stop cha } tickChan := time.NewTicker(duration) // calling f first so that we're not waiting for the first tick - // f(stop, notifyStopped) + f(stop, notifyStopped) for { select { case <-notifyStopped: diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 56eed924..1030050d 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -8,6 +8,7 @@ import ( "math" "reflect" "regexp" + "sort" "strings" "time" @@ -355,3 +356,40 @@ func GetColorAttribute(key string) color.Attribute { } return color.FgWhite } + +// WithShortSha returns a command but with a shorter SHA. in the terminal we're all used to 10 character SHAs but under the hood they're actually 64 characters long. No need including all the characters when we're just displaying a command +func WithShortSha(str string) string { + split := strings.Split(str, " ") + for i, word := range split { + // good enough proxy for now + if len(word) == 64 { + split[i] = word[0:10] + } + } + return strings.Join(split, " ") +} + +// FormatMapItem is for displaying items in a map +func FormatMapItem(padding int, k string, v interface{}) string { + return fmt.Sprintf("%s%s %v\n", strings.Repeat(" ", padding), ColoredString(k+":", color.FgYellow), ColoredString(fmt.Sprintf("%v", v), color.FgWhite)) +} + +// FormatMap is for displaying a map +func FormatMap(padding int, m map[string]string) string { + if len(m) == 0 { + return "none\n" + } + + output := "\n" + + keys := make([]string, 0, len(m)) + for key := range m { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + output += FormatMapItem(padding, key, m[key]) + } + + return output +}