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 dc9dd08e8..b3a2642c9 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 ae21c4dcd..390f6bcc8 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 6b4eb9210..417256735 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 109078b23..bb8db287e 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 24477006a..2c9a0755e 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 2f4cb93f9..bd35a817e 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 32a557b53..d021f4142 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 3192a7c20..f17c0f47f 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 0c75fbcb9..678ee2095 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 bdb816082..ad97918e6 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 a638ee290..daa2c6c27 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 6ea2ee6c6..766096399 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 df7060241..2c7e0b0e5 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 d86b0c84f..eedfc39f8 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 ad97918e6..1fda78fd1 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") + + 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, mainView); err != nil { + 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 b3a2642c9..3934cf56a 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 390f6bcc8..b9d7160f3 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 417256735..a0a46c518 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 bb8db287e..323be0d1d 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 2c9a0755e..1d4902e03 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 bd35a817e..c08a1113a 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 d021f4142..99183344b 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 f17c0f47f..d97ea7696 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 678ee2095..3263d8398 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 1fda78fd1..7d00517d7 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 2c7e0b0e5..32f30929c 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 edc894e9e..4b7fac8bd 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]{