more progress

pull/1/head
Jesse Duffield 5 years ago
parent b2e60e1914
commit aa55ff1a8e

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

@ -3,6 +3,7 @@ package commands
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/docker/docker/api/types"
@ -10,16 +11,18 @@ import (
"github.com/docker/docker/client"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/sirupsen/logrus"
)
// DockerCommand is our main git interface
type DockerCommand struct {
Log *logrus.Entry
OSCommand *OSCommand
Tr *i18n.Localizer
Config config.AppConfigurer
Client *client.Client
Log *logrus.Entry
OSCommand *OSCommand
Tr *i18n.Localizer
Config config.AppConfigurer
Client *client.Client
InDockerComposeProject bool
}
// NewDockerCommand it runs git commands
@ -30,11 +33,12 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localize
}
return &DockerCommand{
Log: log,
OSCommand: osCommand,
Tr: tr,
Config: config,
Client: cli,
Log: log,
OSCommand: osCommand,
Tr: tr,
Config: config,
Client: cli,
InDockerComposeProject: true, // TODO: determine this at startup
}, nil
}
@ -72,7 +76,32 @@ func (c *DockerCommand) UpdateContainerStats(containers []*Container) ([]*Contai
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) {
containers, err := c.Client.ContainerList(context.Background(), types.ContainerListOptions{All: true})
if err != nil {
@ -81,15 +110,12 @@ func (c *DockerCommand) GetContainers() ([]*Container, error) {
ownContainers := make([]*Container, len(containers))
ids := []string{}
for i, container := range containers {
ids = append(ids, container.ID)
ownContainers[i] = &Container{
ID: container.ID,
Name: strings.TrimLeft(container.Names[0], "/"),
ServiceName: container.Labels["com.docker.compose.service"],
ServiceID: container.Labels["com.docker.compose.config-hash"],
ProjectName: container.Labels["com.docker.compose.project"],
ContainerNumber: container.Labels["com.docker.compose.container"],
Container: container,
@ -99,27 +125,69 @@ func (c *DockerCommand) GetContainers() ([]*Container, error) {
}
}
c.UpdateContainerDetails(ownContainers)
// ownContainers, err = c.UpdateContainerStats(ownContainers)
// if err != nil {
// return nil, err
// }
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 nil, err
return err
}
var details []*Details
if err := json.Unmarshal(output, &details); err != nil {
return nil, err
return err
}
for i, container := range ownContainers {
for i, container := range containers {
container.Details = *details[i]
}
// ownContainers, err = c.UpdateContainerStats(ownContainers)
// if err != nil {
// return nil, err
// }
return ownContainers, nil
return nil
}
// GetImages returns a slice of docker images

@ -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()
}

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

@ -81,24 +81,21 @@ func (gui *Gui) handleContainerSelect(g *gocui.Gui, v *gocui.View) error {
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.getContainerContexts()[gui.State.Panels.Containers.ContextIndex] {
case "logs":
if err := gui.renderContainerLogs(mainView, container, writerID); err != nil {
if err := gui.renderContainerLogs(mainView, container); err != nil {
return err
}
case "config":
if err := gui.renderContainerConfig(mainView, container, writerID); err != nil {
if err := gui.renderContainerConfig(mainView, container); err != nil {
return err
}
case "stats":
if err := gui.renderContainerStats(mainView, container, writerID); err != nil {
if err := gui.renderContainerStats(mainView, container); err != nil {
return err
}
default:
@ -108,7 +105,7 @@ func (gui *Gui) handleContainerSelect(g *gocui.Gui, v *gocui.View) error {
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.Title = "Config"
@ -116,12 +113,13 @@ func (gui *Gui) renderContainerConfig(mainView *gocui.View, container *commands.
if err != nil {
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.Title = "Stats"
@ -130,7 +128,9 @@ func (gui *Gui) renderContainerStats(mainView *gocui.View, container *commands.C
return err
}
go func() {
return gui.T.NewTask(func(stop chan struct{}) {
defer stream.Body.Close()
cpuUsageHistory := []float64{}
memoryUsageHistory := []float64{}
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())
}
if gui.State.Panels.Main.WriterID != writerID {
stream.Body.Close()
select {
case <-stop:
return
default:
}
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.Title = "Logs"
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
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
}
func (gui *Gui) runProcessWithLock(cmd *exec.Cmd) {
func (gui *Gui) obtainLock() {
gui.State.MainProcessChan <- struct{}{}
gui.State.MainProcessMutex.Lock()
cmd.Start()
go func() {
<-gui.State.MainProcessChan
cmd.Process.Kill()
}()
}
cmd.Wait()
func (gui *Gui) releaseLock() {
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
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.Split(bufio.ScanLines)
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'))
}
}
@ -228,13 +234,13 @@ func (gui *Gui) renderLogsForTTYContainer(mainView *gocui.View, container *comma
return nil
}
func (gui *Gui) refreshContainers() error {
func (gui *Gui) refreshContainersAndServices() error {
containersView := gui.getContainersView()
if containersView == nil {
// if the containersView hasn't been instantiated yet we just return
return nil
}
if err := gui.refreshStateContainers(); err != nil {
if err := gui.refreshStateContainersAndServices(); err != nil {
return err
}
@ -245,8 +251,15 @@ func (gui *Gui) refreshContainers() error {
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()
isFocused := gui.g.CurrentView().Name() == "containers"
list, err := utils.RenderList(gui.State.Containers, utils.IsFocused(isFocused))
@ -258,19 +271,37 @@ func (gui *Gui) refreshContainers() error {
if containersView == g.CurrentView() {
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
}
func (gui *Gui) refreshStateContainers() error {
containers, err := gui.DockerCommand.GetContainers()
func (gui *Gui) refreshStateContainersAndServices() error {
containers, services, err := gui.DockerCommand.GetContainersAndServices()
if err != nil {
return err
}
gui.State.Containers = containers
gui.State.Services = services
return nil
}
@ -346,22 +377,19 @@ func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error {
description: gui.Tr.SLocalize("remove"),
command: "docker rm " + container.ID[1:10],
configOptions: types.ContainerRemoveOptions{},
runCommand: true,
},
{
description: gui.Tr.SLocalize("removeWithVolumes"),
command: "docker rm --volumes " + container.ID[1:10],
configOptions: types.ContainerRemoveOptions{RemoveVolumes: true},
runCommand: true,
},
{
description: gui.Tr.SLocalize("cancel"),
runCommand: false,
},
}
handleMenuPress := func(index int) error {
if !options[index].runCommand {
if options[index].command == "" {
return nil
}
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 {
return err
}
return gui.refreshContainers()
return gui.refreshContainersAndServices()
}, nil)
}
} 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)
@ -400,7 +428,7 @@ func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshContainers()
return gui.refreshContainersAndServices()
})
}, 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.refreshContainers()
return gui.refreshContainersAndServices()
})
}
@ -432,18 +460,3 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error {
gui.SubProcess = c
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()
})
}

@ -23,6 +23,7 @@ import (
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazydocker/pkg/updates"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/sirupsen/logrus"
@ -74,6 +75,12 @@ type Gui struct {
Updater *updates.Updater
statusManager *statusManager
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 {
@ -87,7 +94,6 @@ type menuPanelState struct {
type mainPanelState struct {
ObjectKey string
WriterID int
}
type imagePanelState struct {
@ -96,6 +102,7 @@ type imagePanelState struct {
}
type panelStates struct {
Services *servicePanelState
Containers *containerPanelState
Menu *menuPanelState
Main *mainPanelState
@ -103,6 +110,7 @@ type panelStates struct {
}
type guiState struct {
Services []*commands.Service
Containers []*commands.Container
Images []*commands.Image
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",
Platform: *oSCommand.Platform,
Panels: &panelStates{
Services: &servicePanelState{SelectedLine: -1, ContextIndex: 0},
Containers: &containerPanelState{SelectedLine: -1, ContextIndex: 0},
Images: &imagePanelState{SelectedLine: -1, ContextIndex: 0},
Menu: &menuPanelState{SelectedLine: 0},
Main: &mainPanelState{
WriterID: 0,
ObjectKey: "",
},
},
@ -151,6 +159,7 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand
Tr: tr,
Updater: updater,
statusManager: &statusManager{},
T: tasks.NewTaskManager(),
}
gui.GenerateSentinelErrors()
@ -229,9 +238,6 @@ func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
if v == nil {
return nil
}
if v.Name() == "containers" || v.Name() == "images" {
gui.State.Panels.Main.WriterID++
}
gui.Log.Info(v.Name() + " focus lost")
return nil
}
@ -512,7 +518,7 @@ func (gui *Gui) Run() error {
gui.waitForIntro.Wait()
gui.goEvery(time.Millisecond*50, gui.renderAppStatus)
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()))

@ -78,16 +78,13 @@ func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error {
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 {
if err := gui.renderImageConfig(mainView, Image); err != nil {
return err
}
default:
@ -97,25 +94,27 @@ func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error {
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.Wrap = false
mainView.Title = "Config"
output := ""
output += utils.WithPadding("ID: ", 10) + image.Image.ID + "\n"
output += utils.WithPadding("Tags: ", 10) + utils.ColoredString(strings.Join(image.Image.RepoTags, ", "), color.FgGreen) + "\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"
gui.T.NewTask(func(stop chan struct{}) {
output := ""
output += utils.WithPadding("ID: ", 10) + image.Image.ID + "\n"
output += utils.WithPadding("Tags: ", 10) + utils.ColoredString(strings.Join(image.Image.RepoTags, ", "), color.FgGreen) + "\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()
if err != nil {
return err
}
history, err := image.RenderHistory()
if err != nil {
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
}

@ -156,35 +156,56 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Key: 'd',
Modifier: gocui.ModNone,
Handler: gui.handleContainersRemoveMenu,
Description: gui.Tr.SLocalize("removeContainer"),
Description: gui.Tr.SLocalize("remove"),
},
{
ViewName: "containers",
Key: 's',
Modifier: gocui.ModNone,
Handler: gui.handleContainerStop,
Description: gui.Tr.SLocalize("stopContainer"),
Description: gui.Tr.SLocalize("stop"),
},
{
ViewName: "containers",
Key: 'r',
Modifier: gocui.ModNone,
Handler: gui.handleContainerRestart,
Description: gui.Tr.SLocalize("restartContainer"),
Description: gui.Tr.SLocalize("restart"),
},
{
ViewName: "containers",
Key: 'a',
Modifier: gocui.ModNone,
Handler: gui.handleContainerAttach,
Description: gui.Tr.SLocalize("attachContainer"),
Description: gui.Tr.SLocalize("attach"),
},
{
ViewName: "containers",
Key: 'R',
ViewName: "services",
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,
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",
@ -216,7 +237,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
}
// 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{
{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView},
{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
}{
"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},
"images": {prevLine: gui.handleImagesPrevLine, nextLine: gui.handleImagesNextLine, focus: gui.handleImagesFocus},
}

@ -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
}

@ -13,7 +13,7 @@ import (
var cyclableViews = []string{"status", "containers", "images"}
func (gui *Gui) refreshSidePanels(g *gocui.Gui) error {
if err := gui.refreshContainers(); err != nil {
if err := gui.refreshContainersAndServices(); err != nil {
return err
}
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))
}
func (gui *Gui) getServicesView() *gocui.View {
v, _ := gui.g.View("services")
return v
}
func (gui *Gui) getContainersView() *gocui.View {
v, _ := gui.g.View("containers")
return v

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

@ -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
}
Loading…
Cancel
Save