From d8d6ec7d7a1033178cb7dd7fc95e9fad0cd571a4 Mon Sep 17 00:00:00 2001 From: DevLeonardoK Date: Tue, 14 Jul 2026 21:21:26 -0300 Subject: [PATCH 1/3] 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. --- docs/keybindings/Keybindings_de.md | 1 + docs/keybindings/Keybindings_en.md | 1 + docs/keybindings/Keybindings_es.md | 1 + docs/keybindings/Keybindings_fr.md | 1 + docs/keybindings/Keybindings_nl.md | 1 + docs/keybindings/Keybindings_pl.md | 1 + docs/keybindings/Keybindings_pt.md | 1 + docs/keybindings/Keybindings_tr.md | 1 + docs/keybindings/Keybindings_zh.md | 1 + pkg/gui/container_logs.go | 32 +++++++++++++++++++++++++++++- pkg/gui/containers_panel.go | 2 +- pkg/gui/gui.go | 7 +++++++ pkg/gui/keybindings.go | 7 +++++++ pkg/i18n/english.go | 2 ++ 14 files changed, 57 insertions(+), 2 deletions(-) diff --git a/docs/keybindings/Keybindings_de.md b/docs/keybindings/Keybindings_de.md index dc9dd08e..b3a2642c 100644 --- a/docs/keybindings/Keybindings_de.md +++ b/docs/keybindings/Keybindings_de.md @@ -25,6 +25,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct a: anbinden m: zeige Protokolle E: exec shell + t: cycle how many log lines are shown c: führe vordefinierten benutzerdefinierten Befehl aus b: view bulk commands w: open in browser (first port is http) diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md index ae21c4dc..390f6bcc 100644 --- a/docs/keybindings/Keybindings_en.md +++ b/docs/keybindings/Keybindings_en.md @@ -25,6 +25,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct a: attach m: view logs E: exec shell + t: cycle how many log lines are shown c: run predefined custom command b: view bulk commands w: open in browser (first port is http) diff --git a/docs/keybindings/Keybindings_es.md b/docs/keybindings/Keybindings_es.md index 6b4eb921..41725673 100644 --- a/docs/keybindings/Keybindings_es.md +++ b/docs/keybindings/Keybindings_es.md @@ -25,6 +25,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct a: attach m: ver logs E: ejecutar shell + t: cycle how many log lines are shown c: ejecutar comando personalizado b: ver comandos masivos w: abrir en navegador (first port is http) diff --git a/docs/keybindings/Keybindings_fr.md b/docs/keybindings/Keybindings_fr.md index 109078b2..bb8db287 100644 --- a/docs/keybindings/Keybindings_fr.md +++ b/docs/keybindings/Keybindings_fr.md @@ -25,6 +25,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct a: attacher m: voir les enregistrements E: exécuter le shell + t: cycle how many log lines are shown c: exécuter une commande prédéfinie b: voir les commandes groupées w: ouvrir dans le navigateur (le premier port est http) diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md index 24477006..2c9a0755 100644 --- a/docs/keybindings/Keybindings_nl.md +++ b/docs/keybindings/Keybindings_nl.md @@ -25,6 +25,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct a: verbinden m: bekijk logs E: exec shell + t: cycle how many log lines are shown c: draai een vooraf bedacht aangepaste opdracht b: view bulk commands w: open in browser (first port is http) diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md index 2f4cb93f..bd35a817 100644 --- a/docs/keybindings/Keybindings_pl.md +++ b/docs/keybindings/Keybindings_pl.md @@ -25,6 +25,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct a: przyczep m: pokaż logi E: exec shell + t: cycle how many log lines are shown c: wykonaj predefiniowaną własną komende b: view bulk commands w: open in browser (first port is http) diff --git a/docs/keybindings/Keybindings_pt.md b/docs/keybindings/Keybindings_pt.md index 32a557b5..d021f414 100644 --- a/docs/keybindings/Keybindings_pt.md +++ b/docs/keybindings/Keybindings_pt.md @@ -25,6 +25,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct a: anexar m: ver logs E: executar shell + t: cycle how many log lines are shown c: executar comando personalizado predefinido b: ver comandos em massa w: abrir no navegador (primeira porta é http) diff --git a/docs/keybindings/Keybindings_tr.md b/docs/keybindings/Keybindings_tr.md index 3192a7c2..f17c0f47 100644 --- a/docs/keybindings/Keybindings_tr.md +++ b/docs/keybindings/Keybindings_tr.md @@ -25,6 +25,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct a: bağlan/iliştir m: kayıt defterini görüntüle E: exec shell + t: cycle how many log lines are shown c: önceden tanımlanmış özel komutu çalıştır b: view bulk commands w: open in browser (first port is http) diff --git a/docs/keybindings/Keybindings_zh.md b/docs/keybindings/Keybindings_zh.md index 0c75fbcb..678ee209 100644 --- a/docs/keybindings/Keybindings_zh.md +++ b/docs/keybindings/Keybindings_zh.md @@ -25,6 +25,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct a: attach m: 查看日志 E: 执行shell + t: cycle how many log lines are shown c: 运行预定义的自定义命令 b: 查看批量命令 w: 在浏览器中打开(第一个端口为http) diff --git a/pkg/gui/container_logs.go b/pkg/gui/container_logs.go index bdb81608..ad97918e 100644 --- a/pkg/gui/container_logs.go +++ b/pkg/gui/container_logs.go @@ -11,11 +11,40 @@ import ( "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{}) { @@ -36,6 +65,7 @@ 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") if err := gui.writeContainerLogs(container, ctx, mainView); err != nil { gui.Log.Error(err) @@ -110,7 +140,7 @@ func (gui *Gui) writeContainerLogs(ctr *commands.Container, ctx context.Context, ShowStderr: true, Timestamps: gui.Config.UserConfig.Logs.Timestamps, Since: gui.Config.UserConfig.Logs.Since, - Tail: gui.Config.UserConfig.Logs.Tail, + Tail: gui.State.ContainerLogsTail, Follow: true, }) if err != nil { diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index a638ee29..daa2c6c2 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -68,7 +68,7 @@ func (gui *Gui) getContainersPanel() *panels.SideListPanel[*commands.Container] // where a container restarts but the new logs don't get read. // Note that this might be jarring if we have a lot of logs and the container // restarts a lot, so let's keep an eye on it. - return "containers-" + container.ID + "-" + container.Container.State + return "containers-" + container.ID + "-" + container.Container.State + "-" + gui.State.ContainerLogsTail }, }, ListPanel: panels.ListPanel[*commands.Container]{ diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 6ea2ee6c..76609639 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -82,6 +82,12 @@ type guiState struct { // if true, we show containers with an 'exited' status in the containers panel ShowExitedContainers bool + // the number of lines to pass as `--tail` when reading a container's logs. Empty + // string means we show all of the logs. This starts out as whatever is configured + // in the user's config, and can be cycled through ContainerLogsTailPresets via a + // keybinding. + ContainerLogsTail string + ScreenMode WindowMaximisation // Maintains the state of manual filtering i.e. typing in a substring @@ -136,6 +142,7 @@ func NewGui(log *logrus.Entry, dockerCommand *commands.DockerCommand, oSCommand ViewStack: []string{}, ShowExitedContainers: true, + ContainerLogsTail: config.UserConfig.Logs.Tail, ScreenMode: getScreenMode(config), } diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index df706024..2c7e0b0e 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -241,6 +241,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handleContainersExecShell, Description: gui.Tr.ExecShell, }, + { + ViewName: "containers", + Key: 't', + Modifier: gocui.ModNone, + Handler: gui.cycleContainerLogsTail, + Description: gui.Tr.CycleContainerLogsTail, + }, { ViewName: "containers", Key: 'c', diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index d86b0c84..eedfc39f 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -65,6 +65,7 @@ type TranslationSet struct { NextContext string Attach string ViewLogs string + CycleContainerLogsTail string UpProject string DownProject string ServicesTitle string @@ -196,6 +197,7 @@ func englishSet() TranslationSet { NextContext: "next tab", Attach: "attach", ViewLogs: "view logs", + CycleContainerLogsTail: "cycle how many log lines are shown", UpProject: "up project", DownProject: "down project", RemoveImage: "remove image", From e57958fc3bff87c943a0d8522058a4b7481d8999 Mon Sep 17 00:00:00 2001 From: DevLeonardoK Date: Tue, 14 Jul 2026 21:32:12 -0300 Subject: [PATCH 2/3] 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) } From d5f6eee69f6f66e4b0ed331b93b31097ff917c5b Mon Sep 17 00:00:00 2001 From: DevLeonardoK Date: Tue, 14 Jul 2026 21:45:37 -0300 Subject: [PATCH 3/3] Support cycling the log tail count from the services panel too The services panel's Logs tab renders the underlying container's logs through the same code path, so bind 't' there as well and include the tail value in the services context cache key so the tab re-renders when the value changes. Also switch the tail-limiting writer to SetContent so the clear+rewrite is atomic and the view never renders empty in between. --- docs/keybindings/Keybindings_de.md | 1 + docs/keybindings/Keybindings_en.md | 1 + docs/keybindings/Keybindings_es.md | 1 + docs/keybindings/Keybindings_fr.md | 1 + docs/keybindings/Keybindings_nl.md | 1 + docs/keybindings/Keybindings_pl.md | 1 + docs/keybindings/Keybindings_pt.md | 1 + docs/keybindings/Keybindings_tr.md | 1 + docs/keybindings/Keybindings_zh.md | 1 + pkg/gui/container_logs.go | 9 +++++++-- pkg/gui/keybindings.go | 7 +++++++ pkg/gui/services_panel.go | 2 +- 12 files changed, 24 insertions(+), 3 deletions(-) diff --git a/docs/keybindings/Keybindings_de.md b/docs/keybindings/Keybindings_de.md index b3a2642c..3934cf56 100644 --- a/docs/keybindings/Keybindings_de.md +++ b/docs/keybindings/Keybindings_de.md @@ -46,6 +46,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct S: start a: anbinden m: zeige Protokolle + t: cycle how many log lines are shown U: up project D: down project R: zeige Neustartoptionen diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md index 390f6bcc..b9d7160f 100644 --- a/docs/keybindings/Keybindings_en.md +++ b/docs/keybindings/Keybindings_en.md @@ -46,6 +46,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct S: start a: attach m: view logs + t: cycle how many log lines are shown U: up project D: down project R: view restart options diff --git a/docs/keybindings/Keybindings_es.md b/docs/keybindings/Keybindings_es.md index 41725673..a0a46c51 100644 --- a/docs/keybindings/Keybindings_es.md +++ b/docs/keybindings/Keybindings_es.md @@ -46,6 +46,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct S: iniciar a: attach m: ver logs + t: cycle how many log lines are shown U: levantar proyecto D: dar de baja el proyecto R: ver opciones de reinicio diff --git a/docs/keybindings/Keybindings_fr.md b/docs/keybindings/Keybindings_fr.md index bb8db287..323be0d1 100644 --- a/docs/keybindings/Keybindings_fr.md +++ b/docs/keybindings/Keybindings_fr.md @@ -46,6 +46,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct S: démarrer a: attacher m: voir les enregistrements + t: cycle how many log lines are shown U: up project D: down project R: voir les options de redémarrage diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md index 2c9a0755..1d4902e0 100644 --- a/docs/keybindings/Keybindings_nl.md +++ b/docs/keybindings/Keybindings_nl.md @@ -46,6 +46,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct S: start a: verbinden m: bekijk logs + t: cycle how many log lines are shown U: up project D: down project R: bekijk herstart opties diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md index bd35a817..c08a1113 100644 --- a/docs/keybindings/Keybindings_pl.md +++ b/docs/keybindings/Keybindings_pl.md @@ -46,6 +46,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct S: start a: przyczep m: pokaż logi + t: cycle how many log lines are shown U: up project D: down project R: pokaż opcje restartu diff --git a/docs/keybindings/Keybindings_pt.md b/docs/keybindings/Keybindings_pt.md index d021f414..99183344 100644 --- a/docs/keybindings/Keybindings_pt.md +++ b/docs/keybindings/Keybindings_pt.md @@ -46,6 +46,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct S: iniciar a: anexar m: ver logs + t: cycle how many log lines are shown U: subir projeto D: derrubar projeto R: ver opções de reinício diff --git a/docs/keybindings/Keybindings_tr.md b/docs/keybindings/Keybindings_tr.md index f17c0f47..d97ea769 100644 --- a/docs/keybindings/Keybindings_tr.md +++ b/docs/keybindings/Keybindings_tr.md @@ -46,6 +46,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct S: start a: bağlan/iliştir m: kayıt defterini görüntüle + t: cycle how many log lines are shown U: up project D: down project R: yeniden başlatma seçeneklerini görüntüle diff --git a/docs/keybindings/Keybindings_zh.md b/docs/keybindings/Keybindings_zh.md index 678ee209..3263d839 100644 --- a/docs/keybindings/Keybindings_zh.md +++ b/docs/keybindings/Keybindings_zh.md @@ -46,6 +46,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct S: 启动项目 a: attach m: 查看日志 + t: cycle how many log lines are shown U: 创建并启动容器 D: 停止并移除容器 R: 查看重启选项 diff --git a/pkg/gui/container_logs.go b/pkg/gui/container_logs.go index 1fda78fd..7d00517d 100644 --- a/pkg/gui/container_logs.go +++ b/pkg/gui/container_logs.go @@ -44,6 +44,10 @@ func (gui *Gui) cycleContainerLogsTail(g *gocui.Gui, v *gocui.View) error { gui.State.ContainerLogsTail = containerLogsTailPresets[(currentIdx+1)%len(containerLogsTailPresets)] + if v != nil && v.Name() == "services" { + return gui.Panels.Services.HandleSelect() + } + return gui.Panels.Containers.HandleSelect() } @@ -77,8 +81,9 @@ func (w *tailLimitingWriter) Write(p []byte) (int, error) { w.lines = w.lines[excess:] } - w.view.Clear() - fmt.Fprint(w.view, strings.Join(w.lines, "\n")+"\n") + // SetContent clears and rewrites atomically, so the view never renders empty + // in between + w.view.SetContent(strings.Join(w.lines, "\n") + "\n") return len(p), nil } diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 2c7e0b0e..32f30929 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -325,6 +325,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handleServiceRenderLogsToMain, Description: gui.Tr.ViewLogs, }, + { + ViewName: "services", + Key: 't', + Modifier: gocui.ModNone, + Handler: gui.cycleContainerLogsTail, + Description: gui.Tr.CycleContainerLogsTail, + }, { ViewName: "services", Key: 'U', diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index edc894e9..4b7fac8b 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -53,7 +53,7 @@ func (gui *Gui) getServicesPanel() *panels.SideListPanel[*commands.Service] { if service.Container == nil { return "services-" + service.ID } - return "services-" + service.ID + "-" + service.Container.ID + "-" + service.Container.Container.State + return "services-" + service.ID + "-" + service.Container.ID + "-" + service.Container.Container.State + "-" + gui.State.ContainerLogsTail }, }, ListPanel: panels.ListPanel[*commands.Service]{