more progress

This commit is contained in:
Jesse Duffield 2019-06-02 00:28:47 +10:00
parent b2e60e1914
commit aa55ff1a8e
12 changed files with 618 additions and 131 deletions

View file

@ -18,6 +18,7 @@ import (
type Container struct { type Container struct {
Name string Name string
ServiceName string ServiceName string
ServiceID string
ContainerNumber string // might make this an int in the future if need be ContainerNumber string // might make this an int in the future if need be
ProjectName string ProjectName string
ID string ID string

View file

@ -3,6 +3,7 @@ package commands
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"strings" "strings"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
@ -10,16 +11,18 @@ import (
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/i18n" "github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// DockerCommand is our main git interface // DockerCommand is our main git interface
type DockerCommand struct { type DockerCommand struct {
Log *logrus.Entry Log *logrus.Entry
OSCommand *OSCommand OSCommand *OSCommand
Tr *i18n.Localizer Tr *i18n.Localizer
Config config.AppConfigurer Config config.AppConfigurer
Client *client.Client Client *client.Client
InDockerComposeProject bool
} }
// NewDockerCommand it runs git commands // NewDockerCommand it runs git commands
@ -30,11 +33,12 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localize
} }
return &DockerCommand{ return &DockerCommand{
Log: log, Log: log,
OSCommand: osCommand, OSCommand: osCommand,
Tr: tr, Tr: tr,
Config: config, Config: config,
Client: cli, Client: cli,
InDockerComposeProject: true, // TODO: determine this at startup
}, nil }, nil
} }
@ -72,7 +76,32 @@ func (c *DockerCommand) UpdateContainerStats(containers []*Container) ([]*Contai
return containers, nil return containers, nil
} }
// GetContainers returns a slice of docker containers // GetContainersAndServices returns a slice of docker containers
func (c *DockerCommand) GetContainersAndServices() ([]*Container, []*Service, error) {
containers, err := c.GetContainers()
if err != nil {
return nil, nil, err
}
services, err := c.GetServices()
if err != nil {
return nil, nil, err
}
// find out which services have corresponding containers and assign them
for _, service := range services {
for _, container := range containers {
if container.ServiceID != "" && container.ServiceID == service.ID {
service.Container = container
}
}
}
return containers, services, nil
}
// GetContainers gets the docker containers
func (c *DockerCommand) GetContainers() ([]*Container, error) { func (c *DockerCommand) GetContainers() ([]*Container, error) {
containers, err := c.Client.ContainerList(context.Background(), types.ContainerListOptions{All: true}) containers, err := c.Client.ContainerList(context.Background(), types.ContainerListOptions{All: true})
if err != nil { if err != nil {
@ -81,15 +110,12 @@ func (c *DockerCommand) GetContainers() ([]*Container, error) {
ownContainers := make([]*Container, len(containers)) ownContainers := make([]*Container, len(containers))
ids := []string{}
for i, container := range containers { for i, container := range containers {
ids = append(ids, container.ID)
ownContainers[i] = &Container{ ownContainers[i] = &Container{
ID: container.ID, ID: container.ID,
Name: strings.TrimLeft(container.Names[0], "/"), Name: strings.TrimLeft(container.Names[0], "/"),
ServiceName: container.Labels["com.docker.compose.service"], ServiceName: container.Labels["com.docker.compose.service"],
ServiceID: container.Labels["com.docker.compose.config-hash"],
ProjectName: container.Labels["com.docker.compose.project"], ProjectName: container.Labels["com.docker.compose.project"],
ContainerNumber: container.Labels["com.docker.compose.container"], ContainerNumber: container.Labels["com.docker.compose.container"],
Container: container, Container: container,
@ -99,20 +125,7 @@ func (c *DockerCommand) GetContainers() ([]*Container, error) {
} }
} }
cmd := c.OSCommand.RunCustomCommand("docker inspect " + strings.Join(ids, " ")) c.UpdateContainerDetails(ownContainers)
output, err := cmd.CombinedOutput()
if err != nil {
return nil, err
}
var details []*Details
if err := json.Unmarshal(output, &details); err != nil {
return nil, err
}
for i, container := range ownContainers {
container.Details = *details[i]
}
// ownContainers, err = c.UpdateContainerStats(ownContainers) // ownContainers, err = c.UpdateContainerStats(ownContainers)
// if err != nil { // if err != nil {
@ -122,6 +135,61 @@ func (c *DockerCommand) GetContainers() ([]*Container, error) {
return ownContainers, nil return ownContainers, nil
} }
// GetServices gets services
func (c *DockerCommand) GetServices() ([]*Service, error) {
if !c.InDockerComposeProject {
return nil, nil
}
composeCommand := c.Config.GetUserConfig().GetString("commandTemplates.dockerCompose")
output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s config --hash=*", composeCommand))
if err != nil {
return nil, err
}
// output looks like:
// service1 998d6d286b0499e0ff23d66302e720991a2asdkf9c30d0542034f610daf8a971
// service2 asdld98asdklasd9bccd02438de0994f8e19cbe691feb3755336ec5ca2c55971
lines := utils.SplitLines(output)
services := make([]*Service, len(lines))
for i, str := range lines {
arr := strings.Split(str, " ")
services[i] = &Service{
Name: arr[0],
ID: arr[1],
}
}
return services, nil
}
// UpdateContainerDetails attaches the details returned from docker inspect to each of the containers
// this contains a bit more info than what you get from the go-docker client
func (c *DockerCommand) UpdateContainerDetails(containers []*Container) error {
ids := make([]string, len(containers))
for i, container := range containers {
ids[i] = container.ID
}
cmd := c.OSCommand.RunCustomCommand("docker inspect " + strings.Join(ids, " "))
output, err := cmd.CombinedOutput()
if err != nil {
return err
}
var details []*Details
if err := json.Unmarshal(output, &details); err != nil {
return err
}
for i, container := range containers {
container.Details = *details[i]
}
return nil
}
// GetImages returns a slice of docker images // GetImages returns a slice of docker images
func (c *DockerCommand) GetImages() ([]*Image, error) { func (c *DockerCommand) GetImages() ([]*Image, error) {
images, err := c.Client.ImageList(context.Background(), types.ImageListOptions{}) images, err := c.Client.ImageList(context.Background(), types.ImageListOptions{})

62
pkg/commands/service.go Normal file
View file

@ -0,0 +1,62 @@
package commands
import (
"os/exec"
"github.com/docker/docker/api/types"
"github.com/fatih/color"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/sirupsen/logrus"
)
// Service : A docker Service
type Service struct {
Name string
ID string
DisplayString string
OSCommand *OSCommand
Log *logrus.Entry
Container *Container
}
// GetDisplayStrings returns the dispaly string of Container
func (s *Service) GetDisplayStrings(isFocused bool) []string {
if s.Container == nil {
return []string{"", utils.ColoredString(s.Name, color.FgWhite)}
}
cont := s.Container
return []string{utils.ColoredString(cont.Container.State, cont.GetColor()), utils.ColoredString(cont.Name, color.FgWhite), cont.Stats.CPUPerc}
}
// Remove removes the service's containers
func (s *Service) Remove(options types.ContainerRemoveOptions) error {
return s.Container.Remove(options)
}
// Stop stops the service's containers
func (s *Service) Stop() error {
templateString := s.OSCommand.Config.GetUserConfig().GetString("commandTemplates.stopService")
command := utils.ApplyTemplate(templateString, s)
return s.OSCommand.RunCommand(command)
}
// Restart restarts the service
func (s *Service) Restart() error {
templateString := s.OSCommand.Config.GetUserConfig().GetString("commandTemplates.restartService")
command := utils.ApplyTemplate(templateString, s)
return s.OSCommand.RunCommand(command)
}
// Attach attaches to the service
func (s *Service) Attach() *exec.Cmd {
// TODO: if you have a custom command for attaching to a service here is the place to use it
return s.Container.Attach()
}
// Top returns process information
func (s *Service) Top() (types.ContainerProcessList, error) {
return s.Container.Top()
}

View file

@ -249,7 +249,8 @@ update:
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined' reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
confirmOnQuit: false confirmOnQuit: false
commandTemplates: commandTemplates:
restartService: "docker-compose restart {{ .ServiceName }}" restartService: "docker-compose restart {{ .Name }}"
dockerCompose: "apdev compose"
`) `)
} }

View file

@ -81,24 +81,21 @@ func (gui *Gui) handleContainerSelect(g *gocui.Gui, v *gocui.View) error {
mainView := gui.getMainView() mainView := gui.getMainView()
gui.State.Panels.Main.WriterID++
writerID := gui.State.Panels.Main.WriterID
mainView.Clear() mainView.Clear()
mainView.SetOrigin(0, 0) mainView.SetOrigin(0, 0)
mainView.SetCursor(0, 0) mainView.SetCursor(0, 0)
switch gui.getContainerContexts()[gui.State.Panels.Containers.ContextIndex] { switch gui.getContainerContexts()[gui.State.Panels.Containers.ContextIndex] {
case "logs": case "logs":
if err := gui.renderContainerLogs(mainView, container, writerID); err != nil { if err := gui.renderContainerLogs(mainView, container); err != nil {
return err return err
} }
case "config": case "config":
if err := gui.renderContainerConfig(mainView, container, writerID); err != nil { if err := gui.renderContainerConfig(mainView, container); err != nil {
return err return err
} }
case "stats": case "stats":
if err := gui.renderContainerStats(mainView, container, writerID); err != nil { if err := gui.renderContainerStats(mainView, container); err != nil {
return err return err
} }
default: default:
@ -108,7 +105,7 @@ func (gui *Gui) handleContainerSelect(g *gocui.Gui, v *gocui.View) error {
return nil return nil
} }
func (gui *Gui) renderContainerConfig(mainView *gocui.View, container *commands.Container, writerID int) error { func (gui *Gui) renderContainerConfig(mainView *gocui.View, container *commands.Container) error {
mainView.Autoscroll = false mainView.Autoscroll = false
mainView.Title = "Config" mainView.Title = "Config"
@ -116,12 +113,13 @@ func (gui *Gui) renderContainerConfig(mainView *gocui.View, container *commands.
if err != nil { if err != nil {
return err return err
} }
gui.renderString(gui.g, "main", string(data))
return nil return gui.T.NewTask(func(stop chan struct{}) {
gui.renderString(gui.g, "main", string(data))
})
} }
func (gui *Gui) renderContainerStats(mainView *gocui.View, container *commands.Container, writerID int) error { func (gui *Gui) renderContainerStats(mainView *gocui.View, container *commands.Container) error {
mainView.Autoscroll = false mainView.Autoscroll = false
mainView.Title = "Stats" mainView.Title = "Stats"
@ -130,7 +128,9 @@ func (gui *Gui) renderContainerStats(mainView *gocui.View, container *commands.C
return err return err
} }
go func() { return gui.T.NewTask(func(stop chan struct{}) {
defer stream.Body.Close()
cpuUsageHistory := []float64{} cpuUsageHistory := []float64{}
memoryUsageHistory := []float64{} memoryUsageHistory := []float64{}
scanner := bufio.NewScanner(stream.Body) scanner := bufio.NewScanner(stream.Body)
@ -156,31 +156,28 @@ func (gui *Gui) renderContainerStats(mainView *gocui.View, container *commands.C
gui.createErrorPanel(gui.g, err.Error()) gui.createErrorPanel(gui.g, err.Error())
} }
if gui.State.Panels.Main.WriterID != writerID { select {
stream.Body.Close() case <-stop:
return return
default:
} }
gui.reRenderString(gui.g, "main", contents) gui.reRenderString(gui.g, "main", contents)
} }
})
stream.Body.Close()
}()
return nil
} }
func (gui *Gui) renderContainerLogs(mainView *gocui.View, container *commands.Container, writerID int) error { func (gui *Gui) renderContainerLogs(mainView *gocui.View, container *commands.Container) error {
mainView.Autoscroll = true mainView.Autoscroll = true
mainView.Title = "Logs" mainView.Title = "Logs"
if container.Details.Config.AttachStdin { if container.Details.Config.AttachStdin {
return gui.renderLogsForTTYContainer(mainView, container, writerID) return gui.renderLogsForTTYContainer(mainView, container)
} }
return gui.renderLogsForRegularContainer(mainView, container, writerID) return gui.renderLogsForRegularContainer(mainView, container)
} }
func (gui *Gui) renderLogsForRegularContainer(mainView *gocui.View, container *commands.Container, writerID int) error { func (gui *Gui) renderLogsForRegularContainer(mainView *gocui.View, container *commands.Container) error {
var cmd *exec.Cmd var cmd *exec.Cmd
cmd = gui.OSCommand.RunCustomCommand("docker logs --since=60m --timestamps --follow " + container.ID) cmd = gui.OSCommand.RunCustomCommand("docker logs --since=60m --timestamps --follow " + container.ID)
@ -191,21 +188,29 @@ func (gui *Gui) renderLogsForRegularContainer(mainView *gocui.View, container *c
return nil return nil
} }
func (gui *Gui) runProcessWithLock(cmd *exec.Cmd) { func (gui *Gui) obtainLock() {
gui.State.MainProcessChan <- struct{}{} gui.State.MainProcessChan <- struct{}{}
gui.State.MainProcessMutex.Lock() gui.State.MainProcessMutex.Lock()
cmd.Start() }
go func() { func (gui *Gui) releaseLock() {
<-gui.State.MainProcessChan
cmd.Process.Kill()
}()
cmd.Wait()
gui.State.MainProcessMutex.Unlock() gui.State.MainProcessMutex.Unlock()
} }
func (gui *Gui) renderLogsForTTYContainer(mainView *gocui.View, container *commands.Container, writerID int) error { func (gui *Gui) runProcessWithLock(cmd *exec.Cmd) {
gui.T.NewTask(func(stop chan struct{}) {
cmd.Start()
go func() {
<-stop
cmd.Process.Kill()
}()
cmd.Wait()
})
}
func (gui *Gui) renderLogsForTTYContainer(mainView *gocui.View, container *commands.Container) error {
var cmd *exec.Cmd var cmd *exec.Cmd
cmd = gui.OSCommand.RunCustomCommand("docker logs --since=60m --follow " + container.ID) cmd = gui.OSCommand.RunCustomCommand("docker logs --since=60m --follow " + container.ID)
@ -219,6 +224,7 @@ func (gui *Gui) renderLogsForTTYContainer(mainView *gocui.View, container *comma
s := bufio.NewScanner(r) s := bufio.NewScanner(r)
s.Split(bufio.ScanLines) s.Split(bufio.ScanLines)
for s.Scan() { for s.Scan() {
// I might put a check on the stopped channel here. Would mean more code duplication though
mainView.Write(append(s.Bytes(), '\n')) mainView.Write(append(s.Bytes(), '\n'))
} }
} }
@ -228,13 +234,13 @@ func (gui *Gui) renderLogsForTTYContainer(mainView *gocui.View, container *comma
return nil return nil
} }
func (gui *Gui) refreshContainers() error { func (gui *Gui) refreshContainersAndServices() error {
containersView := gui.getContainersView() containersView := gui.getContainersView()
if containersView == nil { if containersView == nil {
// if the containersView hasn't been instantiated yet we just return // if the containersView hasn't been instantiated yet we just return
return nil return nil
} }
if err := gui.refreshStateContainers(); err != nil { if err := gui.refreshStateContainersAndServices(); err != nil {
return err return err
} }
@ -245,8 +251,15 @@ func (gui *Gui) refreshContainers() error {
gui.State.Panels.Containers.SelectedLine = len(gui.State.Containers) - 1 gui.State.Panels.Containers.SelectedLine = len(gui.State.Containers) - 1
} }
gui.g.Update(func(g *gocui.Gui) error { // doing the exact same thing for services
if len(gui.State.Services) > 0 && gui.State.Panels.Services.SelectedLine == -1 {
gui.State.Panels.Services.SelectedLine = 0
}
if len(gui.State.Services)-1 < gui.State.Panels.Services.SelectedLine {
gui.State.Panels.Services.SelectedLine = len(gui.State.Services) - 1
}
gui.g.Update(func(g *gocui.Gui) error {
containersView.Clear() containersView.Clear()
isFocused := gui.g.CurrentView().Name() == "containers" isFocused := gui.g.CurrentView().Name() == "containers"
list, err := utils.RenderList(gui.State.Containers, utils.IsFocused(isFocused)) list, err := utils.RenderList(gui.State.Containers, utils.IsFocused(isFocused))
@ -258,19 +271,37 @@ func (gui *Gui) refreshContainers() error {
if containersView == g.CurrentView() { if containersView == g.CurrentView() {
return gui.handleContainerSelect(g, containersView) return gui.handleContainerSelect(g, containersView)
} }
// doing the exact same thing for services
if !gui.DockerCommand.InDockerComposeProject {
return nil
}
servicesView := gui.getServicesView()
servicesView.Clear()
isFocused = gui.g.CurrentView().Name() == "services"
list, err = utils.RenderList(gui.State.Containers, utils.IsFocused(isFocused))
if err != nil {
return err
}
fmt.Fprint(servicesView, list)
if servicesView == g.CurrentView() {
return gui.handleContainerSelect(g, servicesView)
}
return nil return nil
}) })
return nil return nil
} }
func (gui *Gui) refreshStateContainers() error { func (gui *Gui) refreshStateContainersAndServices() error {
containers, err := gui.DockerCommand.GetContainers() containers, services, err := gui.DockerCommand.GetContainersAndServices()
if err != nil { if err != nil {
return err return err
} }
gui.State.Containers = containers gui.State.Containers = containers
gui.State.Services = services
return nil return nil
} }
@ -346,22 +377,19 @@ func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
description: gui.Tr.SLocalize("remove"), description: gui.Tr.SLocalize("remove"),
command: "docker rm " + container.ID[1:10], command: "docker rm " + container.ID[1:10],
configOptions: types.ContainerRemoveOptions{}, configOptions: types.ContainerRemoveOptions{},
runCommand: true,
}, },
{ {
description: gui.Tr.SLocalize("removeWithVolumes"), description: gui.Tr.SLocalize("removeWithVolumes"),
command: "docker rm --volumes " + container.ID[1:10], command: "docker rm --volumes " + container.ID[1:10],
configOptions: types.ContainerRemoveOptions{RemoveVolumes: true}, configOptions: types.ContainerRemoveOptions{RemoveVolumes: true},
runCommand: true,
}, },
{ {
description: gui.Tr.SLocalize("cancel"), description: gui.Tr.SLocalize("cancel"),
runCommand: false,
}, },
} }
handleMenuPress := func(index int) error { handleMenuPress := func(index int) error {
if !options[index].runCommand { if options[index].command == "" {
return nil return nil
} }
configOptions := options[index].configOptions configOptions := options[index].configOptions
@ -374,7 +402,7 @@ func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
if err := container.Remove(configOptions); err != nil { if err := container.Remove(configOptions); err != nil {
return err return err
} }
return gui.refreshContainers() return gui.refreshContainersAndServices()
}, nil) }, nil)
} }
} else { } else {
@ -382,7 +410,7 @@ func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
} }
} }
return gui.refreshContainers() return gui.refreshContainersAndServices()
} }
return gui.createMenu("", options, len(options), handleMenuPress) return gui.createMenu("", options, len(options), handleMenuPress)
@ -400,7 +428,7 @@ func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(gui.g, err.Error())
} }
return gui.refreshContainers() return gui.refreshContainersAndServices()
}) })
}, nil) }, nil)
@ -417,7 +445,7 @@ func (gui *Gui) handleContainerRestart(g *gocui.Gui, v *gocui.View) error {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(gui.g, err.Error())
} }
return gui.refreshContainers() return gui.refreshContainersAndServices()
}) })
} }
@ -432,18 +460,3 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error {
gui.SubProcess = c gui.SubProcess = c
return gui.Errors.ErrSubProcess return gui.Errors.ErrSubProcess
} }
func (gui *Gui) handleServiceRestart(g *gocui.Gui, v *gocui.View) error {
container, err := gui.getSelectedContainer(g)
if err != nil {
return nil
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("RestartingStatus"), func() error {
if err := container.RestartService(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshContainers()
})
}

View file

@ -23,6 +23,7 @@ import (
"github.com/jesseduffield/lazydocker/pkg/commands" "github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/i18n" "github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazydocker/pkg/updates" "github.com/jesseduffield/lazydocker/pkg/updates"
"github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -74,6 +75,12 @@ type Gui struct {
Updater *updates.Updater Updater *updates.Updater
statusManager *statusManager statusManager *statusManager
waitForIntro sync.WaitGroup waitForIntro sync.WaitGroup
T *tasks.TaskManager
}
type servicePanelState struct {
SelectedLine int
ContextIndex int // for specifying if you are looking at logs/stats/config/etc
} }
type containerPanelState struct { type containerPanelState struct {
@ -87,7 +94,6 @@ type menuPanelState struct {
type mainPanelState struct { type mainPanelState struct {
ObjectKey string ObjectKey string
WriterID int
} }
type imagePanelState struct { type imagePanelState struct {
@ -96,6 +102,7 @@ type imagePanelState struct {
} }
type panelStates struct { type panelStates struct {
Services *servicePanelState
Containers *containerPanelState Containers *containerPanelState
Menu *menuPanelState Menu *menuPanelState
Main *mainPanelState Main *mainPanelState
@ -103,6 +110,7 @@ type panelStates struct {
} }
type guiState struct { type guiState struct {
Services []*commands.Service
Containers []*commands.Container Containers []*commands.Container
Images []*commands.Image Images []*commands.Image
MenuItemCount int // can't store the actual list because it's of interface{} type MenuItemCount int // can't store the actual list because it's of interface{} type
@ -123,11 +131,11 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
PreviousView: "containers", PreviousView: "containers",
Platform: *oSCommand.Platform, Platform: *oSCommand.Platform,
Panels: &panelStates{ Panels: &panelStates{
Services: &servicePanelState{SelectedLine: -1, ContextIndex: 0},
Containers: &containerPanelState{SelectedLine: -1, ContextIndex: 0}, Containers: &containerPanelState{SelectedLine: -1, ContextIndex: 0},
Images: &imagePanelState{SelectedLine: -1, ContextIndex: 0}, Images: &imagePanelState{SelectedLine: -1, ContextIndex: 0},
Menu: &menuPanelState{SelectedLine: 0}, Menu: &menuPanelState{SelectedLine: 0},
Main: &mainPanelState{ Main: &mainPanelState{
WriterID: 0,
ObjectKey: "", ObjectKey: "",
}, },
}, },
@ -151,6 +159,7 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
Tr: tr, Tr: tr,
Updater: updater, Updater: updater,
statusManager: &statusManager{}, statusManager: &statusManager{},
T: tasks.NewTaskManager(),
} }
gui.GenerateSentinelErrors() gui.GenerateSentinelErrors()
@ -229,9 +238,6 @@ func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
if v == nil { if v == nil {
return nil return nil
} }
if v.Name() == "containers" || v.Name() == "images" {
gui.State.Panels.Main.WriterID++
}
gui.Log.Info(v.Name() + " focus lost") gui.Log.Info(v.Name() + " focus lost")
return nil return nil
} }
@ -512,7 +518,7 @@ func (gui *Gui) Run() error {
gui.waitForIntro.Wait() gui.waitForIntro.Wait()
gui.goEvery(time.Millisecond*50, gui.renderAppStatus) gui.goEvery(time.Millisecond*50, gui.renderAppStatus)
gui.goEvery(time.Millisecond*30, gui.reRenderMain) gui.goEvery(time.Millisecond*30, gui.reRenderMain)
gui.goEvery(time.Millisecond*500, gui.refreshContainers) gui.goEvery(time.Millisecond*500, gui.refreshContainersAndServices)
}() }()
g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout())) g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout()))

