diff --git a/pkg/commands/docker.go b/pkg/commands/docker.go index 347e3edc..e0fa3b67 100644 --- a/pkg/commands/docker.go +++ b/pkg/commands/docker.go @@ -8,7 +8,6 @@ import ( "io" ogLog "log" "os/exec" - "sort" "strings" "sync" "time" @@ -20,7 +19,6 @@ import ( "github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/i18n" "github.com/jesseduffield/lazydocker/pkg/utils" - "github.com/samber/lo" "github.com/sirupsen/logrus" ) @@ -36,15 +34,11 @@ type DockerCommand struct { Config *config.AppConfig Client *client.Client InDockerComposeProject bool - ShowExited bool ErrorChan chan error ContainerMutex sync.Mutex ServiceMutex sync.Mutex - Containers []*Container - // DisplayContainers is the array of containers we will display in the containers panel. If Gui.ShowAllContainers is false, this will only be those containers which aren't based on a service. This reduces clutter and duplication in the UI - DisplayContainers []*Container - Closers []io.Closer + Closers []io.Closer } var _ io.Closer = &DockerCommand{} @@ -89,7 +83,6 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Translat Config: config, Client: cli, ErrorChan: errorChan, - ShowExited: true, InDockerComposeProject: true, Closers: []io.Closer{tunnelCloser}, } @@ -119,26 +112,7 @@ func (c *DockerCommand) Close() error { return utils.CloseMany(c.Closers) } -func (c *DockerCommand) MonitorContainerStats(ctx context.Context) { - // periodically loop through running containers and see if we need to create a monitor goroutine for any - // every second we check if we need to spawn a new goroutine - ticker := time.NewTicker(time.Second) - defer ticker.Stop() - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - for _, container := range c.Containers { - if !container.MonitoringStats { - go c.createClientStatMonitor(container) - } - } - } - } -} - -func (c *DockerCommand) createClientStatMonitor(container *Container) { +func (c *DockerCommand) CreateClientStatMonitor(container *Container) { container.MonitoringStats = true stream, err := c.Client.ContainerStats(context.Background(), container.ID, true) if err != nil { @@ -172,11 +146,11 @@ func (c *DockerCommand) createClientStatMonitor(container *Container) { container.MonitoringStats = false } -func (c *DockerCommand) RefreshContainersAndServices(currentServices []*Service) ([]*Container, []*Service, error) { +func (c *DockerCommand) RefreshContainersAndServices(currentServices []*Service, currentContainers []*Container) ([]*Container, []*Service, error) { c.ServiceMutex.Lock() defer c.ServiceMutex.Unlock() - containers, err := c.GetContainers() + containers, err := c.GetContainers(currentContainers) if err != nil { return nil, nil, err } @@ -194,17 +168,7 @@ func (c *DockerCommand) RefreshContainersAndServices(currentServices []*Service) c.assignContainersToServices(containers, services) - displayContainers := containers - if !c.Config.UserConfig.Gui.ShowAllContainers { - displayContainers = c.obtainStandaloneContainers(containers, services) - } - - c.Containers = containers - c.DisplayContainers = c.filterOutExited(displayContainers) - c.DisplayContainers = c.filterOutIgnoredContainers(c.DisplayContainers) - c.DisplayContainers = c.sortedContainers(c.DisplayContainers) - - return c.DisplayContainers, services, nil + return containers, services, nil } func (c *DockerCommand) assignContainersToServices(containers []*Container, services []*Service) { @@ -220,72 +184,11 @@ L: } } -// filterOutExited filters out the exited containers if c.ShowExited is false -func (c *DockerCommand) filterOutExited(containers []*Container) []*Container { - if c.ShowExited { - return containers - } - toReturn := []*Container{} - for _, container := range containers { - if container.Container.State != "exited" { - toReturn = append(toReturn, container) - } - } - return toReturn -} - -func (c *DockerCommand) filterOutIgnoredContainers(containers []*Container) []*Container { - return lo.Filter(containers, func(container *Container, _ int) bool { - return !lo.SomeBy(c.Config.UserConfig.Ignore, func(ignore string) bool { - return strings.Contains(container.Name, ignore) - }) - }) -} - -// sortedContainers returns containers sorted by state if c.SortContainersByState is true (follows 1- running, 2- exited, 3- created) -// and sorted by name if c.SortContainersByState is false -func (c *DockerCommand) sortedContainers(containers []*Container) []*Container { - if !c.Config.UserConfig.Gui.LegacySortContainers { - states := map[string]int{ - "running": 1, - "exited": 2, - "created": 3, - } - sort.Slice(containers, func(i, j int) bool { - stateLeft := states[containers[i].Container.State] - stateRight := states[containers[j].Container.State] - if stateLeft == stateRight { - return containers[i].Name < containers[j].Name - } - return states[containers[i].Container.State] < states[containers[j].Container.State] - }) - } - return containers -} - -// obtainStandaloneContainers returns standalone containers. Standalone containers are containers which are either one-off containers, or whose service is not part of this docker-compose context -func (c *DockerCommand) obtainStandaloneContainers(containers []*Container, services []*Service) []*Container { - standaloneContainers := []*Container{} -L: - for _, container := range containers { - for _, service := range services { - if !container.OneOff && container.ServiceName != "" && container.ServiceName == service.Name { - continue L - } - } - standaloneContainers = append(standaloneContainers, container) - } - - return standaloneContainers -} - // GetContainers gets the docker containers -func (c *DockerCommand) GetContainers() ([]*Container, error) { +func (c *DockerCommand) GetContainers(existingContainers []*Container) ([]*Container, error) { c.ContainerMutex.Lock() defer c.ContainerMutex.Unlock() - existingContainers := c.Containers - containers, err := c.Client.ContainerList(context.Background(), types.ContainerListOptions{All: true}) if err != nil { return nil, err @@ -369,11 +272,11 @@ func (c *DockerCommand) GetServices() ([]*Service, error) { // 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() error { +func (c *DockerCommand) UpdateContainerDetails(containers []*Container) error { c.ContainerMutex.Lock() defer c.ContainerMutex.Unlock() - for _, container := range c.Containers { + for _, container := range containers { details, err := c.Client.ContainerInspect(context.Background(), container.ID) if err != nil { c.Log.Error(err) diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index e6b7d870..e73404e2 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -8,7 +8,6 @@ import ( "github.com/docker/docker/api/types" "github.com/fatih/color" - "github.com/go-errors/errors" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazydocker/pkg/commands" "github.com/jesseduffield/lazydocker/pkg/config" @@ -16,81 +15,97 @@ import ( "github.com/samber/lo" ) -// list panel functions - -func (gui *Gui) getContainerContexts() []string { - return []string{"logs", "stats", "env", "config", "top"} -} - -func (gui *Gui) getContainerContextTitles() []string { - return []string{gui.Tr.LogsTitle, gui.Tr.StatsTitle, gui.Tr.EnvTitle, gui.Tr.ConfigTitle, gui.Tr.TopTitle} -} - -func (gui *Gui) getSelectedContainer() (*commands.Container, error) { - selectedLine := gui.State.Panels.Containers.SelectedLine - if selectedLine == -1 { - return &commands.Container{}, gui.Errors.ErrNoContainers +func (gui *Gui) getContainersPanel() *SideListPanel[*commands.Container] { + states := map[string]int{ + "running": 1, + "exited": 2, + "created": 3, } - return gui.DockerCommand.DisplayContainers[selectedLine], nil -} - -func (gui *Gui) handleContainersClick(g *gocui.Gui, v *gocui.View) error { - itemCount := len(gui.DockerCommand.DisplayContainers) - handleSelect := gui.handleContainerSelect - selectedLine := &gui.State.Panels.Containers.SelectedLine - - return gui.handleClick(v, itemCount, selectedLine, handleSelect) -} - -func (gui *Gui) handleContainerSelect(g *gocui.Gui, v *gocui.View) error { - container, err := gui.getSelectedContainer() - if err != nil { - if err != gui.Errors.ErrNoContainers { - return err + // Standalone containers are containers which are either one-off containers, or whose service is not part of this docker-compose context. + isStandaloneContainer := func(container *commands.Container) bool { + if container.OneOff || container.ServiceName == "" { + return true } - return nil + + return !lo.SomeBy(gui.Panels.Services.list.GetAllItems(), func(service *commands.Service) bool { + return service.Name == container.ServiceName + }) } - gui.focusY(gui.State.Panels.Containers.SelectedLine, len(gui.DockerCommand.DisplayContainers), v) + return &SideListPanel[*commands.Container]{ + contextKeyPrefix: "containers", + ListPanel: ListPanel[*commands.Container]{ + list: NewFilteredList[*commands.Container](), + view: gui.Views.Containers, + }, + contextIdx: 0, + noItemsMessge: gui.Tr.NoContainers, + gui: gui.intoInterface(), + contexts: []ContextConfig[*commands.Container]{ + { + key: "logs", + title: gui.Tr.LogsTitle, + render: gui.renderContainerLogsToMain, + }, + { + key: "stats", + title: gui.Tr.StatsTitle, + render: gui.renderContainerStats, + }, + { + key: "env", + title: gui.Tr.EnvTitle, + render: gui.renderContainerEnv, + }, + { + key: "config", + title: gui.Tr.ConfigTitle, + render: gui.renderContainerConfig, + }, + { + key: "top", + title: gui.Tr.TopTitle, + render: gui.renderContainerTop, + }, + }, + getSearchStrings: func(container *commands.Container) []string { + // TODO: think about more things to search on + return []string{container.Name} + }, + getContextCacheKey: func(container *commands.Container) string { + return container.ID + "-" + container.Container.State + }, + // sortedContainers returns containers sorted by state if c.SortContainersByState is true (follows 1- running, 2- exited, 3- created) + // and sorted by name if c.SortContainersByState is false + sort: func(a *commands.Container, b *commands.Container) bool { + if gui.Config.UserConfig.Gui.LegacySortContainers { + return a.Name < b.Name + } - key := "containers-" + container.ID + "-" + gui.getContainerContexts()[gui.State.Panels.Containers.ContextIndex] - if !gui.shouldRefresh(key) { - return nil + stateLeft := states[a.Container.State] + stateRight := states[b.Container.State] + if stateLeft == stateRight { + return a.Name < b.Name + } + + return states[a.Container.State] < states[b.Container.State] + }, + filter: func(container *commands.Container) bool { + // Note that this is O(N*M) time complexity where N is the number of services + // and M is the number of containers. We expect N to be small but M may be large, + // so we will need to keep an eye on this. + if !gui.Config.UserConfig.Gui.ShowAllContainers && !isStandaloneContainer(container) { + return false + } + + if !gui.State.ShowExitedContainers && container.Container.State == "exited" { + return false + } + + return true + }, } - - mainView := gui.getMainView() - mainView.Tabs = gui.getContainerContextTitles() - mainView.TabIndex = gui.State.Panels.Containers.ContextIndex - - gui.clearMainView() - - switch gui.getContainerContexts()[gui.State.Panels.Containers.ContextIndex] { - case "logs": - if err := gui.renderContainerLogsToMain(container); err != nil { - return err - } - case "config": - if err := gui.renderContainerConfig(container); err != nil { - return err - } - case "env": - if err := gui.renderContainerEnv(container); err != nil { - return err - } - case "stats": - if err := gui.renderContainerStats(container); err != nil { - return err - } - case "top": - if err := gui.renderContainerTop(container); err != nil { - return err - } - default: - return errors.New("Unknown context for containers panel") - } - - return nil } func (gui *Gui) renderContainerEnv(container *commands.Container) error { @@ -236,14 +251,16 @@ func (gui *Gui) refreshContainersAndServices() error { originalSelectedLineIdx := gui.Panels.Services.selectedIdx selectedService, isServiceSelected := gui.Panels.Services.list.TryGet(originalSelectedLineIdx) - _, services, err := gui.DockerCommand.RefreshContainersAndServices( + containers, services, err := gui.DockerCommand.RefreshContainersAndServices( gui.Panels.Services.list.GetAllItems(), + gui.Panels.Containers.list.GetAllItems(), ) if err != nil { return err } gui.Panels.Services.SetItems(services) + gui.Panels.Containers.SetItems(containers) // see if our selected service has moved if isServiceSelected { @@ -258,91 +275,20 @@ func (gui *Gui) refreshContainersAndServices() error { } } - if len(gui.DockerCommand.DisplayContainers) > 0 && gui.State.Panels.Containers.SelectedLine == -1 { - gui.State.Panels.Containers.SelectedLine = 0 - } - if len(gui.DockerCommand.DisplayContainers)-1 < gui.State.Panels.Containers.SelectedLine { - gui.State.Panels.Containers.SelectedLine = len(gui.DockerCommand.DisplayContainers) - 1 - } - - gui.renderContainersAndServices() - - return nil + return gui.renderContainersAndServices() } -func (gui *Gui) renderContainersAndServices() { +func (gui *Gui) renderContainersAndServices() error { if gui.DockerCommand.InDockerComposeProject { if err := gui.Panels.Services.RerenderList(); err != nil { - gui.ErrorChan <- err - } - } - - gui.g.Update(func(g *gocui.Gui) error { - containersView := gui.getContainersView() - containersView.Clear() - isFocused := gui.g.CurrentView().Name() == "containers" - - list, err := utils.RenderList(gui.DockerCommand.DisplayContainers, utils.IsFocused(isFocused)) - if err != nil { return err } - fmt.Fprint(containersView, list) - - if containersView == g.CurrentView() { - if err := gui.handleContainerSelect(g, containersView); err != nil { - return err - } - } - - return nil - }) -} - -func (gui *Gui) handleContainersNextLine(g *gocui.Gui, v *gocui.View) error { - if gui.popupPanelFocused() || gui.g.CurrentView() != v { - return nil } - panelState := gui.State.Panels.Containers - gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.DisplayContainers), false) - - return gui.handleContainerSelect(gui.g, v) -} - -func (gui *Gui) handleContainersPrevLine(g *gocui.Gui, v *gocui.View) error { - if gui.popupPanelFocused() || gui.g.CurrentView() != v { - return nil + if err := gui.Panels.Containers.RerenderList(); err != nil { + return err } - panelState := gui.State.Panels.Containers - gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.DisplayContainers), true) - - return gui.handleContainerSelect(gui.g, v) -} - -func (gui *Gui) handleContainersNextContext(g *gocui.Gui, v *gocui.View) error { - contexts := gui.getContainerContexts() - if gui.State.Panels.Containers.ContextIndex >= len(contexts)-1 { - gui.State.Panels.Containers.ContextIndex = 0 - } else { - gui.State.Panels.Containers.ContextIndex++ - } - - _ = gui.handleContainerSelect(gui.g, v) - - return nil -} - -func (gui *Gui) handleContainersPrevContext(g *gocui.Gui, v *gocui.View) error { - contexts := gui.getContainerContexts() - if gui.State.Panels.Containers.ContextIndex <= 0 { - gui.State.Panels.Containers.ContextIndex = len(contexts) - 1 - } else { - gui.State.Panels.Containers.ContextIndex-- - } - - _ = gui.handleContainerSelect(gui.g, v) - return nil } @@ -358,17 +304,13 @@ func (r *removeContainerOption) GetDisplayStrings(isFocused bool) []string { } func (gui *Gui) handleHideStoppedContainers(g *gocui.Gui, v *gocui.View) error { - gui.DockerCommand.ShowExited = !gui.DockerCommand.ShowExited + gui.State.ShowExitedContainers = !gui.State.ShowExitedContainers - if err := gui.refreshContainersAndServices(); err != nil { - return err - } - - return nil + return gui.Panels.Containers.RerenderList() } func (gui *Gui) handleContainersRemoveMenu(g *gocui.Gui, v *gocui.View) error { - container, err := gui.getSelectedContainer() + container, err := gui.Panels.Containers.GetSelectedItem() if err != nil { return nil } @@ -431,7 +373,7 @@ func (gui *Gui) PauseContainer(container *commands.Container) error { } func (gui *Gui) handleContainerPause(g *gocui.Gui, v *gocui.View) error { - container, err := gui.getSelectedContainer() + container, err := gui.Panels.Containers.GetSelectedItem() if err != nil { return nil } @@ -440,7 +382,7 @@ func (gui *Gui) handleContainerPause(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error { - container, err := gui.getSelectedContainer() + container, err := gui.Panels.Containers.GetSelectedItem() if err != nil { return nil } @@ -457,7 +399,7 @@ func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleContainerRestart(g *gocui.Gui, v *gocui.View) error { - container, err := gui.getSelectedContainer() + container, err := gui.Panels.Containers.GetSelectedItem() if err != nil { return nil } @@ -472,7 +414,7 @@ func (gui *Gui) handleContainerRestart(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error { - container, err := gui.getSelectedContainer() + container, err := gui.Panels.Containers.GetSelectedItem() if err != nil { return nil } @@ -498,7 +440,7 @@ func (gui *Gui) handlePruneContainers() error { } func (gui *Gui) handleContainerViewLogs(g *gocui.Gui, v *gocui.View) error { - container, err := gui.getSelectedContainer() + container, err := gui.Panels.Containers.GetSelectedItem() if err != nil { return nil } @@ -509,7 +451,7 @@ func (gui *Gui) handleContainerViewLogs(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleContainersExecShell(g *gocui.Gui, v *gocui.View) error { - container, err := gui.getSelectedContainer() + container, err := gui.Panels.Containers.GetSelectedItem() if err != nil { return nil } @@ -530,7 +472,7 @@ func (gui *Gui) containerExecShell(container *commands.Container) error { } func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error { - container, err := gui.getSelectedContainer() + container, err := gui.Panels.Containers.GetSelectedItem() if err != nil { return nil } @@ -547,8 +489,10 @@ func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error func (gui *Gui) handleStopContainers() error { return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmStopContainers, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error { - for _, container := range gui.DockerCommand.Containers { - _ = container.Stop() + for _, container := range gui.Panels.Containers.list.GetAllItems() { + if err := container.Stop(); err != nil { + gui.Log.Error(err) + } } return nil @@ -559,8 +503,10 @@ func (gui *Gui) handleStopContainers() error { func (gui *Gui) handleRemoveContainers() error { return gui.createConfirmationPanel(gui.Tr.Confirm, gui.Tr.ConfirmRemoveContainers, func(g *gocui.Gui, v *gocui.View) error { return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error { - for _, container := range gui.DockerCommand.Containers { - _ = container.Remove(types.ContainerRemoveOptions{Force: true}) + for _, container := range gui.Panels.Containers.list.GetAllItems() { + if err := container.Remove(types.ContainerRemoveOptions{Force: true}); err != nil { + gui.Log.Error(err) + } } return nil @@ -592,7 +538,7 @@ func (gui *Gui) handleContainersBulkCommand(g *gocui.Gui, v *gocui.View) error { // Open first port in browser func (gui *Gui) handleContainersOpenInBrowserCommand(g *gocui.Gui, v *gocui.View) error { - container, err := gui.getSelectedContainer() + container, err := gui.Panels.Containers.GetSelectedItem() if err != nil { return nil } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 494e3447..1a9d9d00 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -22,30 +22,6 @@ import ( // OverlappingEdges determines if panel edges overlap var OverlappingEdges = false -// SentinelErrors are the errors that have special meaning and need to be checked -// by calling functions. The less of these, the better -type SentinelErrors struct { - ErrNoContainers error - ErrNoVolumes error -} - -// GenerateSentinelErrors makes the sentinel errors for the gui. We're defining it here -// because we can't do package-scoped errors with localization, and also because -// it seems like package-scoped variables are bad in general -// https://dave.cheney.net/2017/06/11/go-without-package-scoped-variables -// In the future it would be good to implement some of the recommendations of -// that article. For now, if we don't need an error to be a sentinel, we will just -// define it inline. This has implications for error messages that pop up everywhere -// in that we'll be duplicating the default values. We may need to look at -// having a default localisation bundle defined, and just using keys-only when -// localising things in the code. -func (gui *Gui) GenerateSentinelErrors() { - gui.Errors = SentinelErrors{ - ErrNoContainers: errors.New(gui.Tr.NoContainers), - ErrNoVolumes: errors.New(gui.Tr.NoVolumes), - } -} - // Gui wraps the gocui Gui object which handles rendering and events type Gui struct { g *gocui.Gui @@ -55,7 +31,6 @@ type Gui struct { State guiState Config *config.AppConfig Tr *i18n.TranslationSet - Errors SentinelErrors statusManager *statusManager T *tasks.TaskManager ErrorChan chan error @@ -73,9 +48,10 @@ type Gui struct { } type Panels struct { - Images *SideListPanel[*commands.Image] - Services *SideListPanel[*commands.Service] - Volumes *SideListPanel[*commands.Volume] + Services *SideListPanel[*commands.Service] + Containers *SideListPanel[*commands.Container] + Images *SideListPanel[*commands.Image] + Volumes *SideListPanel[*commands.Volume] } type Mutexes struct { @@ -83,11 +59,6 @@ type Mutexes struct { ViewStackMutex sync.Mutex } -type containerPanelState struct { - SelectedLine int - ContextIndex int // for specifying if you are looking at logs/stats/config/etc -} - type projectState struct { ContextIndex int // for specifying if you are looking at credits/logs } @@ -103,10 +74,9 @@ type mainPanelState struct { } type panelStates struct { - Containers *containerPanelState - Menu *menuPanelState - Main *mainPanelState - Project *projectState + Menu *menuPanelState + Main *mainPanelState + Project *projectState } type guiState struct { @@ -118,6 +88,9 @@ type guiState struct { SubProcessOutput string Stats map[string]commands.ContainerStats + // if true, we show containers with an 'exited' status in the contaniners panel + ShowExitedContainers bool + ScreenMode WindowMaximisation Searching searchingState @@ -156,8 +129,7 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand initialState := guiState{ Platform: *oSCommand.Platform, Panels: &panelStates{ - Containers: &containerPanelState{SelectedLine: -1, ContextIndex: 0}, - Menu: &menuPanelState{SelectedLine: 0}, + Menu: &menuPanelState{SelectedLine: 0}, Main: &mainPanelState{ ObjectKey: "", }, @@ -170,6 +142,7 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand Images: NewFilteredList[*commands.Image](), Volumes: NewFilteredList[*commands.Volume](), }, + ShowExitedContainers: true, } cyclableViews := []string{"project", "containers", "images", "volumes"} @@ -191,8 +164,6 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand CyclableViews: cyclableViews, } - gui.GenerateSentinelErrors() - return gui, nil } @@ -246,12 +217,6 @@ func (gui *Gui) Run() error { throttledRefresh := throttle.ThrottleFunc(time.Millisecond*50, true, gui.refresh) defer throttledRefresh.Stop() - ctx, finish := context.WithCancel(context.Background()) - defer finish() - - go gui.listenForEvents(ctx, throttledRefresh.Trigger) - go gui.DockerCommand.MonitorContainerStats(ctx) - go func() { for err := range gui.ErrorChan { if err == nil { @@ -274,9 +239,10 @@ func (gui *Gui) Run() error { // TODO: see if we can avoid the circular dependency gui.Panels = Panels{ - Images: gui.getImagesPanel(), - Services: gui.getServicesPanel(), - Volumes: gui.getVolumesPanel(), + Services: gui.getServicesPanel(), + Containers: gui.getContainersPanel(), + Images: gui.getImagesPanel(), + Volumes: gui.getVolumesPanel(), } if err = gui.keybindings(g); err != nil { @@ -295,11 +261,17 @@ func (gui *Gui) Run() error { } } + ctx, finish := context.WithCancel(context.Background()) + defer finish() + + go gui.listenForEvents(ctx, throttledRefresh.Trigger) + go gui.monitorContainerStats(ctx) + go func() { throttledRefresh.Trigger() gui.goEvery(time.Millisecond*30, gui.reRenderMain) - gui.goEvery(time.Millisecond*1000, gui.DockerCommand.UpdateContainerDetails) + gui.goEvery(time.Millisecond*1000, gui.updateContainerDetails) gui.goEvery(time.Millisecond*1000, gui.checkForContextChange) gui.goEvery(time.Millisecond*1000, gui.rerenderContainersAndServices) }() @@ -311,6 +283,10 @@ func (gui *Gui) Run() error { return err } +func (gui *Gui) updateContainerDetails() error { + return gui.DockerCommand.UpdateContainerDetails(gui.Panels.Containers.list.GetAllItems()) +} + func (gui *Gui) rerenderContainersAndServices() error { // we need to regularly re-render these because their stats will be changed in the background gui.renderContainersAndServices() @@ -479,3 +455,22 @@ func (gui *Gui) IgnoreStrings() []string { func (gui *Gui) Update(f func() error) { gui.g.Update(func(*gocui.Gui) error { return f() }) } + +func (gui *Gui) monitorContainerStats(ctx context.Context) { + // periodically loop through running containers and see if we need to create a monitor goroutine for any + // every second we check if we need to spawn a new goroutine + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + for _, container := range gui.Panels.Containers.list.GetAllItems() { + if !container.MonitoringStats { + go gui.DockerCommand.CreateClientStatMonitor(container) + } + } + } + } +} diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index b1a40fdd..04b3baeb 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -202,14 +202,14 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { ViewName: "containers", Key: '[', Modifier: gocui.ModNone, - Handler: gui.handleContainersPrevContext, + Handler: wrappedHandler(gui.Panels.Containers.OnPrevContext), Description: gui.Tr.PreviousContext, }, { ViewName: "containers", Key: ']', Modifier: gocui.ModNone, - Handler: gui.handleContainersNextContext, + Handler: wrappedHandler(gui.Panels.Containers.OnNextContext), Description: gui.Tr.NextContext, }, { @@ -585,7 +585,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { }{ "menu": {onKeyUpPress: gui.handleMenuPrevLine, onKeyDownPress: gui.handleMenuNextLine, onClick: gui.handleMenuClick}, "services": {onKeyUpPress: wrappedHandler(gui.Panels.Services.OnPrevLine), onKeyDownPress: wrappedHandler(gui.Panels.Services.OnNextLine), onClick: wrappedHandler(gui.Panels.Services.OnClick)}, - "containers": {onKeyUpPress: gui.handleContainersPrevLine, onKeyDownPress: gui.handleContainersNextLine, onClick: gui.handleContainersClick}, + "containers": {onKeyUpPress: wrappedHandler(gui.Panels.Containers.OnPrevLine), onKeyDownPress: wrappedHandler(gui.Panels.Containers.OnNextLine), onClick: wrappedHandler(gui.Panels.Containers.OnClick)}, "images": {onKeyUpPress: wrappedHandler(gui.Panels.Images.OnPrevLine), onKeyDownPress: wrappedHandler(gui.Panels.Images.OnNextLine), onClick: wrappedHandler(gui.Panels.Images.OnClick)}, "volumes": {onKeyUpPress: wrappedHandler(gui.Panels.Volumes.OnPrevLine), onKeyDownPress: wrappedHandler(gui.Panels.Volumes.OnNextLine), onClick: wrappedHandler(gui.Panels.Volumes.OnClick)}, "main": {onKeyUpPress: gui.scrollUpMain, onKeyDownPress: gui.scrollDownMain, onClick: gui.handleMainClick}, diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index a6635706..1e332932 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -126,8 +126,7 @@ func (gui *Gui) focusPointInView(view *gocui.View) { } listViews := map[string]listViewState{ - "containers": {selectedLine: gui.State.Panels.Containers.SelectedLine, lineCount: len(gui.DockerCommand.DisplayContainers)}, - "menu": {selectedLine: gui.State.Panels.Menu.SelectedLine, lineCount: gui.State.MenuItemCount}, + "menu": {selectedLine: gui.State.Panels.Menu.SelectedLine, lineCount: gui.State.MenuItemCount}, } if state, ok := listViews[view.Name()]; ok { @@ -141,6 +140,8 @@ func (gui *Gui) focusPointInView(view *gocui.View) { gui.Panels.Services.Refocus() case "volumes": gui.Panels.Volumes.Refocus() + case "containers": + gui.Panels.Containers.Refocus() } } diff --git a/pkg/gui/list_panel.go b/pkg/gui/list_panel.go index 4736814c..57dc02d8 100644 --- a/pkg/gui/list_panel.go +++ b/pkg/gui/list_panel.go @@ -66,6 +66,9 @@ type SideListPanel[T comparable] struct { getContextCacheKey func(item T) string sort func(a, b T) bool + + // this filter is applied on top of additional default filters + filter func(T) bool } type ContextConfig[T any] struct { @@ -192,6 +195,10 @@ func (self *SideListPanel[T]) FilterAndSort() { filterString := self.gui.FilterString(self.view) self.list.Filter(func(item T, index int) bool { + if self.filter != nil && !self.filter(item) { + return false + } + if lo.SomeBy(self.gui.IgnoreStrings(), func(ignore string) bool { return lo.SomeBy(self.getSearchStrings(item), func(searchString string) bool { return strings.Contains(searchString, ignore) diff --git a/pkg/gui/main_panel.go b/pkg/gui/main_panel.go index 50eec7f2..db0092aa 100644 --- a/pkg/gui/main_panel.go +++ b/pkg/gui/main_panel.go @@ -91,8 +91,8 @@ func (gui *Gui) onMainTabClick(tabIndex int) error { gui.Panels.Services.SetContextIndex(tabIndex) return gui.Panels.Services.HandleSelect() case "containers": - gui.State.Panels.Containers.ContextIndex = tabIndex - return gui.handleContainerSelect(gui.g, gui.getContainersView()) + gui.Panels.Containers.SetContextIndex(tabIndex) + return gui.Panels.Containers.HandleSelect() case "images": gui.Panels.Images.SetContextIndex(tabIndex) return gui.Panels.Images.HandleSelect() diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 2154ebd3..82fc1cf4 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -79,7 +79,7 @@ func (gui *Gui) newLineFocused(v *gocui.View) error { case "services": return gui.Panels.Services.HandleSelect() case "containers": - return gui.handleContainerSelect(gui.g, v) + return gui.Panels.Containers.HandleSelect() case "images": return gui.Panels.Images.HandleSelect() case "volumes":