From e57958fc3bff87c943a0d8522058a4b7481d8999 Mon Sep 17 00:00:00 2001 From: DevLeonardoK Date: Tue, 14 Jul 2026 21:32:12 -0300 Subject: [PATCH] Keep a real sliding window for the tail line count Previously the selected --tail value only trimmed the initial log snapshot; since logs are followed live afterwards, the view kept growing unbounded past the chosen count. Now the main panel writer re-truncates to the last N lines on every new line, so e.g. tail: 50 always shows only the 50 most recent lines. Behavior when following logs to stdout (the 'm' keybinding) is unchanged, matching plain `docker logs --tail N -f` there. --- pkg/gui/container_logs.go | 50 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/pkg/gui/container_logs.go b/pkg/gui/container_logs.go index ad97918e..1fda78fd 100644 --- a/pkg/gui/container_logs.go +++ b/pkg/gui/container_logs.go @@ -6,6 +6,8 @@ import ( "io" "os" "os/signal" + "strconv" + "strings" "time" "github.com/docker/docker/api/types/container" @@ -45,6 +47,42 @@ func (gui *Gui) cycleContainerLogsTail(g *gocui.Gui, v *gocui.View) error { return gui.Panels.Containers.HandleSelect() } +// tailLimitingWriter wraps a writer and re-renders it on every line so that it only +// ever displays the last maxLines lines, keeping a true sliding window rather than +// just applying `--tail` to the initial snapshot and then growing forever as new +// lines are followed in. +type tailLimitingWriter struct { + view *gocui.View + maxLines int + lines []string + partial string +} + +func newTailLimitingWriter(view *gocui.View, maxLines int) *tailLimitingWriter { + return &tailLimitingWriter{view: view, maxLines: maxLines} +} + +func (w *tailLimitingWriter) Write(p []byte) (int, error) { + w.partial += string(p) + split := strings.Split(w.partial, "\n") + w.partial = split[len(split)-1] + newLines := split[:len(split)-1] + + if len(newLines) == 0 { + return len(p), nil + } + + w.lines = append(w.lines, newLines...) + if excess := len(w.lines) - w.maxLines; excess > 0 { + w.lines = w.lines[excess:] + } + + w.view.Clear() + fmt.Fprint(w.view, strings.Join(w.lines, "\n")+"\n") + + return len(p), nil +} + func (gui *Gui) renderContainerLogsToMain(container *commands.Container) tasks.TaskFunc { return gui.NewTickerTask(TickerTaskOpts{ Func: func(ctx context.Context, notifyStopped chan struct{}) { @@ -65,9 +103,17 @@ func (gui *Gui) renderContainerLogsToMainAux(container *commands.Container, ctx }() mainView := gui.Views.Main - mainView.Subtitle = fmt.Sprintf("tail: %s (%s to cycle)", containerLogsTailLabel(gui.State.ContainerLogsTail), "t") + tail := gui.State.ContainerLogsTail + mainView.Subtitle = fmt.Sprintf("tail: %s (%s to cycle)", containerLogsTailLabel(tail), "t") - if err := gui.writeContainerLogs(container, ctx, mainView); err != nil { + var writer io.Writer = mainView + if maxLines, err := strconv.Atoi(tail); err == nil && maxLines > 0 { + // keep re-truncating to the chosen number of lines as new ones stream in, + // instead of just limiting the initial snapshot and growing unbounded after that + writer = newTailLimitingWriter(mainView, maxLines) + } + + if err := gui.writeContainerLogs(container, ctx, writer); err != nil { gui.Log.Error(err) }