mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-24 08: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 {
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
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'
|
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"
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
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"}
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
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