Reticulum-Go/pkg/debug/color_handler.go

99 lines
2.5 KiB
Go

// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024-2026 Quad4.io
package debug
import (
"bytes"
"context"
"io"
"log/slog"
"os"
"sync"
"quad4/reticulum-go/pkg/term"
)
// colorState is shared across WithAttrs/WithGroup clones.
type colorState struct {
mu sync.Mutex
buf bytes.Buffer
w io.Writer
file *os.File
}
// colorHandler wraps a text handler and injects ANSI around the level token.
// slog.TextHandler quotes strings that contain ESC, so ReplaceAttr cannot be used.
type colorHandler struct {
inner slog.Handler
state *colorState
}
func newColorHandler(w io.Writer, opts *slog.HandlerOptions) *colorHandler {
st := &colorState{w: w, file: os.Stderr}
return &colorHandler{
inner: slog.NewTextHandler(&st.buf, opts),
state: st,
}
}
func (h *colorHandler) Enabled(ctx context.Context, level slog.Level) bool {
return h.inner.Enabled(ctx, level)
}
func (h *colorHandler) Handle(ctx context.Context, r slog.Record) error {
h.state.mu.Lock()
defer h.state.mu.Unlock()
h.state.buf.Reset()
if err := h.inner.Handle(ctx, r); err != nil {
return err
}
line := colorizeLevelBytes(h.state.buf.Bytes(), r.Level, h.state.file)
_, err := h.state.w.Write(line)
return err
}
func (h *colorHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &colorHandler{inner: h.inner.WithAttrs(attrs), state: h.state}
}
func (h *colorHandler) WithGroup(name string) slog.Handler {
return &colorHandler{inner: h.inner.WithGroup(name), state: h.state}
}
func colorizeLevelBytes(line []byte, level slog.Level, f *os.File) []byte {
plain := level.String()
token := append([]byte("level="), plain...)
idx := bytes.Index(line, token)
if idx < 0 {
return line
}
colored := colorLevelLabel(f, level, plain)
out := make([]byte, 0, len(line)+len(colored)-len(plain))
out = append(out, line[:idx+len("level=")]...)
out = append(out, colored...)
out = append(out, line[idx+len(token):]...)
return out
}
func colorLevelLabel(f *os.File, level slog.Level, label string) string {
switch {
case level >= slog.LevelError:
return term.Red(f, label)
case level >= slog.LevelWarn:
return term.Yellow(f, label)
case level >= slog.LevelInfo:
return term.Cyan(f, label)
default:
return term.Bold(f, label)
}
}
// useColorLogs reports whether text logs should use ANSI on stderr only.
// Disabled for JSON, file-only, or dual stderr+file writers so log files stay plain.
func useColorLogs() bool {
if jsonFormat || omitStderr || extraWriter != nil {
return false
}
return term.ColorEnabled(os.Stderr)
}