diff --git a/pkg/commands/container.go b/pkg/commands/container.go index f46d7ffc..56aaa1e7 100644 --- a/pkg/commands/container.go +++ b/pkg/commands/container.go @@ -105,7 +105,7 @@ func (c *Container) Attach() (*exec.Cmd, error) { } // Top returns process information -func (c *Container) Top() (container.ContainerTopOKBody, error) { +func (c *Container) Top(ctx context.Context) (container.ContainerTopOKBody, error) { detail, err := c.Inspect() if err != nil { return container.ContainerTopOKBody{}, err @@ -116,7 +116,7 @@ func (c *Container) Top() (container.ContainerTopOKBody, error) { return container.ContainerTopOKBody{}, errors.New("container is not running") } - return c.Client.ContainerTop(context.Background(), c.ID, []string{}) + return c.Client.ContainerTop(ctx, c.ID, []string{}) } // PruneContainers prunes containers @@ -131,8 +131,8 @@ func (c *Container) Inspect() (dockerTypes.ContainerJSON, error) { } // RenderTop returns details about the container -func (c *Container) RenderTop() (string, error) { - result, err := c.Top() +func (c *Container) RenderTop(ctx context.Context) (string, error) { + result, err := c.Top(ctx) if err != nil { return "", err } diff --git a/pkg/commands/os.go b/pkg/commands/os.go index e449a6ee..79b7dea3 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -1,6 +1,7 @@ package commands import ( + "context" "fmt" "io/ioutil" "os" @@ -65,6 +66,15 @@ func (c *OSCommand) RunCommandWithOutput(command string) (string, error) { return output, err } +// RunCommandWithOutput wrapper around commands returning their output and error +func (c *OSCommand) RunCommandWithOutputContext(ctx context.Context, command string) (string, error) { + cmd := c.ExecutableFromStringContext(ctx, command) + before := time.Now() + output, err := sanitisedCommandOutput(cmd.Output()) + c.Log.Warn(fmt.Sprintf("'%s': %s", command, time.Since(before))) + return output, err +} + // RunExecutableWithOutput runs an executable file and returns its output func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) { return sanitisedCommandOutput(cmd.CombinedOutput()) @@ -79,10 +89,15 @@ func (c *OSCommand) RunExecutable(cmd *exec.Cmd) error { // ExecutableFromString takes a string like `docker ps -a` and returns an executable command for it func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd { splitCmd := str.ToArgv(commandStr) - // c.Log.Info(splitCmd) return c.command(splitCmd[0], splitCmd[1:]...) } +// Same as ExecutableFromString but cancellable via a context +func (c *OSCommand) ExecutableFromStringContext(ctx context.Context, commandStr string) *exec.Cmd { + splitCmd := str.ToArgv(commandStr) + return exec.CommandContext(ctx, splitCmd[0], splitCmd[1:]...) +} + // RunCommand runs a command and just returns the error func (c *OSCommand) RunCommand(command string) error { _, err := c.RunCommandWithOutput(command) diff --git a/pkg/commands/service.go b/pkg/commands/service.go index 599eb597..368b0dab 100644 --- a/pkg/commands/service.go +++ b/pkg/commands/service.go @@ -1,10 +1,9 @@ package commands import ( + "context" "os/exec" - "github.com/docker/docker/api/types/container" - dockerTypes "github.com/docker/docker/api/types" "github.com/jesseduffield/lazydocker/pkg/utils" "github.com/sirupsen/logrus" @@ -58,11 +57,6 @@ func (s *Service) Attach() (*exec.Cmd, error) { return s.Container.Attach() } -// Top returns process information -func (s *Service) Top() (container.ContainerTopOKBody, error) { - return s.Container.Top() -} - // ViewLogs attaches to a subprocess viewing the service's logs func (s *Service) ViewLogs() (*exec.Cmd, error) { templateString := s.OSCommand.Config.UserConfig.CommandTemplates.ViewServiceLogs @@ -78,12 +72,12 @@ func (s *Service) ViewLogs() (*exec.Cmd, error) { } // RenderTop renders the process list of the service -func (s *Service) RenderTop() (string, error) { +func (s *Service) RenderTop(ctx context.Context) (string, error) { templateString := s.OSCommand.Config.UserConfig.CommandTemplates.ServiceTop command := utils.ApplyTemplate( templateString, s.DockerCommand.NewCommandObject(CommandObject{Service: s}), ) - return s.OSCommand.RunCommandWithOutput(command) + return s.OSCommand.RunCommandWithOutputContext(ctx, command) } diff --git a/pkg/gui/container_logs.go b/pkg/gui/container_logs.go index 4759a1f9..7efe06bb 100644 --- a/pkg/gui/container_logs.go +++ b/pkg/gui/container_logs.go @@ -12,16 +12,19 @@ import ( "github.com/docker/docker/pkg/stdcopy" "github.com/fatih/color" "github.com/jesseduffield/lazydocker/pkg/commands" + "github.com/jesseduffield/lazydocker/pkg/tasks" "github.com/jesseduffield/lazydocker/pkg/utils" ) -func (gui *Gui) renderContainerLogsToMain(container *commands.Container) error { - mainView := gui.Views.Main - mainView.Autoscroll = true - mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel - - return gui.T.NewTickerTask(time.Millisecond*200, nil, func(stop, notifyStopped chan struct{}) { - gui.renderContainerLogsToMainAux(container, stop, notifyStopped) +func (gui *Gui) renderContainerLogsToMain(container *commands.Container) tasks.TaskFunc { + return gui.NewTickerTask(TickerTaskOpts{ + Func: func(stop, notifyStopped chan struct{}) { + gui.renderContainerLogsToMainAux(container, stop, notifyStopped) + }, + Duration: time.Millisecond * 200, + Before: nil, + Wrap: gui.Config.UserConfig.Gui.WrapMainPanel, + Autoscroll: true, }) } diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 03008490..8c316edc 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -1,6 +1,7 @@ package gui import ( + "context" "encoding/json" "fmt" "strings" @@ -14,6 +15,7 @@ import ( "github.com/jesseduffield/lazydocker/pkg/gui/panels" "github.com/jesseduffield/lazydocker/pkg/gui/presentation" "github.com/jesseduffield/lazydocker/pkg/gui/types" + "github.com/jesseduffield/lazydocker/pkg/tasks" "github.com/jesseduffield/lazydocker/pkg/utils" "github.com/samber/lo" ) @@ -119,28 +121,10 @@ func sortContainers(a *commands.Container, b *commands.Container, legacySort boo return containerStates[a.Container.State] < containerStates[b.Container.State] } -type MainRenderTask struct { - Wrap bool - Autoscroll bool - - StrContent string - - // TicketTask +func (gui *Gui) renderContainerEnv(container *commands.Container) tasks.TaskFunc { + return gui.NewSimpleRenderStringTask(func() string { return gui.containerEnv(container) }) } -func (gui *Gui) renderContainerEnv(container *commands.Container) error { - mainView := gui.Views.Main - mainView.Autoscroll = false - mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel - - return gui.T.NewTask(func(stop chan struct{}) { - _ = gui.RenderStringMain(gui.containerEnv(container)) - }) -} - -// I want a struct that gets returned explaining what needs to happen. So for example, -// autoscroll: true, wrap: true, content: string - func (gui *Gui) containerEnv(container *commands.Container) string { if !container.DetailsLoaded() { return gui.Tr.WaitingForContainerInfo @@ -172,14 +156,8 @@ func (gui *Gui) containerEnv(container *commands.Container) string { return output } -func (gui *Gui) renderContainerConfig(container *commands.Container) error { - mainView := gui.Views.Main - mainView.Autoscroll = false - mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel - - return gui.T.NewTask(func(stop chan struct{}) { - _ = gui.RenderStringMain(gui.containerConfigStr(container)) - }) +func (gui *Gui) renderContainerConfig(container *commands.Container) tasks.TaskFunc { + return gui.NewSimpleRenderStringTask(func() string { return gui.containerConfigStr(container) }) } func (gui *Gui) containerConfigStr(container *commands.Container) string { @@ -232,35 +210,48 @@ func (gui *Gui) containerConfigStr(container *commands.Container) string { return output } -func (gui *Gui) renderContainerStats(container *commands.Container) error { - mainView := gui.Views.Main - mainView.Autoscroll = false - mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel +func (gui *Gui) renderContainerStats(container *commands.Container) tasks.TaskFunc { + return gui.NewTickerTask(TickerTaskOpts{ + Func: func(stop, notifyStopped chan struct{}) { + contents, err := presentation.RenderStats(gui.Config.UserConfig, container, gui.Views.Main.Width()) + if err != nil { + _ = gui.createErrorPanel(err.Error()) + } - return gui.T.NewTickerTask(time.Second, func(stop chan struct{}) { gui.clearMainView() }, func(stop, notifyStopped chan struct{}) { - width, _ := mainView.Size() - - contents, err := presentation.RenderStats(gui.Config.UserConfig, container, width) - if err != nil { - _ = gui.createErrorPanel(err.Error()) - } - - gui.reRenderStringMain(contents) + gui.reRenderStringMain(contents) + }, + Duration: time.Second, + Before: func(stop chan struct{}) { gui.clearMainView() }, + Wrap: false, // wrapping looks bad here so we're overriding the config value + Autoscroll: false, }) } -func (gui *Gui) renderContainerTop(container *commands.Container) error { - mainView := gui.Views.Main - mainView.Autoscroll = false - mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel +// TODO: remove this and just use a context +func stopIntoCtx(stop chan struct{}) context.Context { + ctx, ctxCancel := context.WithCancel(context.Background()) + go func() { + <-stop + ctxCancel() + }() + return ctx +} - return gui.T.NewTickerTask(time.Second, func(stop chan struct{}) { gui.clearMainView() }, func(stop, notifyStopped chan struct{}) { - contents, err := container.RenderTop() - if err != nil { - gui.reRenderStringMain(err.Error()) - } +func (gui *Gui) renderContainerTop(container *commands.Container) tasks.TaskFunc { + return gui.NewTickerTask(TickerTaskOpts{ + Func: func(stop, notifyStopped chan struct{}) { + ctx := stopIntoCtx(stop) + contents, err := container.RenderTop(ctx) + if err != nil { + gui.RenderStringMain(err.Error()) + } - gui.reRenderStringMain(contents) + gui.reRenderStringMain(contents) + }, + Duration: time.Second, + Before: func(stop chan struct{}) { gui.clearMainView() }, + Wrap: gui.Config.UserConfig.Gui.WrapMainPanel, + Autoscroll: false, }) } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 26ca44a0..585cdd02 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -36,7 +36,7 @@ type Gui struct { Config *config.AppConfig Tr *i18n.TranslationSet statusManager *statusManager - T *tasks.TaskManager + taskManager *tasks.TaskManager ErrorChan chan error Views Views @@ -135,7 +135,7 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand Config: config, Tr: tr, statusManager: &statusManager{}, - T: tasks.NewTaskManager(log, tr), + taskManager: tasks.NewTaskManager(log, tr), ErrorChan: errorChan, } @@ -173,7 +173,7 @@ func (gui *Gui) goEvery(interval time.Duration, function func() error) { // Run setup the gui with keybindings and start the mainloop func (gui *Gui) Run() error { // closing our task manager which in turn closes the current task if there is any, so we aren't leaving processes lying around after closing lazydocker - defer gui.T.Close() + defer gui.taskManager.Close() g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, gocui.NORMAL, false, map[rune]string{}) if err != nil { diff --git a/pkg/gui/images_panel.go b/pkg/gui/images_panel.go index 673928d1..6ef50fa9 100644 --- a/pkg/gui/images_panel.go +++ b/pkg/gui/images_panel.go @@ -13,6 +13,7 @@ import ( "github.com/jesseduffield/lazydocker/pkg/gui/panels" "github.com/jesseduffield/lazydocker/pkg/gui/presentation" "github.com/jesseduffield/lazydocker/pkg/gui/types" + "github.com/jesseduffield/lazydocker/pkg/tasks" "github.com/jesseduffield/lazydocker/pkg/utils" "github.com/samber/lo" ) @@ -25,11 +26,9 @@ func (gui *Gui) getImagesPanel() *panels.SideListPanel[*commands.Image] { GetMainTabs: func() []panels.MainTab[*commands.Image] { return []panels.MainTab[*commands.Image]{ { - Key: "config", - Title: gui.Tr.ConfigTitle, - Render: func(image *commands.Image) error { - return gui.renderImageConfig(image) - }, + Key: "config", + Title: gui.Tr.ConfigTitle, + Render: gui.renderImageConfigTask, }, } }, @@ -66,13 +65,11 @@ func (gui *Gui) getImagesPanel() *panels.SideListPanel[*commands.Image] { } } -func (gui *Gui) renderImageConfig(image *commands.Image) error { - return gui.T.NewTask(func(stop chan struct{}) { - mainView := gui.Views.Main - mainView.Autoscroll = false - mainView.Wrap = false // don't care what your config is this page is ugly without wrapping - - _ = gui.RenderStringMain(gui.imageConfigStr(image)) +func (gui *Gui) renderImageConfigTask(image *commands.Image) tasks.TaskFunc { + return gui.NewRenderStringTask(RenderStringTaskOpts{ + GetStrContent: func() string { return gui.imageConfigStr(image) }, + Autoscroll: false, + Wrap: false, // don't care what your config is this page is ugly without wrapping }) } diff --git a/pkg/gui/panels/context_state.go b/pkg/gui/panels/context_state.go index 2cdfeec9..36610cff 100644 --- a/pkg/gui/panels/context_state.go +++ b/pkg/gui/panels/context_state.go @@ -1,6 +1,9 @@ package panels -import "github.com/samber/lo" +import ( + "github.com/jesseduffield/lazydocker/pkg/tasks" + "github.com/samber/lo" +) // A 'context' generally corresponds to an item and the tab in the main panel that we're // displaying. So if we switch to a new item, or change the tab in the panel panel @@ -24,7 +27,7 @@ type MainTab[T any] struct { // title of the tab, rendered in the main view Title string // function to render the content of the tab - Render func(item T) error + Render func(item T) tasks.TaskFunc } func (self *ContextState[T]) GetMainTabTitles() []string { diff --git a/pkg/gui/panels/side_list_panel.go b/pkg/gui/panels/side_list_panel.go index 19c734f2..f5473880 100644 --- a/pkg/gui/panels/side_list_panel.go +++ b/pkg/gui/panels/side_list_panel.go @@ -63,7 +63,7 @@ var _ ISideListPanel = &SideListPanel[int]{} type IGui interface { HandleClick(v *gocui.View, itemCount int, selectedLine *int, handleSelect func() error) error - RenderStringMain(message string) error + RenderStringMain(message string) FocusY(selectedLine int, itemCount int, view *gocui.View) ShouldRefresh(contextKey string) bool GetMainView() *gocui.View @@ -72,6 +72,8 @@ type IGui interface { FilterString(view *gocui.View) string IgnoreStrings() []string Update(func() error) + + QueueTask(f func(stop chan struct{})) error } func (self *SideListPanel[T]) HandleClick() error { @@ -105,7 +107,8 @@ func (self *SideListPanel[T]) HandleSelect() error { } if self.NoItemsMessage != "" { - return self.Gui.RenderStringMain(self.NoItemsMessage) + // TODO: use task for this + self.Gui.RenderStringMain(self.NoItemsMessage) } return nil @@ -130,7 +133,9 @@ func (self *SideListPanel[T]) renderContext(item T) error { mainView.Tabs = self.ContextState.GetMainTabTitles() mainView.TabIndex = self.ContextState.mainTabIdx - return self.ContextState.GetCurrentMainTab().Render(item) + task := self.ContextState.GetCurrentMainTab().Render(item) + + return self.Gui.QueueTask(task) } func (self *SideListPanel[T]) GetSelectedItem() (T, error) { diff --git a/pkg/gui/presentation/containers.go b/pkg/gui/presentation/containers.go index 36c5ea09..e1a9d309 100644 --- a/pkg/gui/presentation/containers.go +++ b/pkg/gui/presentation/containers.go @@ -14,18 +14,20 @@ import ( ) func GetContainerDisplayStrings(container *commands.Container) []string { - image := strings.TrimPrefix(container.Container.Image, "sha256:") - return []string{ getContainerDisplayStatus(container), getContainerDisplaySubstatus(container), container.Name, getDisplayCPUPerc(container), - utils.ColoredString(image, color.FgMagenta), utils.ColoredString(displayPorts(container), color.FgYellow), + utils.ColoredString(displayContainerImage(container), color.FgMagenta), } } +func displayContainerImage(container *commands.Container) string { + return strings.TrimPrefix(container.Container.Image, "sha256:") +} + func displayPorts(c *commands.Container) string { portStrings := lo.Map(c.Container.Ports, func(port dockerTypes.Port, _ int) string { if port.PublicPort == 0 { diff --git a/pkg/gui/presentation/services.go b/pkg/gui/presentation/services.go index 954c2968..c99f1586 100644 --- a/pkg/gui/presentation/services.go +++ b/pkg/gui/presentation/services.go @@ -14,6 +14,7 @@ func GetServiceDisplayStrings(service *commands.Service) []string { service.Name, "", "", + "", } } @@ -24,5 +25,6 @@ func GetServiceDisplayStrings(service *commands.Service) []string { service.Name, getDisplayCPUPerc(container), utils.ColoredString(displayPorts(container), color.FgYellow), + utils.ColoredString(displayContainerImage(container), color.FgMagenta), } } diff --git a/pkg/gui/project_panel.go b/pkg/gui/project_panel.go index 9db206d0..8a24a824 100644 --- a/pkg/gui/project_panel.go +++ b/pkg/gui/project_panel.go @@ -10,6 +10,7 @@ import ( "github.com/jesseduffield/lazydocker/pkg/commands" "github.com/jesseduffield/lazydocker/pkg/gui/panels" "github.com/jesseduffield/lazydocker/pkg/gui/presentation" + "github.com/jesseduffield/lazydocker/pkg/tasks" "github.com/jesseduffield/lazydocker/pkg/utils" "github.com/jesseduffield/yaml" ) @@ -89,14 +90,8 @@ func (gui *Gui) getProjectName() string { return projectName } -func (gui *Gui) renderCredits(_project *commands.Project) error { - return gui.T.NewTask(func(stop chan struct{}) { - mainView := gui.Views.Main - mainView.Autoscroll = false - mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel - - _ = gui.RenderStringMain(gui.creditsStr()) - }) +func (gui *Gui) renderCredits(_project *commands.Project) tasks.TaskFunc { + return gui.NewSimpleRenderStringTask(func() string { return gui.creditsStr() }) } func (gui *Gui) creditsStr() string { @@ -116,47 +111,40 @@ func (gui *Gui) creditsStr() string { }, "\n\n") } -func (gui *Gui) renderAllLogs(_project *commands.Project) error { - return gui.T.NewTask(func(stop chan struct{}) { - mainView := gui.Views.Main - mainView.Autoscroll = true - mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel +func (gui *Gui) renderAllLogs(_project *commands.Project) tasks.TaskFunc { + return gui.NewTask(TaskOpts{ + Autoscroll: true, + Wrap: gui.Config.UserConfig.Gui.WrapMainPanel, + Func: func(stop chan struct{}) { + gui.clearMainView() - gui.clearMainView() + cmd := gui.OSCommand.RunCustomCommand( + utils.ApplyTemplate( + gui.Config.UserConfig.CommandTemplates.AllLogs, + gui.DockerCommand.NewCommandObject(commands.CommandObject{}), + ), + ) - cmd := gui.OSCommand.RunCustomCommand( - utils.ApplyTemplate( - gui.Config.UserConfig.CommandTemplates.AllLogs, - gui.DockerCommand.NewCommandObject(commands.CommandObject{}), - ), - ) + cmd.Stdout = gui.Views.Main + cmd.Stderr = gui.Views.Main - cmd.Stdout = mainView - cmd.Stderr = mainView + gui.OSCommand.PrepareForChildren(cmd) + _ = cmd.Start() - gui.OSCommand.PrepareForChildren(cmd) - _ = cmd.Start() + go func() { + <-stop + if err := gui.OSCommand.Kill(cmd); err != nil { + gui.Log.Error(err) + } + }() - go func() { - <-stop - if err := gui.OSCommand.Kill(cmd); err != nil { - gui.Log.Error(err) - } - }() - - _ = cmd.Wait() + _ = cmd.Wait() + }, }) } -func (gui *Gui) renderDockerComposeConfig(_project *commands.Project) error { - return gui.T.NewTask(func(stop chan struct{}) { - mainView := gui.Views.Main - mainView.Autoscroll = false - mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel - - config := gui.DockerCommand.DockerComposeConfig() - _ = gui.RenderStringMain(config) - }) +func (gui *Gui) renderDockerComposeConfig(_project *commands.Project) tasks.TaskFunc { + return gui.NewSimpleRenderStringTask(func() string { return gui.DockerCommand.DockerComposeConfig() }) } func (gui *Gui) handleOpenConfig(g *gocui.Gui, v *gocui.View) error { diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index 8a930940..d7bf5414 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -11,6 +11,7 @@ import ( "github.com/jesseduffield/lazydocker/pkg/gui/panels" "github.com/jesseduffield/lazydocker/pkg/gui/presentation" "github.com/jesseduffield/lazydocker/pkg/gui/types" + "github.com/jesseduffield/lazydocker/pkg/tasks" "github.com/jesseduffield/lazydocker/pkg/utils" "github.com/samber/lo" ) @@ -79,50 +80,51 @@ func (gui *Gui) getServicesPanel() *panels.SideListPanel[*commands.Service] { } } -func (gui *Gui) renderServiceContainerConfig(service *commands.Service) error { +func (gui *Gui) renderServiceContainerConfig(service *commands.Service) tasks.TaskFunc { if service.Container == nil { - return gui.RenderStringMain(gui.Tr.NoContainer) + return gui.NewSimpleRenderStringTask(func() string { return gui.Tr.NoContainer }) } return gui.renderContainerConfig(service.Container) } -func (gui *Gui) renderServiceContainerEnv(service *commands.Service) error { +func (gui *Gui) renderServiceContainerEnv(service *commands.Service) tasks.TaskFunc { if service.Container == nil { - return gui.RenderStringMain(gui.Tr.NoContainer) + return gui.NewSimpleRenderStringTask(func() string { return gui.Tr.NoContainer }) } return gui.renderContainerEnv(service.Container) } -func (gui *Gui) renderServiceStats(service *commands.Service) error { +func (gui *Gui) renderServiceStats(service *commands.Service) tasks.TaskFunc { if service.Container == nil { - return nil + return gui.NewSimpleRenderStringTask(func() string { return gui.Tr.NoContainer }) } return gui.renderContainerStats(service.Container) } -func (gui *Gui) renderServiceTop(service *commands.Service) error { - mainView := gui.Views.Main - mainView.Autoscroll = false - mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel +func (gui *Gui) renderServiceTop(service *commands.Service) tasks.TaskFunc { + return gui.NewTickerTask(TickerTaskOpts{ + Func: func(stop, notifyStopped chan struct{}) { + ctx := stopIntoCtx(stop) + contents, err := service.RenderTop(ctx) + if err != nil { + gui.RenderStringMain(err.Error()) + } - return gui.T.NewTickerTask(time.Second, func(stop chan struct{}) { gui.clearMainView() }, func(stop, notifyStopped chan struct{}) { - contents, err := service.RenderTop() - if err != nil { - gui.reRenderStringMain(err.Error()) - } - - gui.reRenderStringMain(contents) + gui.reRenderStringMain(contents) + }, + Duration: time.Second, + Before: func(stop chan struct{}) { gui.clearMainView() }, + Wrap: gui.Config.UserConfig.Gui.WrapMainPanel, + Autoscroll: false, }) } -func (gui *Gui) renderServiceLogs(service *commands.Service) error { +func (gui *Gui) renderServiceLogs(service *commands.Service) tasks.TaskFunc { if service.Container == nil { - return gui.T.NewTask(func(stop chan struct{}) { - gui.reRenderStringMain(gui.Tr.NoContainerForService) - }) + return gui.NewSimpleRenderStringTask(func() string { return gui.Tr.NoContainerForService }) } return gui.renderContainerLogsToMain(service.Container) diff --git a/pkg/gui/tasks_adapter.go b/pkg/gui/tasks_adapter.go new file mode 100644 index 00000000..18214ce0 --- /dev/null +++ b/pkg/gui/tasks_adapter.go @@ -0,0 +1,100 @@ +package gui + +import ( + "time" + + "github.com/jesseduffield/lazydocker/pkg/tasks" +) + +func (gui *Gui) QueueTask(f func(stop chan struct{})) error { + return gui.taskManager.NewTask(f) +} + +type RenderStringTaskOpts struct { + Autoscroll bool + Wrap bool + GetStrContent func() string +} + +type TaskOpts struct { + Autoscroll bool + Wrap bool + Func func(stop chan struct{}) +} + +type TickerTaskOpts struct { + Duration time.Duration + Before func(stop chan struct{}) + Func func(stop, notifyStopped chan struct{}) + Autoscroll bool + Wrap bool +} + +func (gui *Gui) NewRenderStringTask(opts RenderStringTaskOpts) tasks.TaskFunc { + taskOpts := TaskOpts{ + Autoscroll: opts.Autoscroll, + Wrap: opts.Wrap, + Func: func(stop chan struct{}) { + gui.RenderStringMain(opts.GetStrContent()) + }, + } + + return gui.NewTask(taskOpts) +} + +// assumes it's cheap to obtain the content (otherwise we would pass a function that returns the content) +func (gui *Gui) NewSimpleRenderStringTask(getContent func() string) tasks.TaskFunc { + return gui.NewRenderStringTask(RenderStringTaskOpts{ + GetStrContent: getContent, + Autoscroll: false, + Wrap: gui.Config.UserConfig.Gui.WrapMainPanel, + }) +} + +func (gui *Gui) NewTask(opts TaskOpts) tasks.TaskFunc { + return func(stop chan struct{}) { + mainView := gui.Views.Main + mainView.Autoscroll = opts.Autoscroll + mainView.Wrap = opts.Wrap + + opts.Func(stop) + } +} + +// NewTickerTask is a convenience function for making a new task that repeats some action once per e.g. second +// the before function gets called after the lock is obtained, but before the ticker starts. +// if you handle a message on the stop channel in f() you need to send a message on the notifyStopped channel because returning is not sufficient. Here, unlike in a regular task, simply returning means we're now going to wait till the next tick to run again. +func (gui *Gui) NewTickerTask(opts TickerTaskOpts) tasks.TaskFunc { + notifyStopped := make(chan struct{}, 10) + + task := func(stop chan struct{}) { + if opts.Before != nil { + opts.Before(stop) + } + tickChan := time.NewTicker(opts.Duration) + defer tickChan.Stop() + // calling f first so that we're not waiting for the first tick + opts.Func(stop, notifyStopped) + for { + select { + case <-notifyStopped: + gui.Log.Info("exiting ticker task due to notifyStopped channel") + return + case <-stop: + gui.Log.Info("exiting ticker task due to stopped cahnnel") + return + case <-tickChan.C: + gui.Log.Info("running ticker task again") + opts.Func(stop, notifyStopped) + } + } + } + + taskOpts := TaskOpts{ + Autoscroll: opts.Autoscroll, + Wrap: opts.Wrap, + Func: task, + } + + return gui.NewTask(taskOpts) +} diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index b5bdf55d..44042a0b 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -145,8 +145,8 @@ func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error { return nil } -func (gui *Gui) RenderStringMain(s string) error { - return gui.renderString(gui.g, "main", s) +func (gui *Gui) RenderStringMain(s string) { + _ = gui.renderString(gui.g, "main", s) } // reRenderString sets the main view's content, without changing its origin diff --git a/pkg/gui/volumes_panel.go b/pkg/gui/volumes_panel.go index fa2c2f62..96085949 100644 --- a/pkg/gui/volumes_panel.go +++ b/pkg/gui/volumes_panel.go @@ -10,6 +10,7 @@ import ( "github.com/jesseduffield/lazydocker/pkg/gui/panels" "github.com/jesseduffield/lazydocker/pkg/gui/presentation" "github.com/jesseduffield/lazydocker/pkg/gui/types" + "github.com/jesseduffield/lazydocker/pkg/tasks" "github.com/jesseduffield/lazydocker/pkg/utils" "github.com/samber/lo" ) @@ -52,14 +53,8 @@ func (gui *Gui) getVolumesPanel() *panels.SideListPanel[*commands.Volume] { } } -func (gui *Gui) renderVolumeConfig(volume *commands.Volume) error { - return gui.T.NewTask(func(stop chan struct{}) { - mainView := gui.Views.Main - mainView.Autoscroll = false - mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel - - _ = gui.RenderStringMain(gui.volumeConfigStr(volume)) - }) +func (gui *Gui) renderVolumeConfig(volume *commands.Volume) tasks.TaskFunc { + return gui.NewSimpleRenderStringTask(func() string { return gui.volumeConfigStr(volume) }) } func (gui *Gui) volumeConfigStr(volume *commands.Volume) string { diff --git a/pkg/tasks/tasks.go b/pkg/tasks/tasks.go index dcb8795e..01bd689c 100644 --- a/pkg/tasks/tasks.go +++ b/pkg/tasks/tasks.go @@ -27,6 +27,8 @@ type Task struct { f func(chan struct{}) } +type TaskFunc func(stop chan struct{}) + func NewTaskManager(log *logrus.Entry, translationSet *i18n.TranslationSet) *TaskManager { return &TaskManager{Log: log, Tr: translationSet} }