add images panel

pull/1/head
Jesse Duffield 5 years ago
parent 1488bfbcd4
commit 4fe26f4f88

@ -9,10 +9,11 @@ import (
"github.com/docker/docker/client"
"github.com/fatih/color"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/sirupsen/logrus"
"golang.org/x/xerrors"
)
// Container : A git Container
// Container : A docker Container
type Container struct {
Name string
ServiceName string
@ -21,6 +22,7 @@ type Container struct {
DisplayString string
Client *client.Client
OSCommand *OSCommand
Log *logrus.Entry
}
// GetDisplayStrings returns the dispaly string of Container

@ -2,7 +2,6 @@ package commands
import (
"context"
"fmt"
"strings"
"github.com/davecgh/go-spew/spew"
@ -48,8 +47,6 @@ func (c *DockerCommand) GetContainers() ([]*Container, error) {
ownContainers := make([]*Container, len(containers))
for i, container := range containers {
c.Log.Warn(spew.Sdump(container))
c.Log.Warn(fmt.Sprintf("%s %s\n", container.ID[:10], container.Image))
serviceName, ok := container.Labels["com.docker.compose.service"]
if !ok {
serviceName = ""
@ -62,8 +59,40 @@ func (c *DockerCommand) GetContainers() ([]*Container, error) {
Container: container,
Client: c.Client,
OSCommand: c.OSCommand,
Log: c.Log,
}
}
return ownContainers, nil
}
// GetImages returns a slice of docker images
func (c *DockerCommand) GetImages() ([]*Image, error) {
images, err := c.Client.ImageList(context.Background(), types.ImageListOptions{})
if err != nil {
return nil, err
}
ownImages := make([]*Image, len(images))
for i, image := range images {
c.Log.Warn(spew.Sdump(image))
name := "none"
tags := image.RepoTags
if len(tags) > 0 {
name = tags[0]
}
ownImages[i] = &Image{
ID: image.ID,
Name: name,
Image: image,
Client: c.Client,
OSCommand: c.OSCommand,
Log: c.Log,
}
}
return ownImages, nil
}

@ -0,0 +1,24 @@
package commands
import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/fatih/color"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/sirupsen/logrus"
)
// Image : A docker Image
type Image struct {
Name string
ID string
Image types.ImageSummary
Client *client.Client
OSCommand *OSCommand
Log *logrus.Entry
}
// GetDisplayStrings returns the display string of Image
func (i *Image) GetDisplayStrings(isFocused bool) []string {
return []string{utils.ColoredString(i.Name, color.FgWhite)}
}

@ -91,15 +91,15 @@ func (gui *Gui) handleContainerSelect(g *gocui.Gui, v *gocui.View) error {
switch gui.getContainerContexts()[gui.State.Panels.Containers.ContextIndex] {
case "logs":
if err := gui.renderLogs(mainView, container, writerID); err != nil {
if err := gui.renderContainerLogs(mainView, container, writerID); err != nil {
return err
}
case "config":
if err := gui.renderConfig(mainView, container, writerID); err != nil {
if err := gui.renderContainerConfig(mainView, container, writerID); err != nil {
return err
}
case "stats":
if err := gui.renderStats(mainView, container, writerID); err != nil {
if err := gui.renderContainerStats(mainView, container, writerID); err != nil {
return err
}
default:
@ -109,7 +109,7 @@ func (gui *Gui) handleContainerSelect(g *gocui.Gui, v *gocui.View) error {
return nil
}
func (gui *Gui) renderConfig(mainView *gocui.View, container *commands.Container, writerID int) error {
func (gui *Gui) renderContainerConfig(mainView *gocui.View, container *commands.Container, writerID int) error {
mainView.Autoscroll = false
mainView.Title = "Config"
@ -122,7 +122,7 @@ func (gui *Gui) renderConfig(mainView *gocui.View, container *commands.Container
return nil
}
func (gui *Gui) renderStats(mainView *gocui.View, container *commands.Container, writerID int) error {
func (gui *Gui) renderContainerStats(mainView *gocui.View, container *commands.Container, writerID int) error {
mainView.Autoscroll = false
mainView.Title = "Stats"
@ -171,7 +171,7 @@ func (gui *Gui) renderStats(mainView *gocui.View, container *commands.Container,
return nil
}
func (gui *Gui) renderLogs(mainView *gocui.View, container *commands.Container, writerID int) error {
func (gui *Gui) renderContainerLogs(mainView *gocui.View, container *commands.Container, writerID int) error {
mainView.Autoscroll = true
mainView.Title = "Logs"

@ -35,6 +35,7 @@ var OverlappingEdges = false
type SentinelErrors struct {
ErrSubProcess error
ErrNoContainers error
ErrNoImages error
}
// GenerateSentinelErrors makes the sentinel errors for the gui. We're defining it here
@ -50,7 +51,8 @@ type SentinelErrors struct {
func (gui *Gui) GenerateSentinelErrors() {
gui.Errors = SentinelErrors{
ErrSubProcess: errors.New(gui.Tr.SLocalize("RunningSubprocess")),
ErrNoContainers: errors.New(gui.Tr.SLocalize("NoChangedContainers")),
ErrNoContainers: errors.New(gui.Tr.SLocalize("NoContainers")),
ErrNoImages: errors.New(gui.Tr.SLocalize("NoImages")),
}
}
@ -87,14 +89,21 @@ type mainPanelState struct {
WriterID int
}
type imagePanelState struct {
SelectedLine int
ContextIndex int // for specifying if you are looking at logs/stats/config/etc
}
type panelStates struct {
Containers *containerPanelState
Menu *menuPanelState
Main *mainPanelState
Images *imagePanelState
}
type guiState struct {
Containers []*commands.Container
Images []*commands.Image
MenuItemCount int // can't store the actual list because it's of interface{} type
PreviousView string
Platform commands.Platform
@ -112,6 +121,7 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
Platform: *oSCommand.Platform,
Panels: &panelStates{
Containers: &containerPanelState{SelectedLine: -1, ContextIndex: 0},
Images: &imagePanelState{SelectedLine: -1, ContextIndex: 0},
Menu: &menuPanelState{SelectedLine: 0},
Main: &mainPanelState{
WriterID: 0,
@ -207,7 +217,7 @@ func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
if v == nil {
return nil
}
if v.Name() == "containers" {
if v.Name() == "containers" || v.Name() == "images" {
gui.State.Panels.Main.WriterID++
}
gui.Log.Info(v.Name() + " focus lost")
@ -265,7 +275,8 @@ func (gui *Gui) layout(g *gocui.Gui) error {
vHeights := map[string]int{
"status": 3,
"containers": usableSpace,
"containers": usableSpace/2 + usableSpace%2,
"images": usableSpace / 2,
"options": 1,
}
@ -277,6 +288,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
vHeights = map[string]int{
"status": defaultHeight,
"containers": defaultHeight,
"images": defaultHeight,
"options": defaultHeight,
}
vHeights[currentCyclebleView] = height - defaultHeight*4 - 1
@ -318,7 +330,17 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
containersView.Highlight = true
containersView.Title = gui.Tr.SLocalize("ContainersTitle")
v.FgColor = gocui.ColorWhite
containersView.FgColor = gocui.ColorWhite
}
imagesView, err := g.SetViewBeneath("images", "containers", vHeights["images"])
if err != nil {
if err.Error() != "unknown view" {
return err
}
imagesView.Highlight = true
imagesView.Title = gui.Tr.SLocalize("ImagesTitle")
imagesView.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("options", appStatusOptionsBoundary-1, height-2, optionsVersionBoundary-1, height, 0); err != nil {
@ -387,6 +409,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
listViews := map[*gocui.View]listViewState{
containersView: {selectedLine: gui.State.Panels.Containers.SelectedLine, lineCount: len(gui.State.Containers)},
imagesView: {selectedLine: gui.State.Panels.Images.SelectedLine, lineCount: len(gui.State.Images)},
}
// menu view might not exist so we check to be safe

@ -0,0 +1,272 @@
package gui
import (
"encoding/json"
"fmt"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/utils"
)
// list panel functions
func (gui *Gui) getImageContexts() []string {
return []string{"config"}
}
func (gui *Gui) getSelectedImage(g *gocui.Gui) (*commands.Image, error) {
selectedLine := gui.State.Panels.Images.SelectedLine
if selectedLine == -1 {
return &commands.Image{}, gui.Errors.ErrNoImages
}
return gui.State.Images[selectedLine], nil
}
func (gui *Gui) handleImagesFocus(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
cx, cy := v.Cursor()
_, oy := v.Origin()
prevSelectedLine := gui.State.Panels.Images.SelectedLine
newSelectedLine := cy - oy
if newSelectedLine > len(gui.State.Images)-1 || len(utils.Decolorise(gui.State.Images[newSelectedLine].Name)) < cx {
return gui.handleImageSelect(gui.g, v)
}
gui.State.Panels.Images.SelectedLine = newSelectedLine
if prevSelectedLine == newSelectedLine && gui.currentViewName() == v.Name() {
return gui.handleImagePress(gui.g, v)
} else {
return gui.handleImageSelect(gui.g, v)
}
}
func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error {
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
Image, err := gui.getSelectedImage(g)
if err != nil {
if err != gui.Errors.ErrNoImages {
return err
}
return gui.renderString(g, "main", gui.Tr.SLocalize("NoImages"))
}
key := Image.ID + "-" + gui.getImageContexts()[gui.State.Panels.Images.ContextIndex]
if gui.State.Panels.Main.ObjectKey == key {
return nil
} else {
gui.State.Panels.Main.ObjectKey = key
}
if err := gui.focusPoint(0, gui.State.Panels.Images.SelectedLine, len(gui.State.Images), v); err != nil {
return err
}
mainView := gui.getMainView()
gui.State.Panels.Main.WriterID++
writerID := gui.State.Panels.Main.WriterID
mainView.Clear()
mainView.SetOrigin(0, 0)
mainView.SetCursor(0, 0)
switch gui.getImageContexts()[gui.State.Panels.Images.ContextIndex] {
case "config":
if err := gui.renderImageConfig(mainView, Image, writerID); err != nil {
return err
}
default:
return errors.New("Unknown context for Images panel")
}
return nil
}
func (gui *Gui) renderImageConfig(mainView *gocui.View, Image *commands.Image, writerID int) error {
mainView.Autoscroll = false
mainView.Title = "Config"
data, err := json.MarshalIndent(&Image.Image, "", " ")
if err != nil {
return err
}
gui.renderString(gui.g, "main", string(data))
return nil
}
func (gui *Gui) refreshImages() error {
ImagesView := gui.getImagesView()
if ImagesView == nil {
// if the ImagesView hasn't been instantiated yet we just return
return nil
}
if err := gui.refreshStateImages(); err != nil {
return err
}
if len(gui.State.Images) > 0 && gui.State.Panels.Images.SelectedLine == -1 {
gui.State.Panels.Images.SelectedLine = 0
}
if len(gui.State.Images)-1 < gui.State.Panels.Images.SelectedLine {
gui.State.Panels.Images.SelectedLine = len(gui.State.Images) - 1
}
gui.g.Update(func(g *gocui.Gui) error {
ImagesView.Clear()
isFocused := gui.g.CurrentView().Name() == "Images"
list, err := utils.RenderList(gui.State.Images, isFocused)
if err != nil {
return err
}
fmt.Fprint(ImagesView, list)
if ImagesView == g.CurrentView() {
return gui.handleImageSelect(g, ImagesView)
}
return nil
})
return nil
}
func (gui *Gui) refreshStateImages() error {
Images, err := gui.DockerCommand.GetImages()
if err != nil {
return err
}
gui.State.Images = Images
return nil
}
func (gui *Gui) handleImagesNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Images
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Images), false)
return gui.handleImageSelect(gui.g, v)
}
func (gui *Gui) handleImagesPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Images
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Images), true)
return gui.handleImageSelect(gui.g, v)
}
func (gui *Gui) handleImagePress(g *gocui.Gui, v *gocui.View) error {
return nil
}
func (gui *Gui) handleImagesPrevContext(g *gocui.Gui, v *gocui.View) error {
contexts := gui.getImageContexts()
if gui.State.Panels.Images.ContextIndex >= len(contexts)-1 {
gui.State.Panels.Images.ContextIndex = 0
} else {
gui.State.Panels.Images.ContextIndex++
}
gui.handleImageSelect(gui.g, v)
return nil
}
func (gui *Gui) handleImagesNextContext(g *gocui.Gui, v *gocui.View) error {
contexts := gui.getImageContexts()
if gui.State.Panels.Images.ContextIndex <= 0 {
gui.State.Panels.Images.ContextIndex = len(contexts) - 1
} else {
gui.State.Panels.Images.ContextIndex--
}
gui.handleImageSelect(gui.g, v)
return nil
}
// type removeOption struct {
// description string
// command string
// configOptions types.ImageRemoveOptions
// runCommand bool
// }
// // GetDisplayStrings is a function.
// func (r *removeOption) GetDisplayStrings(isFocused bool) []string {
// return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
// }
// func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
// Image, err := gui.getSelectedImage(g)
// if err != nil {
// return nil
// }
// options := []*removeOption{
// {
// description: gui.Tr.SLocalize("remove"),
// command: "docker rm " + Image.ID[1:10],
// configOptions: types.ImageRemoveOptions{},
// runCommand: true,
// },
// {
// description: gui.Tr.SLocalize("removeWithVolumes"),
// command: "docker rm --volumes " + Image.ID[1:10],
// configOptions: types.ImageRemoveOptions{RemoveVolumes: true},
// runCommand: true,
// },
// {
// description: gui.Tr.SLocalize("cancel"),
// runCommand: false,
// },
// }
// handleMenuPress := func(index int) error {
// if !options[index].runCommand {
// return nil
// }
// configOptions := options[index].configOptions
// if cerr := Image.Remove(configOptions); cerr != nil {
// var originalErr commands.ComplexError
// if xerrors.As(cerr, &originalErr) {
// if originalErr.Code == commands.MustStopImage {
// return gui.createConfirmationPanel(gui.g, v, gui.Tr.SLocalize("Confirm"), gui.Tr.SLocalize("mustForceToRemove"), func(g *gocui.Gui, v *gocui.View) error {
// configOptions.Force = true
// if err := Image.Remove(configOptions); err != nil {
// return err
// }
// return gui.refreshImages()
// }, nil)
// }
// } else {
// return gui.createErrorPanel(gui.g, err.Error())
// }
// }
// return gui.refreshImages()
// }
// return gui.createMenu("", options, len(options), handleMenuPress)
// }

@ -179,10 +179,23 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleContainerAttach,
Description: gui.Tr.SLocalize("attachContainer"),
},
{
ViewName: "images",
Key: '[',
Modifier: gocui.ModNone,
Handler: gui.handleImagesPrevContext,
Description: gui.Tr.SLocalize("previousContext"),
}, {
ViewName: "images",
Key: ']',
Modifier: gocui.ModNone,
Handler: gui.handleImagesNextContext,
Description: gui.Tr.SLocalize("nextContext"),
},
}
// TODO: add more views here
for _, viewName := range []string{"status", "containers", "menu"} {
for _, viewName := range []string{"status", "containers", "images", "menu"} {
bindings = append(bindings, []*Binding{
{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},
@ -199,6 +212,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
}{
"menu": {prevLine: gui.handleMenuPrevLine, nextLine: gui.handleMenuNextLine, focus: gui.handleMenuSelect},
"containers": {prevLine: gui.handleContainersPrevLine, nextLine: gui.handleContainersNextLine, focus: gui.handleContainersFocus},
"images": {prevLine: gui.handleImagesPrevLine, nextLine: gui.handleImagesNextLine, focus: gui.handleImagesFocus},
}
for viewName, functions := range listPanelMap {

@ -10,12 +10,15 @@ import (
"github.com/spkg/bom"
)
var cyclableViews = []string{"status", "containers"}
var cyclableViews = []string{"status", "containers", "images"}
func (gui *Gui) refreshSidePanels(g *gocui.Gui) error {
if err := gui.refreshContainers(); err != nil {
return err
}
if err := gui.refreshImages(); err != nil {
return err
}
return nil
}
@ -77,6 +80,8 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
return gui.handleStatusSelect(g, v)
case "containers":
return gui.handleContainerSelect(g, v)
case "images":
return gui.handleImageSelect(g, v)
case "confirmation":
return nil
case "main":
@ -218,6 +223,11 @@ func (gui *Gui) getContainersView() *gocui.View {
return v
}
func (gui *Gui) getImagesView() *gocui.View {
v, _ := gui.g.View("images")
return v
}
func (gui *Gui) getMainView() *gocui.View {
v, _ := gui.g.View("main")
return v

@ -27,9 +27,6 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "LogTitle",
Other: "Log",
}, &i18n.Message{
ID: "ContainersTitle",
Other: "Containers",
}, &i18n.Message{
ID: "BranchesTitle",
Other: "Branches",
@ -150,9 +147,6 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "checkout",
Other: "checkout",
}, &i18n.Message{
ID: "NoChangedContainers",
Other: "No changed containers",
}, &i18n.Message{
ID: "ContainerHasNoUnstagedChanges",
Other: "Container has no unstaged changes to add",
@ -810,5 +804,21 @@ func addEnglish(i18nObject *i18n.Bundle) error {
ID: "attachContainer",
Other: "attach to container",
},
&i18n.Message{
ID: "ContainersTitle",
Other: "Containers",
},
&i18n.Message{
ID: "ImagesTitle",
Other: "Images",
},
&i18n.Message{
ID: "NoContainers",
Other: "No containers",
},
&i18n.Message{
ID: "NoImages",
Other: "No images",
},
)
}

Loading…
Cancel
Save