Files
MujicaUI/log_viewer.go
T

131 lines
3.7 KiB
Go

package mujica
import (
"fmt"
"log/slog"
"time"
"github.com/egoist/mygo/ui"
"mujicaui/theme"
)
// LogLevel is a log line's severity.
type LogLevel int
const (
LogDebug LogLevel = iota
LogInfo
LogWarn
LogError
)
// LogLine is one entry of a LogViewer.
type LogLine struct {
Time time.Time
Level LogLevel
Text string
}
// LogViewerState is a LogViewer's filters, highlight and place.
type LogViewerState struct {
// Hidden levels are filtered out.
Hidden [4]bool
// Query is highlighted in the shown lines.
Query string
// Follow keeps the newest line in view; it starts on.
Follow bool
rows ui.ListState
shown []int
scanned int
hidden [4]bool
started bool
}
// LogViewerOptions configure a LogViewer.
type LogViewerOptions struct {
Disabled bool
}
// LogViewer shows filtered, highlighted log lines, virtualized.
func LogViewer(c *ui.Context, lines []LogLine, s *LogViewerState, opts LogViewerOptions) ChoiceResult {
if s == nil {
panic("mujica: LogViewer needs a state")
}
if !s.started {
s.started, s.Follow = true, true
}
var res ChoiceResult
res.Element = codePanel(c).Disabled(opts.Disabled)
res.Element.Children(func() {
codeHeader(c, func() {
for lv := LogDebug; lv <= LogError; lv++ {
on := !s.Hidden[lv]
if Toggle(c, &on, logViewerName(c, lv), ToggleOptions{Disabled: opts.Disabled}).Changed() {
s.Hidden[lv] = !on
res.changed = true
slog.Debug("mujica: log_viewer level", "level", lv, "shown", on)
}
}
SearchInput(c, &s.Query, SearchInputOptions{Disabled: opts.Disabled, Placeholder: msg(c, "log_viewer.highlight")}).Element.Grow(1).MinWidth(0)
was := s.Follow
if Toggle(c, &s.Follow, msg(c, "log_viewer.follow"), ToggleOptions{Disabled: opts.Disabled}).Changed() {
res.changed = true
slog.Debug("mujica: log_viewer follow", "on", s.Follow)
if s.Follow && !was {
s.rows.ScrollToEnd()
}
}
})
logViewerFilter(lines, s)
if len(s.shown) == 0 {
codeCaption(c, msg(c, "log_viewer.empty")).Padding(Density(c).Unit() * 4)
return
}
s.rows.FollowEnd = s.Follow
s.rows.Key = func(i int) any { return s.shown[i] }
s.rows.Label = func(i int) string { return lines[s.shown[i]].Text }
d := Density(c)
ui.List(c, &s.rows, len(s.shown), func(i int) {
logViewerRow(c, lines[s.shown[i]], s.Query)
}).Grow(1).MinHeight(d.ControlHeight()*3).Padding(d.Unit(), 0)
})
return res
}
// logViewerFilter extends or redoes the shown lines.
func logViewerFilter(lines []LogLine, s *LogViewerState) {
if s.hidden != s.Hidden || s.scanned > len(lines) {
s.hidden, s.scanned, s.shown = s.Hidden, 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 !s.Hidden[lv] {
s.shown = append(s.shown, i)
}
}
s.scanned = len(lines)
}
func logViewerName(c *ui.Context, lv LogLevel) string {
return msg(c, [...]string{"log_viewer.debug", "log_viewer.info", "log_viewer.warn", "log_viewer.error"}[lv])
}
func logViewerRow(c *ui.Context, l LogLine, query string) {
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()).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, codeMatchSpans(c, l.Text, query)...).Font(theme.MonoFont).FontSize(FontSize(c, codeSize)).
SingleLine().Grow(1).MinWidth(0)
})
}