Files
MujicaUI/log_stream.go
T
2026-10-05 14:55:00 -04:00

123 lines
3.7 KiB
Go

package mujica
import (
"fmt"
"log/slog"
"github.com/egoist/mygo/ui"
"mujicaui/theme"
)
// LogStreamState is a LogStream's pause, level filter and place.
type LogStreamState struct {
Paused bool
// MinLevel hides lines below it.
MinLevel LogLevel
// frozen is the line count at pause; lines after it wait.
frozen int
shown []int
scanned int
min LogLevel
rows ui.ListState
}
// LogStreamOptions configure a LogStream.
type LogStreamOptions struct {
// Height of the line well; zero means 320.
Height float32
Disabled bool
}
// LogStream shows live ANSI log lines that follow the end, pause and filter by level, virtualized.
func LogStream(c *ui.Context, lines []LogLine, s *LogStreamState, opts LogStreamOptions) ChoiceResult {
if s == nil {
panic("mujica: LogStream needs a state")
}
if opts.Height < 0 || s.MinLevel < LogDebug || s.MinLevel > LogError {
panic("mujica: LogStream needs Height >= 0 and a known MinLevel")
}
h := opts.Height
if h == 0 {
h = 320
}
var r ChoiceResult
r.Element = codePanel(c).Disabled(opts.Disabled).Height(h)
locked := r.Element.IsDisabled()
r.Element.Children(func() {
codeHeader(c, func() {
was := s.Paused
label := msg(c, "log_stream.pause")
if s.Paused {
label = msg(c, "log_stream.resume")
}
if Toggle(c, &s.Paused, label, ToggleOptions{Disabled: locked}).Changed() && !locked {
r.changed = true
} else {
s.Paused = was
}
levels := make([]Segment, 4)
for lv := LogDebug; lv <= LogError; lv++ {
levels[lv] = Segment{Label: logViewerName(c, lv)}
}
pick := int(s.MinLevel)
SegmentedControl(c, &pick, levels, SegmentedControlOptions{Label: msg(c, "log_stream.level"), Disabled: locked})
if LogLevel(pick) != s.MinLevel && !locked {
s.MinLevel, r.changed = LogLevel(pick), true
}
ui.Box(c).Grow(1)
if s.Paused && len(lines) > s.frozen {
codeCaption(c, fmt.Sprintf(msg(c, "log_stream.waiting"), len(lines)-s.frozen)).Role(ui.RoleStatus)
}
})
if !s.Paused {
s.frozen = len(lines)
}
logStreamFilter(lines[:min(s.frozen, len(lines))], s)
if len(s.shown) == 0 {
codeCaption(c, msg(c, "log_stream.empty")).Padding(Density(c).Unit() * 4)
return
}
s.rows.FollowEnd = !s.Paused
s.rows.Key = func(i int) any { return s.shown[i] }
d := Density(c)
ui.List(c, &s.rows, len(s.shown), func(i int) { logStreamRow(c, lines[s.shown[i]]) }).Grow(1).Padding(d.Unit(), 0)
})
if r.changed {
slog.Debug("mujica: log_stream change", "paused", s.Paused, "min_level", s.MinLevel)
}
return r
}
// logStreamFilter extends the shown lines, or redoes them after a filter change.
func logStreamFilter(lines []LogLine, s *LogStreamState) {
if s.min != s.MinLevel || s.scanned > len(lines) {
s.min, s.scanned, s.shown = s.MinLevel, 0, s.shown[:0]
}
for i := s.scanned; i < len(lines); i++ {
lv := lines[i].Level
if lv < LogDebug || lv > LogError {
panic("mujica: LogLine level out of range")
}
if lv >= s.MinLevel {
s.shown = append(s.shown, i)
}
}
s.scanned = len(lines)
}
// logStreamRow shows one line: time, level word and ANSI text.
func logStreamRow(c *ui.Context, l LogLine) {
k := Tokens(c)
d := Density(c)
col := [...]ui.Color{k.TextMuted, k.Info, k.Warning, k.Danger}[l.Level]
ui.Row(c).MinHeight(d.ControlHeight() - 8).Padding(0, d.Unit()*3).Gap(d.Unit() * 3).AlignItems(ui.Center).Children(func() {
if !l.Time.IsZero() {
codeMono(c, l.Time.Format("15:04:05.000")).TextColor(k.TextMuted).Shrink(0)
}
ui.Text(c, fmt.Sprintf("%-5s", logViewerName(c, l.Level))).Font(theme.MonoFont).FontSize(FontSize(c, codeSize)).
FontWeight(700).TextColor(col).SingleLine().Shrink(0)
ui.RichText(c, codeAnsiSpans(c, l.Text)...).Font(theme.MonoFont).FontSize(FontSize(c, codeSize)).Grow(1).MinWidth(0)
})
}