lazydocker/pkg/gui/container_logs.go
DevLeonardoK d8d6ec7d7a Add keybinding to cycle log tail line count in Logs tab
Lets you press 't' while focused on the containers panel to cycle
through common --tail presets (all, 50, 100, 200, 500) for the
selected container's Logs tab, instead of only being able to set a
fixed value via the logs.tail config option. The current value is
shown in the Logs tab subtitle.
2026-07-14 21:21:26 -03:00

182 lines
4.6 KiB
Go

package gui
import (
"context"
"fmt"
"io"
"os"
"os/signal"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stdcopy"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/tasks"
"github.com/jesseduffield/lazydocker/pkg/utils"
)
// containerLogsTailPresets are the values we cycle through when the user asks to
// change how many lines of logs are displayed. An empty string means 'show all logs'.
var containerLogsTailPresets = []string{"", "50", "100", "200", "500"}
func containerLogsTailLabel(tail string) string {
if tail == "" {
return "all"
}
return tail
}
// cycleContainerLogsTail moves to the next tail preset (wrapping back to the start)
// and forces the logs tab to re-render with the new value.
func (gui *Gui) cycleContainerLogsTail(g *gocui.Gui, v *gocui.View) error {
currentIdx := 0
for i, preset := range containerLogsTailPresets {
if preset == gui.State.ContainerLogsTail {
currentIdx = i
break
}
}
gui.State.ContainerLogsTail = containerLogsTailPresets[(currentIdx+1)%len(containerLogsTailPresets)]
return gui.Panels.Containers.HandleSelect()
}
func (gui *Gui) renderContainerLogsToMain(container *commands.Container) tasks.TaskFunc {
return gui.NewTickerTask(TickerTaskOpts{
Func: func(ctx context.Context, notifyStopped chan struct{}) {
gui.renderContainerLogsToMainAux(container, ctx, notifyStopped)
},
Duration: time.Millisecond * 200,
// TODO: see why this isn't working (when switching from Top tab to Logs tab in the services panel, the tops tab's content isn't removed)
Before: func(ctx context.Context) { gui.clearMainView() },
Wrap: gui.Config.UserConfig.Gui.WrapMainPanel,
Autoscroll: true,
})
}
func (gui *Gui) renderContainerLogsToMainAux(container *commands.Container, ctx context.Context, notifyStopped chan struct{}) {
gui.clearMainView()
defer func() {
notifyStopped <- struct{}{}
}()
mainView := gui.Views.Main
mainView.Subtitle = fmt.Sprintf("tail: %s (%s to cycle)", containerLogsTailLabel(gui.State.ContainerLogsTail), "t")
if err := gui.writeContainerLogs(container, ctx, mainView); err != nil {
gui.Log.Error(err)
}
// if we are here because the task has been stopped, we should return
// if we are here then the container must have exited, meaning we should wait until it's back again before
ticker := time.NewTicker(time.Millisecond * 100)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
result, err := container.Inspect()
if err != nil {
// if we get an error, then the container has probably been removed so we'll get out of here
gui.Log.Error(err)
return
}
if result.State.Running {
return
}
}
}
}
func (gui *Gui) renderLogsToStdout(container *commands.Container) {
stop := make(chan os.Signal, 1)
defer signal.Stop(stop)
ctx, cancel := context.WithCancel(context.Background())
go func() {
signal.Notify(stop, os.Interrupt)
<-stop
cancel()
}()
if err := gui.g.Suspend(); err != nil {
gui.Log.Error(err)
return
}
defer func() {
if err := gui.g.Resume(); err != nil {
gui.Log.Error(err)
}
}()
if err := gui.writeContainerLogs(container, ctx, os.Stdout); err != nil {
gui.Log.Error(err)
return
}
gui.promptToReturn()
}
func (gui *Gui) promptToReturn() {
if !gui.Config.UserConfig.Gui.ReturnImmediately {
fmt.Fprintf(os.Stdout, "\n\n%s", utils.ColoredString(gui.Tr.PressEnterToReturn, color.FgGreen))
// wait for enter press
if _, err := fmt.Scanln(); err != nil {
gui.Log.Error(err)
}
}
}
func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context, writer io.Writer) error {
readCloser, err := gui.DockerCommand.Client.ContainerLogs(ctx, ctr.ID, container.LogsOptions{
ShowStdout: true,
ShowStderr: true,
Timestamps: gui.Config.UserConfig.Logs.Timestamps,
Since: gui.Config.UserConfig.Logs.Since,
Tail: gui.State.ContainerLogsTail,
Follow: true,
})
if err != nil {
gui.Log.Error(err)
return err
}
defer readCloser.Close()
if !ctr.DetailsLoaded() {
// loop until the details load or context is cancelled, using timer
ticker := time.NewTicker(time.Millisecond * 100)
defer ticker.Stop()
outer:
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
if ctr.DetailsLoaded() {
break outer
}
}
}
}
if ctr.Details.Config.Tty {
_, err = io.Copy(writer, readCloser)
if err != nil {
return err
}
} else {
_, err = stdcopy.StdCopy(writer, writer, readCloser)
if err != nil {
return err
}
}
return nil
}