From 14e96fcbf81e1db03c8b1ace62afa52c3fdc5865 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 24 Sep 2026 10:40:52 -0400 Subject: [PATCH] fix(ui): render TUI log lines on their own rows, with colors and a small header (#5324) - log lines shown under the TUI were glued onto the last UI row, and the last row could get erased (and overwritten by the report) when the TUI exits. Fixed in `bubbly` v0.2.2 - log colors were lost because the logger latched its TTY check on the first entry, which went to the TUI footer buffer. `go-logger` v0.2.0 checks per entry and supports `TerminalWriter`, which the footer writer now implements (`NO_COLOR` is still honored) - log lines under the TUI get a dim `Logs (non-fatal, see -v for more):` title with a branch pointing at the first entry, so it's clear what the trailing lines are Signed-off-by: Alex Goodman --- cmd/syft/internal/ui/ui.go | 46 +++++++++++++++++++++++++++++++++++++- go.mod | 4 ++-- go.sum | 8 +++---- 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/cmd/syft/internal/ui/ui.go b/cmd/syft/internal/ui/ui.go index 193768c51..4673a2917 100644 --- a/cmd/syft/internal/ui/ui.go +++ b/cmd/syft/internal/ui/ui.go @@ -1,6 +1,7 @@ package ui import ( + "bytes" "fmt" "io" "os" @@ -8,6 +9,7 @@ import ( "time" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" "github.com/wagoodman/go-partybus" "github.com/anchore/bubbly" @@ -52,7 +54,7 @@ func New(out io.Writer, quiet bool, handlers ...bubbly.EventHandler) *UI { func (m *UI) Setup(subscription partybus.Unsubscribable) error { // we still want to collect log messages, however, we also the logger shouldn't write to the screen directly if logWrapper, ok := log.Get().(logger.Controller); ok { - logWrapper.SetOutput(m.frame.(*frame.Frame).Footer()) + logWrapper.SetOutput(&logFooter{w: m.frame.(*frame.Frame).Footer()}) } m.subscription = subscription @@ -187,6 +189,48 @@ func (m UI) View() string { return m.frame.View() } +var ( + logTitleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // dark grey + // the first log line gets a branch pointing at it from the title, the rest are indented to match + logBranch = logTitleStyle.Render("└─▶") + " " + logIndent = " " +) + +// logFooter decorates log entries drawn below the TUI with a one-time title (written when the first entry arrives) +// and a branch from the title to the first log line. Writes are serialized by the logger. +type logFooter struct { + w io.Writer + started bool +} + +func (l *logFooter) Write(p []byte) (int, error) { + var buf bytes.Buffer + first := !l.started + if first { + buf.WriteString(logTitleStyle.Render("Logs (non-fatal, see -v for more):") + "\n") + l.started = true + } + for i, line := range bytes.SplitAfter(p, []byte("\n")) { + if len(line) == 0 { + continue + } + if first && i == 0 { + buf.WriteString(logBranch) + } else { + buf.WriteString(logIndent) + } + buf.Write(line) + } + if _, err := l.w.Write(buf.Bytes()); err != nil { + return 0, err + } + return len(p), nil +} + +// IsTerminal marks the footer as terminal-bound (it is drawn by the TUI) so the logger keeps colors. NO_COLOR is +// still honored by the formatter. +func (*logFooter) IsTerminal() bool { return true } + func runWithTimeout(timeout time.Duration, fn func() error) (err error) { c := make(chan struct{}, 1) go func() { diff --git a/go.mod b/go.mod index 8c49b84ff..327deee1d 100644 --- a/go.mod +++ b/go.mod @@ -11,12 +11,12 @@ require ( github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/acobaugh/osrelease v0.1.0 github.com/adrg/xdg v0.5.3 - github.com/anchore/bubbly v0.2.1 + github.com/anchore/bubbly v0.2.2 github.com/anchore/clio v0.1.1 github.com/anchore/fangs v0.1.1 github.com/anchore/go-collections v0.1.1 github.com/anchore/go-homedir v0.1.1 - github.com/anchore/go-logger v0.1.1 + github.com/anchore/go-logger v0.2.0 github.com/anchore/go-macholibre v0.1.1 github.com/anchore/go-rpmdb v0.2.0 github.com/anchore/go-sync v0.1.2 diff --git a/go.sum b/go.sum index 99c006c5d..6e9b0a686 100644 --- a/go.sum +++ b/go.sum @@ -120,8 +120,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/anchore/bubbly v0.2.1 h1:b3ZNBefY8ks5DI4SQ7BXSbBIsN6eoaTD6s1su7NuCBo= -github.com/anchore/bubbly v0.2.1/go.mod h1:o6a9aH3/fCn+mb5g1T1tBbQZ4hGR0yO5/uD1d2Vtago= +github.com/anchore/bubbly v0.2.2 h1:53JHRlpaNZjMcUYdz7n1U7+kOhy0zsJv9gpGEXvTcz8= +github.com/anchore/bubbly v0.2.2/go.mod h1:M4twoFkczlfuNdU8QV7oQjVJwPbxdUWRMfKydcLba0U= github.com/anchore/clio v0.1.1 h1:9bkYnaFbpT0J3G0qSHyQqa/lpK5zV0C9Gtgh82H/jj0= github.com/anchore/clio v0.1.1/go.mod h1:CFPIopJlakJbBpKtAL/EPpj4W9hgBO7PfFc8N2FAtlE= github.com/anchore/fangs v0.1.1 h1:NBTHocmWM2/0gNhebpoE/wEB0Z0nxh2lESww2o/Uf5M= @@ -130,8 +130,8 @@ github.com/anchore/go-collections v0.1.1 h1:kGlmSehs+ctTBZr+98F2+taHeolHy+XDx1Nc github.com/anchore/go-collections v0.1.1/go.mod h1:zgDbPSNDpfU47tVo7aW+2suG7+vXB1Sq1qtDhRJknQ0= github.com/anchore/go-homedir v0.1.1 h1:KpaO5BPrVT7cVMcEZ9KfFfEi0iuo3DtagPGl0C744X0= github.com/anchore/go-homedir v0.1.1/go.mod h1:9B0DGhbmMAJVGGEbrlb+PkORM5eDFCEOtZ3xQ22qaLA= -github.com/anchore/go-logger v0.1.1 h1:HPBNmQVBdYliwaFVUL7oJ3UvSlaoXAMj/ZagAIYaCpA= -github.com/anchore/go-logger v0.1.1/go.mod h1:ekWuh5BkZVwyXnyEd4fwL29N4pNLssgRsqh2+kFN/b8= +github.com/anchore/go-logger v0.2.0 h1:V8Gazz+F8xwWEWz+whqt8/5163IkO4XrAie+1qbK10k= +github.com/anchore/go-logger v0.2.0/go.mod h1:omqLWBwT/B5/aTwIH6pl5WBylBzeH3BGHUTS6OFeFvY= github.com/anchore/go-lzo v0.1.1 h1:IwL/fvkdtlIrYIXck6WxZ3nb8WjjHziYYmGxlooyOnM= github.com/anchore/go-lzo v0.1.1/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk= github.com/anchore/go-macholibre v0.1.1 h1:sTBHi3S1WVIOCamc6NyT8Sn9mBmD30DCTfjfVm4k6/M=