mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-21 23:01:03 +00:00
more progress
This commit is contained in:
parent
b2e60e1914
commit
aa55ff1a8e
12 changed files with 618 additions and 131 deletions
|
|
@ -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,20 +125,7 @@ func (c *DockerCommand) GetContainers() ([]*Container, error) {
|
|||
}
|
||||
}
|
||||
|
||||
cmd := c.OSCommand.RunCustomCommand("docker inspect " + strings.Join(ids, " "))
|
||||
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]
|
||||
}
|
||||
c.UpdateContainerDetails(ownContainers)
|
||||
|
||||
// ownContainers, err = c.UpdateContainerStats(ownContainers)
|
||||
// if err != nil {
|
||||
|
|
@ -122,6 +135,61 @@ func (c *DockerCommand) GetContainers() ([]*Container, error) {
|
|||
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
|
||||
func (c *DockerCommand) GetImages() ([]*Image, error) {
|
||||
images, err := c.Client.ImageList(context.Background(), types.ImageListOptions{})
|
||||
|
|
|
|||
62
pkg/commands/service.go
Normal file
62
pkg/commands/service.go
Normal 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()
|
||||
}
|
||||
|
|
@ -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},
|
||||
}
|
||||
|
|
|
|||
262
pkg/gui/services_panel.go
Normal file
262
pkg/gui/services_panel.go
Normal 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
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
48
pkg/tasks/tasks.go
Normal file
48
pkg/tasks/tasks.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue