diff --git a/docs/keybindings/Keybindings_de.md b/docs/keybindings/Keybindings_de.md index dc9dd08e..3934cf56 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) @@ -45,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 ae21c4dc..b9d7160f 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) @@ -45,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 6b4eb921..a0a46c51 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) @@ -45,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 109078b2..323be0d1 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) @@ -45,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 24477006..1d4902e0 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) @@ -45,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 2f4cb93f..c08a1113 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) @@ -45,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 32a557b5..99183344 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) @@ -45,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 3192a7c2..d97ea769 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) @@ -45,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 0c75fbcb..3263d839 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) @@ -45,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 bdb81608..7d00517d 100644 --- a/pkg/gui/container_logs.go +++ b/pkg/gui/container_logs.go @@ -6,16 +6,88 @@ import ( "io" "os" "os/signal" + "strconv" + "strings" "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)] + + if v != nil && v.Name() == "services" { + return gui.Panels.Services.HandleSelect() + } + + 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:] + } + + // 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 +} + func (gui *Gui) renderContainerLogsToMain(container *commands.Container) tasks.TaskFunc { return gui.NewTickerTask(TickerTaskOpts{ Func: func(ctx context.Context, notifyStopped chan struct{}) { @@ -36,8 +108,17 @@ func (gui *Gui) renderContainerLogsToMainAux(container *commands.Container, ctx }() mainView := gui.Views.Main + 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) } @@ -110,7 +191,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..32f30929 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', @@ -318,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]{ 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",