vendor/github.com/olekukonko/ll/lh/colorized.go
package lh
import (
"fmt"
"io"
"os"
"sort"
"strings"
"sync"
"time"
"github.com/olekukonko/ll/lx"
)
// Palette defines ANSI color codes for various log components.
// It specifies colors for headers, goroutines, functions, paths, stack traces, and log levels,
// used by ColorizedHandler to format log output with color.
type Palette struct {
Header string // Color for stack trace header and dump separators
Goroutine string // Color for goroutine lines in stack traces
Func string // Color for function names in stack traces
Path string // Color for file paths in stack traces
FileLine string // Color for file line numbers (not used in provided code)
Reset string // Reset code to clear color formatting
Pos string // Color for position in hex dumps
Hex string // Color for hex values in dumps
Ascii string // Color for ASCII values in dumps
Debug string // Color for Debug level messages
Info string // Color for Info level messages
Warn string // Color for Warn level messages
Error string // Color for Error level messages
Title string // Color for dump titles (BEGIN/END separators)
}
// darkPalette defines colors optimized for dark terminal backgrounds.
// It uses bright, contrasting colors for readability on dark backgrounds.
var darkPalette = Palette{
Header: "\033[1;31m", // Bold red for headers
Goroutine: "\033[1;36m", // Bold cyan for goroutines
Func: "\033[97m", // Bright white for functions
Path: "\033[38;5;245m", // Light gray for paths
FileLine: "\033[38;5;111m", // Muted light blue (unused)
Reset: "\033[0m", // Reset color formatting
Title: "\033[38;5;245m", // Light gray for dump titles
Pos: "\033[38;5;117m", // Light blue for dump positions
Hex: "\033[38;5;156m", // Light green for hex values
Ascii: "\033[38;5;224m", // Light pink for ASCII values
Debug: "\033[36m", // Cyan for Debug level
Info: "\033[32m", // Green for Info level
Warn: "\033[33m", // Yellow for Warn level
Error: "\033[31m", // Red for Error level
}
// lightPalette defines colors optimized for light terminal backgrounds.
// It uses darker colors for better contrast on light backgrounds.
var lightPalette = Palette{
Header: "\033[1;31m", // Same red for headers
Goroutine: "\033[34m", // Blue (darker for light bg)
Func: "\033[30m", // Black text for functions
Path: "\033[90m", // Dark gray for paths
FileLine: "\033[94m", // Blue for file lines (unused)
Reset: "\033[0m", // Reset color formatting
Title: "\033[38;5;245m", // Light gray for dump titles
Pos: "\033[38;5;117m", // Light blue for dump positions
Hex: "\033[38;5;156m", // Light green for hex values
Ascii: "\033[38;5;224m", // Light pink for ASCII values
Debug: "\033[36m", // Cyan for Debug level
Info: "\033[32m", // Green for Info level
Warn: "\033[33m", // Yellow for Warn level
Error: "\033[31m", // Red for Error level
}
// ColorizedHandler is a handler that outputs log entries with ANSI color codes.
// It formats log entries with colored namespace, level, message, fields, and stack traces,
// writing the result to the provided writer.
// Thread-safe if the underlying writer is thread-safe.
type ColorizedHandler struct {
w io.Writer // Destination for colored log output
palette Palette // Color scheme for formatting
showTime bool // Whether to display timestamps
timeFormat string // Format for timestamps (defaults to time.RFC3339)
mu sync.Mutex
}
// ColorOption defines a configuration function for ColorizedHandler.
// It allows customization of the handler, such as setting the color palette.
type ColorOption func(*ColorizedHandler)
// WithColorPallet sets the color palette for the ColorizedHandler.
// It allows specifying a custom Palette for dark or light terminal backgrounds.
// Example:
//
// handler := NewColorizedHandler(os.Stdout, WithColorPallet(lightPalette))
func WithColorPallet(pallet Palette) ColorOption {
return func(c *ColorizedHandler) {
c.palette = pallet
}
}
// NewColorizedHandler creates a new ColorizedHandler writing to the specified writer.
// It initializes the handler with a detected or specified color palette and applies
// optional configuration functions.
// Example:
//
// handler := NewColorizedHandler(os.Stdout)
// logger := ll.New("app").Enable().Handler(handler)
// logger.Info("Test") // Output: [app] <colored INFO>: Test
func NewColorizedHandler(w io.Writer, opts ...ColorOption) *ColorizedHandler {
// Initialize with writer
c := &ColorizedHandler{w: w,
showTime: false,
timeFormat: time.RFC3339,
}
// Apply configuration options
for _, opt := range opts {
opt(c)
}
// Detect palette if not set
c.palette = c.detectPalette()
return c
}
// Handle processes a log entry and writes it with ANSI color codes.
// It delegates to specialized methods based on the entry's class (Dump, Raw, or regular).
// Returns an error if writing to the underlying writer fails.
// Thread-safe if the writer is thread-safe.
// Example:
//
// handler.Handle(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes colored output
func (h *ColorizedHandler) Handle(e *lx.Entry) error {
h.mu.Lock()
defer h.mu.Unlock()
switch e.Class {
case lx.ClassDump:
// Handle hex dump entries
return h.handleDumpOutput(e)
case lx.ClassRaw:
// Write raw entries directly
_, err := h.w.Write([]byte(e.Message))
return err
default:
// Handle standard log entries
return h.handleRegularOutput(e)
}
}
// Timestamped enables or disables timestamp display and optionally sets a custom time format.
// If format is empty, defaults to RFC3339.
// Example:
//
// handler := NewColorizedHandler(os.Stdout).Timestamped(true, time.StampMilli)
// // Output: Jan 02 15:04:05.000 [app] INFO: Test
func (h *ColorizedHandler) Timestamped(enable bool, format ...string) {
h.showTime = enable
if len(format) > 0 && format[0] != "" {
h.timeFormat = format[0]
}
}
// handleRegularOutput handles normal log entries.
// It formats the entry with colored namespace, level, message, fields, and stack trace (if present),
// writing the result to the handler's writer.
// Returns an error if writing fails.
// Example (internal usage):
//
// h.handleRegularOutput(&lx.Entry{Message: "test", Level: lx.LevelInfo}) // Writes colored output
func (h *ColorizedHandler) handleRegularOutput(e *lx.Entry) error {
var builder strings.Builder // Buffer for building formatted output
// Add timestamp if enabled
if h.showTime {
builder.WriteString(e.Timestamp.Format(h.timeFormat))
builder.WriteString(lx.Space)
}
// Format namespace with colors
h.formatNamespace(&builder, e)
// Format level with color based on severity
h.formatLevel(&builder, e)
// Add message and fields
builder.WriteString(e.Message)
h.formatFields(&builder, e)
// fmt.Println("------------>", len(e.Stack))
// Format stack trace if present
if len(e.Stack) > 0 {
h.formatStack(&builder, e.Stack)
}
// Append newline for non-None levels
if e.Level != lx.LevelNone {
builder.WriteString(lx.Newline)
}
// Write formatted output to writer
_, err := h.w.Write([]byte(builder.String()))
return err
}
// formatNamespace formats the namespace with ANSI color codes.
// It supports FlatPath ([parent/child]) and NestedPath ([parent]→[child]) styles.
// Example (internal usage):
//
// h.formatNamespace(&builder, &lx.Entry{Namespace: "parent/child", Style: lx.FlatPath}) // Writes "[parent/child]: "
func (h *ColorizedHandler) formatNamespace(b *strings.Builder, e *lx.Entry) {
if e.Namespace == "" {
return
}
b.WriteString(lx.LeftBracket)
switch e.Style {
case lx.NestedPath:
// Split namespace and format as [parent]→[child]
parts := strings.Split(e.Namespace, lx.Slash)
for i, part := range parts {
b.WriteString(part)
b.WriteString(lx.RightBracket)
if i < len(parts)-1 {
b.WriteString(lx.Arrow)
b.WriteString(lx.LeftBracket)
}
}
default: // FlatPath
// Format as [parent/child]
b.WriteString(e.Namespace)
b.WriteString(lx.RightBracket)
}
b.WriteString(lx.Colon)
b.WriteString(lx.Space)
}
// formatLevel formats the log level with ANSI color codes.
// It applies a color based on the level (Debug, Info, Warn, Error) and resets afterward.
// Example (internal usage):
//
// h.formatLevel(&builder, &lx.Entry{Level: lx.LevelInfo}) // Writes "<green>INFO<reset>: "
func (h *ColorizedHandler) formatLevel(b *strings.Builder, e *lx.Entry) {
// Map levels to colors
color := map[lx.LevelType]string{
lx.LevelDebug: h.palette.Debug, // Cyan
lx.LevelInfo: h.palette.Info, // Green
lx.LevelWarn: h.palette.Warn, // Yellow
lx.LevelError: h.palette.Error, // Red
}[e.Level]
b.WriteString(color)
b.WriteString(e.Level.String())
b.WriteString(h.palette.Reset)
b.WriteString(lx.Colon)
b.WriteString(lx.Space)
}
// formatFields formats the log entry's fields in sorted order.
// It writes fields as [key=value key=value], with no additional coloring.
// Example (internal usage):
//
// h.formatFields(&builder, &lx.Entry{Fields: map[string]interface{}{"key": "value"}}) // Writes " [key=value]"
func (h *ColorizedHandler) formatFields(b *strings.Builder, e *lx.Entry) {
if len(e.Fields) == 0 {
return
}
// Collect and sort field keys
var keys []string
for k := range e.Fields {
keys = append(keys, k)
}
sort.Strings(keys)
b.WriteString(lx.Space)
b.WriteString(lx.LeftBracket)
// Format fields as key=value
for i, k := range keys {
if i > 0 {
b.WriteString(lx.Space)
}
b.WriteString(k)
b.WriteString("=")
b.WriteString(fmt.Sprint(e.Fields[k]))
}
b.WriteString(lx.RightBracket)
}
// formatStack formats a stack trace with ANSI color codes.
// It structures the stack trace with colored goroutine, function, and path segments,
// using indentation and separators for readability.
// Example (internal usage):
//
// h.formatStack(&builder, []byte("goroutine 1 [running]:\nmain.main()\n\tmain.go:10")) // Appends colored stack trace
func (h *ColorizedHandler) formatStack(b *strings.Builder, stack []byte) {
b.WriteString("\n")
b.WriteString(h.palette.Header)
b.WriteString("[stack]")
b.WriteString(h.palette.Reset)
b.WriteString("\n")
lines := strings.Split(string(stack), "\n")
if len(lines) == 0 {
return
}
// Format goroutine line
b.WriteString(" ┌─ ")
b.WriteString(h.palette.Goroutine)
b.WriteString(lines[0])
b.WriteString(h.palette.Reset)
b.WriteString("\n")
// Pair function name and file path lines
for i := 1; i < len(lines)-1; i += 2 {
funcLine := strings.TrimSpace(lines[i])
pathLine := strings.TrimSpace(lines[i+1])
if funcLine != "" {
b.WriteString(" │ ")
b.WriteString(h.palette.Func)
b.WriteString(funcLine)
b.WriteString(h.palette.Reset)
b.WriteString("\n")
}
if pathLine != "" {
b.WriteString(" │ ")
// Look for last "/" before ".go:"
lastSlash := strings.LastIndex(pathLine, "/")
goIndex := strings.Index(pathLine, ".go:")
if lastSlash >= 0 && goIndex > lastSlash {
// Prefix path
prefix := pathLine[:lastSlash+1]
// File and line (e.g., ll.go:698 +0x5c)
suffix := pathLine[lastSlash+1:]
b.WriteString(h.palette.Path)
b.WriteString(prefix)
b.WriteString(h.palette.Reset)
b.WriteString(h.palette.Path) // Use mainPath color for suffix
b.WriteString(suffix)
b.WriteString(h.palette.Reset)
} else {
// Fallback: whole line is gray
b.WriteString(h.palette.Path)
b.WriteString(pathLine)
b.WriteString(h.palette.Reset)
}
b.WriteString("\n")
}
}
// Handle any remaining unpaired line
if len(lines)%2 == 0 && strings.TrimSpace(lines[len(lines)-1]) != "" {
b.WriteString(" │ ")
b.WriteString(h.palette.Func)
b.WriteString(strings.TrimSpace(lines[len(lines)-1]))
b.WriteString(h.palette.Reset)
b.WriteString("\n")
}
b.WriteString(" └\n")
}
// handleDumpOutput formats hex dump output with ANSI color codes.
// It applies colors to position, hex, ASCII, and title components of the dump,
// wrapping the output with colored BEGIN/END separators.
// Returns an error if writing fails.
// Example (internal usage):
//
// h.handleDumpOutput(&lx.Entry{Class: lx.ClassDump, Message: "pos 00 hex: 61 62 'ab'"}) // Writes colored dump
func (h *ColorizedHandler) handleDumpOutput(e *lx.Entry) error {
var builder strings.Builder
// Add timestamp if enabled
if h.showTime {
builder.WriteString(e.Timestamp.Format(h.timeFormat))
builder.WriteString(lx.Newline)
}
// Write colored BEGIN separator
builder.WriteString(h.palette.Title)
builder.WriteString("---- BEGIN DUMP ----")
builder.WriteString(h.palette.Reset)
builder.WriteString("\n")
// Process each line of the dump
lines := strings.Split(e.Message, "\n")
length := len(lines)
for i, line := range lines {
if strings.HasPrefix(line, "pos ") {
// Parse and color position and hex/ASCII parts
parts := strings.SplitN(line, "hex:", 2)
if len(parts) == 2 {
builder.WriteString(h.palette.Pos)
builder.WriteString(parts[0])
builder.WriteString(h.palette.Reset)
hexAscii := strings.SplitN(parts[1], "'", 2)
builder.WriteString(h.palette.Hex)
builder.WriteString("hex:")
builder.WriteString(hexAscii[0])
builder.WriteString(h.palette.Reset)
if len(hexAscii) > 1 {
builder.WriteString(h.palette.Ascii)
builder.WriteString("'")
builder.WriteString(hexAscii[1])
builder.WriteString(h.palette.Reset)
}
}
} else if strings.HasPrefix(line, "Dumping value of type:") {
// Color type dump lines
builder.WriteString(h.palette.Header)
builder.WriteString(line)
builder.WriteString(h.palette.Reset)
} else {
// Write non-dump lines as-is
builder.WriteString(line)
}
// Don't add newline for the last line
if i < length-1 {
builder.WriteString("\n")
}
}
// Write colored END separator
builder.WriteString(h.palette.Title)
builder.WriteString("---- END DUMP ----")
builder.WriteString(h.palette.Reset)
builder.WriteString("\n")
// Write formatted output to writer
_, err := h.w.Write([]byte(builder.String()))
return err
}
// detectPalette selects a color palette based on terminal environment variables.
// It checks TERM_BACKGROUND, COLORFGBG, and AppleInterfaceStyle to determine
// whether a light or dark palette is appropriate, defaulting to darkPalette.
// Example (internal usage):
//
// palette := h.detectPalette() // Returns darkPalette or lightPalette
func (h *ColorizedHandler) detectPalette() Palette {
// Check TERM_BACKGROUND (e.g., iTerm2)
if bg, ok := os.LookupEnv("TERM_BACKGROUND"); ok {
if bg == "light" {
return lightPalette // Use light palette for light background
}
return darkPalette // Use dark palette otherwise
}
// Check COLORFGBG (traditional xterm)
if fgBg, ok := os.LookupEnv("COLORFGBG"); ok {
parts := strings.Split(fgBg, ";")
if len(parts) >= 2 {
bg := parts[len(parts)-1] // Last part (some terminals add more fields)
if bg == "7" || bg == "15" || bg == "0;15" { // Handle variations
return lightPalette // Use light palette for light background
}
}
}
// Check macOS dark mode
if style, ok := os.LookupEnv("AppleInterfaceStyle"); ok && strings.EqualFold(style, "dark") {
return darkPalette // Use dark palette for macOS dark mode
}
// Default: dark (conservative choice for terminals)
return darkPalette
}