View file

@ -78,16 +78,13 @@ func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error {
mainView := gui.getMainView() mainView := gui.getMainView()
gui.State.Panels.Main.WriterID++
writerID := gui.State.Panels.Main.WriterID
mainView.Clear() mainView.Clear()
mainView.SetOrigin(0, 0) mainView.SetOrigin(0, 0)
mainView.SetCursor(0, 0) mainView.SetCursor(0, 0)
switch gui.getImageContexts()[gui.State.Panels.Images.ContextIndex] { switch gui.getImageContexts()[gui.State.Panels.Images.ContextIndex] {
case "config": case "config":
if err := gui.renderImageConfig(mainView, Image, writerID); err != nil { if err := gui.renderImageConfig(mainView, Image); err != nil {
return err return err
} }
default: default:
@ -97,25 +94,27 @@ func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error {
return nil return nil
} }
func (gui *Gui) renderImageConfig(mainView *gocui.View, image *commands.Image, writerID int) error { func (gui *Gui) renderImageConfig(mainView *gocui.View, image *commands.Image) error {
mainView.Autoscroll = false mainView.Autoscroll = false
mainView.Wrap = false mainView.Wrap = false
mainView.Title = "Config" mainView.Title = "Config"
output := "" gui.T.NewTask(func(stop chan struct{}) {
output += utils.WithPadding("ID: ", 10) + image.Image.ID + "\n" output := ""
output += utils.WithPadding("Tags: ", 10) + utils.ColoredString(strings.Join(image.Image.RepoTags, ", "), color.FgGreen) + "\n" output += utils.WithPadding("ID: ", 10) + image.Image.ID + "\n"
output += utils.WithPadding("Size: ", 10) + utils.FormatDecimalBytes(int(image.Image.Size)) + "\n" output += utils.WithPadding("Tags: ", 10) + utils.ColoredString(strings.Join(image.Image.RepoTags, ", "), color.FgGreen) + "\n"
output += utils.WithPadding("Created: ", 10) + fmt.Sprintf("%v", time.Unix(image.Image.Created, 0).Format(time.RFC1123)) + "\n" output += utils.WithPadding("Size: ", 10) + utils.FormatDecimalBytes(int(image.Image.Size)) + "\n"
output += utils.WithPadding("Created: ", 10) + fmt.Sprintf("%v", time.Unix(image.Image.Created, 0).Format(time.RFC1123)) + "\n"
history, err := image.RenderHistory() history, err := image.RenderHistory()
if err != nil { if err != nil {
return err gui.Log.Error(err)
} }
output += "\n\n" + history output += "\n\n" + history
gui.renderString(gui.g, "main", output) gui.renderString(gui.g, "main", output)
})
return nil return nil
} }

View file

@ -156,35 +156,56 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Key: 'd', Key: 'd',
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleContainersRemoveMenu, Handler: gui.handleContainersRemoveMenu,
Description: gui.Tr.SLocalize("removeContainer"), Description: gui.Tr.SLocalize("remove"),
}, },
{ {
ViewName: "containers", ViewName: "containers",
Key: 's', Key: 's',
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleContainerStop, Handler: gui.handleContainerStop,
Description: gui.Tr.SLocalize("stopContainer"), Description: gui.Tr.SLocalize("stop"),
}, },
{ {
ViewName: "containers", ViewName: "containers",
Key: 'r', Key: 'r',
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleContainerRestart, Handler: gui.handleContainerRestart,
Description: gui.Tr.SLocalize("restartContainer"), Description: gui.Tr.SLocalize("restart"),
}, },
{ {
ViewName: "containers", ViewName: "containers",
Key: 'a', Key: 'a',
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleContainerAttach, Handler: gui.handleContainerAttach,
Description: gui.Tr.SLocalize("attachContainer"), Description: gui.Tr.SLocalize("attach"),
}, },
{ {
ViewName: "containers", ViewName: "services",
Key: 'R', Key: 'd',
Modifier: gocui.ModNone,
Handler: gui.handleServiceRemoveMenu,
Description: gui.Tr.SLocalize("removeService"),
},
{
ViewName: "services",
Key: 's',
Modifier: gocui.ModNone,
Handler: gui.handleServiceStop,
Description: gui.Tr.SLocalize("stop"),
},
{
ViewName: "services",
Key: 'r',
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleServiceRestart, Handler: gui.handleServiceRestart,
Description: gui.Tr.SLocalize("restartService"), Description: gui.Tr.SLocalize("restart"),
},
{
ViewName: "services",
Key: 'a',
Modifier: gocui.ModNone,
Handler: gui.handleServiceAttach,
Description: gui.Tr.SLocalize("attach"),
}, },
{ {
ViewName: "images", ViewName: "images",
@ -216,7 +237,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
} }
// TODO: add more views here // TODO: add more views here
for _, viewName := range []string{"status", "containers", "images", "menu"} { for _, viewName := range []string{"status", "services", "containers", "images", "menu"} {
bindings = append(bindings, []*Binding{ bindings = append(bindings, []*Binding{
{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView}, {ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView}, {ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},
@ -232,6 +253,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
focus func(*gocui.Gui, *gocui.View) error focus func(*gocui.Gui, *gocui.View) error
}{ }{
"menu": {prevLine: gui.handleMenuPrevLine, nextLine: gui.handleMenuNextLine, focus: gui.handleMenuSelect}, "menu": {prevLine: gui.handleMenuPrevLine, nextLine: gui.handleMenuNextLine, focus: gui.handleMenuSelect},
"services": {prevLine: gui.handleServicesPrevLine, nextLine: gui.handleServicesNextLine, focus: gui.handleServicesFocus},
"containers": {prevLine: gui.handleContainersPrevLine, nextLine: gui.handleContainersNextLine, focus: gui.handleContainersFocus}, "containers": {prevLine: gui.handleContainersPrevLine, nextLine: gui.handleContainersNextLine, focus: gui.handleContainersFocus},
"images": {prevLine: gui.handleImagesPrevLine, nextLine: gui.handleImagesNextLine, focus: gui.handleImagesFocus}, "images": {prevLine: gui.handleImagesPrevLine, nextLine: gui.handleImagesNextLine, focus: gui.handleImagesFocus},
} }

262
pkg/gui/services_panel.go Normal file
View file

@ -0,0 +1,262 @@
package gui
import (
"encoding/json"
"fmt"
"github.com/fatih/color"
"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) getServiceContexts() []string {
return []string{"logs", "config", "stats"}
}
func (gui *Gui) getSelectedService(g *gocui.Gui) (*commands.Service, error) {
selectedLine := gui.State.Panels.Services.SelectedLine
if selectedLine == -1 {
return &commands.Service{}, errors.New("no service selected")
}
return gui.State.Services[selectedLine], nil
}
func (gui *Gui) handleServicesFocus(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
cx, cy := v.Cursor()
_, oy := v.Origin()
prevSelectedLine := gui.State.Panels.Services.SelectedLine
newSelectedLine := cy - oy
if newSelectedLine > len(gui.State.Services)-1 || len(utils.Decolorise(gui.State.Services[newSelectedLine].Name)) < cx {
return gui.handleServiceSelect(gui.g, v)
}
gui.State.Panels.Services.SelectedLine = newSelectedLine
if prevSelectedLine == newSelectedLine && gui.currentViewName() == v.Name() {
return nil
}
return gui.handleServiceSelect(gui.g, v)
}
func (gui *Gui) handleServiceSelect(g *gocui.Gui, v *gocui.View) error {
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
service, err := gui.getSelectedService(g)
if err != nil {
return err
}
key := service.ID + "-" + gui.getServiceContexts()[gui.State.Panels.Services.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.Services.SelectedLine, len(gui.State.Services), v); err != nil {
return err
}
mainView := gui.getMainView()
mainView.Clear()
mainView.SetOrigin(0, 0)
mainView.SetCursor(0, 0)
switch gui.getServiceContexts()[gui.State.Panels.Services.ContextIndex] {
case "logs":
if err := gui.renderServiceLogs(mainView, service); err != nil {
return err
}
case "config":
if err := gui.renderServiceConfig(mainView, service); err != nil {
return err
}
case "stats":
if err := gui.renderServiceStats(mainView, service); err != nil {
return err
}
default:
return errors.New("Unknown context for services panel")
}
return nil
}
func (gui *Gui) renderServiceConfig(mainView *gocui.View, service *commands.Service) error {
mainView.Autoscroll = false
mainView.Title = "Config"
gui.T.NewTask(func(stop chan struct{}) {
// TODO: actually show service config
data, err := json.MarshalIndent(&service.Container.Container, "", " ")
if err != nil {
gui.Log.Error(err)
return
}
gui.renderString(gui.g, "main", string(data))
})
return nil
}
func (gui *Gui) renderServiceStats(mainView *gocui.View, service *commands.Service) error {
return nil
}
func (gui *Gui) renderServiceLogs(mainView *gocui.View, service *commands.Service) error {
return nil
}
func (gui *Gui) handleServicesNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Services
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Services), false)
return gui.handleServiceSelect(gui.g, v)
}
func (gui *Gui) handleServicesPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Services
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Services), true)
return gui.handleServiceSelect(gui.g, v)
}
func (gui *Gui) handleServicesPrevContext(g *gocui.Gui, v *gocui.View) error {
contexts := gui.getServiceContexts()
if gui.State.Panels.Services.ContextIndex >= len(contexts)-1 {
gui.State.Panels.Services.ContextIndex = 0
} else {
gui.State.Panels.Services.ContextIndex++
}
gui.handleServiceSelect(gui.g, v)
return nil
}
func (gui *Gui) handleServicesNextContext(g *gocui.Gui, v *gocui.View) error {
contexts := gui.getServiceContexts()
if gui.State.Panels.Services.ContextIndex <= 0 {
gui.State.Panels.Services.ContextIndex = len(contexts) - 1
} else {
gui.State.Panels.Services.ContextIndex--
}
gui.handleServiceSelect(gui.g, v)
return nil
}
type removeServiceOption struct {
description string
command string
}
// GetDisplayStrings is a function.
func (r *removeServiceOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
}
func (gui *Gui) handleServiceRemoveMenu(g *gocui.Gui, v *gocui.View) error {
service, err := gui.getSelectedService(g)
if err != nil {
return nil
}
composeCommand := gui.Config.GetUserConfig().GetString("commandTemplates.dockerCompose")
options := []*removeServiceOption{
{
description: gui.Tr.SLocalize("remove"),
command: fmt.Sprintf("%s rm --stop --force %s", composeCommand, service.Name),
},
{
description: gui.Tr.SLocalize("removeWithVolumes"),
command: fmt.Sprintf("%s rm --stop --force -v %s", composeCommand, service.Name),
},
{
description: gui.Tr.SLocalize("cancel"),
},
}
handleMenuPress := func(index int) error {
if options[index].command == "" {
return nil
}
if err := gui.OSCommand.RunCommand(options[index].command); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshContainersAndServices()
}
return gui.createMenu("", options, len(options), handleMenuPress)
}
func (gui *Gui) handleServiceStop(g *gocui.Gui, v *gocui.View) error {
service, err := gui.getSelectedService(g)
if err != nil {
return nil
}
return gui.createConfirmationPanel(gui.g, v, gui.Tr.SLocalize("Confirm"), gui.Tr.SLocalize("StopService"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("StoppingStatus"), func() error {
if err := service.Stop(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshContainersAndServices()
})
}, nil)
}
func (gui *Gui) handleServiceRestart(g *gocui.Gui, v *gocui.View) error {
service, err := gui.getSelectedService(g)
if err != nil {
return nil
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("RestartingStatus"), func() error {
if err := service.Restart(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshContainersAndServices()
})
}
func (gui *Gui) handleServiceAttach(g *gocui.Gui, v *gocui.View) error {
service, err := gui.getSelectedService(g)
if err != nil {
return nil
}
c := service.Attach()
gui.SubProcess = c
return gui.Errors.ErrSubProcess
}

View file

@ -13,7 +13,7 @@ import (
var cyclableViews = []string{"status", "containers", "images"} var cyclableViews = []string{"status", "containers", "images"}
func (gui *Gui) refreshSidePanels(g *gocui.Gui) error { func (gui *Gui) refreshSidePanels(g *gocui.Gui) error {
if err := gui.refreshContainers(); err != nil { if err := gui.refreshContainersAndServices(); err != nil {
return err return err
} }
if err := gui.refreshImages(); err != nil { if err := gui.refreshImages(); err != nil {
@ -223,6 +223,11 @@ func (gui *Gui) renderOptionsMap(optionsMap map[string]string) error {
return gui.renderString(gui.g, "options", gui.optionsMapToString(optionsMap)) return gui.renderString(gui.g, "options", gui.optionsMapToString(optionsMap))
} }
func (gui *Gui) getServicesView() *gocui.View {
v, _ := gui.g.View("services")
return v
}
func (gui *Gui) getContainersView() *gocui.View { func (gui *Gui) getContainersView() *gocui.View {
v, _ := gui.g.View("containers") v, _ := gui.g.View("containers")
return v return v

View file

@ -445,7 +445,7 @@ func addEnglish(i18nObject *i18n.Bundle) error {
ID: "ignoreContainer", ID: "ignoreContainer",
Other: `add to .gitignore`, Other: `add to .gitignore`,
}, &i18n.Message{ }, &i18n.Message{
ID: "refreshContainers", ID: "refreshContainersAndServices",
Other: `refresh containers`, Other: `refresh containers`,
}, &i18n.Message{ }, &i18n.Message{
ID: "mergeIntoCurrentBranch", ID: "mergeIntoCurrentBranch",
@ -785,20 +785,20 @@ func addEnglish(i18nObject *i18n.Bundle) error {
Other: "stopping", Other: "stopping",
}, },
&i18n.Message{ &i18n.Message{
ID: "removeContainer", ID: "remove",
Other: "remove container", Other: "remove",
}, },
&i18n.Message{ &i18n.Message{
ID: "stopContainer", ID: "removeService",
Other: "stop container", Other: "remove containers",
}, },
&i18n.Message{ &i18n.Message{
ID: "restartContainer", ID: "stop",
Other: "restart container", Other: "stop",
}, },
&i18n.Message{ &i18n.Message{
ID: "restartService", ID: "restart",
Other: "restart container's service", Other: "restart",
}, },
&i18n.Message{ &i18n.Message{
ID: "previousContext", ID: "previousContext",
@ -809,8 +809,8 @@ func addEnglish(i18nObject *i18n.Bundle) error {
Other: "next context", Other: "next context",
}, },
&i18n.Message{ &i18n.Message{
ID: "attachContainer", ID: "attach",
Other: "attach to container", Other: "attach",
}, },
&i18n.Message{ &i18n.Message{
ID: "ContainersTitle", ID: "ContainersTitle",

48
pkg/tasks/tasks.go Normal file
View file

@ -0,0 +1,48 @@
package tasks
import "sync"
type TaskManager struct {
waitingTasks []*Task
currentTask *Task
waitingMutex sync.Mutex
}
type Task struct {
stop chan struct{}
notifyStopped chan struct{}
}
func NewTaskManager() *TaskManager {
return &TaskManager{}
}
func (t *TaskManager) NewTask(f func(stop chan struct{})) error {
t.waitingMutex.Lock()
defer t.waitingMutex.Unlock()
if t.currentTask != nil {
t.currentTask.Stop()
}
stop := make(chan struct{}, 1) // we don't want to block on this in case the task already returned
notifyStopped := make(chan struct{})
t.currentTask = &Task{
stop: stop,
notifyStopped: notifyStopped,
}
go func() {
f(stop)
notifyStopped <- struct{}{}
}()
return nil
}
func (t *Task) Stop() {
t.stop <- struct{}{}
<-t.notifyStopped
return
}