summary history files

vendor/github.com/olekukonko/ll/lh/text.go
package lh

import (
	"fmt"
	"io"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/olekukonko/ll/lx"
)

// TextHandler is a handler that outputs log entries as plain text.
// It formats log entries with namespace, level, message, fields, and optional stack traces,
// writing the result to the provided writer.
// Thread-safe if the underlying writer is thread-safe.
type TextHandler struct {
	w          io.Writer // Destination for formatted log output
	showTime   bool      // Whether to display timestamps
	timeFormat string    // Format for timestamps (defaults to time.RFC3339)
	mu         sync.Mutex
}

// NewTextHandler creates a new TextHandler writing to the specified writer.
// It initializes the handler with the given writer, suitable for outputs like stdout or files.
// Example:
//
//	handler := NewTextHandler(os.Stdout)
//	logger := ll.New("app").Enable().Handler(handler)
//	logger.Info("Test") // Output: [app] INFO: Test
func NewTextHandler(w io.Writer) *TextHandler {
	return &TextHandler{
		w:          w,
		showTime:   false,
		timeFormat: time.RFC3339,
	}
}

// Timestamped enables or disables timestamp display and optionally sets a custom time format.
// If format is empty, defaults to RFC3339.
// Example:
//
//	handler := NewTextHandler(os.Stdout).TextWithTime(true, time.StampMilli)
//	// Output: Jan 02 15:04:05.000 [app] INFO: Test
func (h *TextHandler) Timestamped(enable bool, format ...string) {
	h.showTime = enable
	if len(format) > 0 && format[0] != "" {
		h.timeFormat = format[0]
	}
}

// Handle processes a log entry and writes it as plain text.
// 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 "INFO: test"
func (h *TextHandler) Handle(e *lx.Entry) error {
	h.mu.Lock()
	defer h.mu.Unlock()

	// Special handling for dump output
	if e.Class == lx.ClassDump {
		return h.handleDumpOutput(e)
	}

	// Raw entries are written directly without formatting
	if e.Class == lx.ClassRaw {
		_, err := h.w.Write([]byte(e.Message))
		return err
	}

	// Handle standard log entries
	return h.handleRegularOutput(e)
}

// handleRegularOutput handles normal log entries.
// It formats the entry with 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 "INFO: test"
func (h *TextHandler) 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 based on style
	switch e.Style {
	case lx.NestedPath:
		if e.Namespace != "" {
			// Split namespace into parts and format as [parent]→[child]
			parts := strings.Split(e.Namespace, lx.Slash)
			for i, part := range parts {
				builder.WriteString(lx.LeftBracket)
				builder.WriteString(part)
				builder.WriteString(lx.RightBracket)
				if i < len(parts)-1 {
					builder.WriteString(lx.Arrow)
				}
			}
			builder.WriteString(lx.Colon)
			builder.WriteString(lx.Space)
		}
	default: // FlatPath
		if e.Namespace != "" {
			// Format namespace as [parent/child]
			builder.WriteString(lx.LeftBracket)
			builder.WriteString(e.Namespace)
			builder.WriteString(lx.RightBracket)
			builder.WriteString(lx.Space)
		}
	}

	// Add level and message
	builder.WriteString(e.Level.String())
	builder.WriteString(lx.Colon)
	builder.WriteString(lx.Space)
	builder.WriteString(e.Message)

	// Add fields in sorted order
	if len(e.Fields) > 0 {
		var keys []string
		for k := range e.Fields {
			keys = append(keys, k)
		}
		// Sort keys for consistent output
		sort.Strings(keys)
		builder.WriteString(lx.Space)
		builder.WriteString(lx.LeftBracket)
		for i, k := range keys {
			if i > 0 {
				builder.WriteString(lx.Space)
			}
			// Format field as key=value
			builder.WriteString(k)
			builder.WriteString("=")
			builder.WriteString(fmt.Sprint(e.Fields[k]))
		}
		builder.WriteString(lx.RightBracket)
	}

	// Add 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
}

// handleDumpOutput specially formats hex dump output (plain text version).
// It wraps the dump message with BEGIN/END separators for clarity.
// Returns an error if writing fails.
// Example (internal usage):
//
//	h.handleDumpOutput(&lx.Entry{Class: lx.ClassDump, Message: "pos 00 hex: 61"}) // Writes "---- BEGIN DUMP ----\npos 00 hex: 61\n---- END DUMP ----\n"
func (h *TextHandler) handleDumpOutput(e *lx.Entry) error {
	// For text handler, we just add a newline before dump output
	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.Newline)
	}

	// Add separator lines and dump content
	builder.WriteString("---- BEGIN DUMP ----\n")
	builder.WriteString(e.Message)
	builder.WriteString("---- END DUMP ----\n")

	// Write formatted output to writer
	_, err := h.w.Write([]byte(builder.String()))
	return err
}

// formatStack formats a stack trace for plain text output.
// It structures the stack trace with indentation and separators for readability,
// including goroutine and function/file details.
// Example (internal usage):
//
//	h.formatStack(&builder, []byte("goroutine 1 [running]:\nmain.main()\n\tmain.go:10")) // Appends formatted stack trace
func (h *TextHandler) formatStack(b *strings.Builder, stack []byte) {
	lines := strings.Split(string(stack), "\n")
	if len(lines) == 0 {
		return
	}

	// Start stack trace section
	b.WriteString("\n[stack]\n")

	// First line: goroutine
	b.WriteString("  ┌─ ")
	b.WriteString(lines[0])
	b.WriteString("\n")

	// Iterate through remaining lines
	for i := 1; i < len(lines); i++ {
		line := strings.TrimSpace(lines[i])
		if line == "" {
			continue
		}

		if strings.Contains(line, ".go") {
			// File path lines get extra indent
			b.WriteString("  ├       ")
		} else {
			// Function names
			b.WriteString("  │   ")
		}
		b.WriteString(line)
		b.WriteString("\n")
	}

	// End stack trace section
	b.WriteString("  └\n")
}