package logie import ( "fmt" "os" "strings" "time" ) const ( printFmt = "%s%s %s\n" escape byte = 0x1B csi byte = '[' separator byte = ';' csiEnd byte = 'm' fgColor byte = '3' bgColor byte = '4' reset byte = '0' ) type Maybe[T any] struct { exists bool value T } func Exists[T any](t T) Maybe[T] { return Maybe[T]{ exists: true, value: t, } } func Empty[T any]() Maybe[T] { return Maybe[T]{} } func (m Maybe[T]) Exists() bool { return m.exists } func (m Maybe[T]) Value() T { return m.value } type Color uint8 const ( ColorBlack = '0' ColorRed = '1' ColorGreen = '2' ColorYellow = '3' ColorBlue = '4' ColorMagenta = '5' ColorCyan = '6' ColorWhite = '7' ) type ColorSet struct { fg Maybe[Color] bg Maybe[Color] font Maybe[Font] } func Colors() ColorSet { return ColorSet{} } func (c ColorSet) Foreground(fg Color) ColorSet { c.fg = Exists(fg) return c } func (c ColorSet) Background(bg Color) ColorSet { c.bg = Exists(bg) return c } func (c ColorSet) Font(f Font) ColorSet { c.font = Exists(f) return c } func (c ColorSet) String(v string) string { var hasFg, hasBg, hasFont = c.fg.Exists(), c.bg.Exists(), c.font.Exists() if !hasFg && !hasBg && !hasFont { return v } // Build format formatBytes := []byte{escape, csi} if hasFg { formatBytes = append(formatBytes, fgColor, byte(c.fg.Value())) } if hasBg { if len(formatBytes) > 2 { formatBytes = append(formatBytes, separator) } formatBytes = append(formatBytes, bgColor, byte(c.bg.Value())) } if hasFont { if len(formatBytes) > 2 { formatBytes = append(formatBytes, separator) } formatBytes = append(formatBytes, byte(c.font.Value())) } formatBytes = append(formatBytes, csiEnd) formatBytes = append(append(formatBytes, []byte(v)...), escape, csi, reset, csiEnd) return string(formatBytes) } type Font uint8 const ( FontBold = '1' FontFaint = '2' FontUnderlined = '4' FontSlowBlink = '5' ) var ( info = Colors().Font(FontFaint).String("[INFO]") warn = Colors().Background(ColorYellow).String("[WARN]") error_ = Colors().Foreground(ColorWhite).Background(ColorRed).Font(FontBold).String("[ERROR]") fatal = Colors().Foreground(ColorRed).Background(ColorYellow).Font(FontBold).String("[FATAL]") ) type Log struct { output *os.File timeFmt string color map[int]ColorSet timeColor ColorSet } func New() Log { return Log{ output: os.Stdout, timeFmt: time.RFC822, timeColor: Colors().Font(FontFaint), color: map[int]ColorSet{}, } } func (l Log) Clone() Log { colorClone := map[int]ColorSet{} for k, cs := range l.color { colorClone[k] = cs } l.color = colorClone return l } // ColorBracket returns a Log with the coloring information for the bracket index. // // The way coloring works in Logie is that you can only color the parts of your // message that are within closed brackets `[]` in its format string // (therefore non-f calls cannot color). The [index] being passed in is a zero-indexed // index of which bracket set to color. func (l Log) ColorBracket(index int, set ColorSet) Log { l = l.Clone() l.color[index] = set return l } func (l Log) time() string { return l.timeColor.String("[" + time.Now().Format(l.timeFmt) + "]") } func (l Log) Logf(level, format string, args ...any) { line := fmt.Sprintf(colorize(format, l.color), args...) fmt.Fprintf(l.output, printFmt, l.time(), level, line) } func colorize(format string, colors map[int]ColorSet) string { var ( parts = []string{} bracketParts = []int{} last int ) outer: for index := 0; index < len(format); index++ { if format[index] == '[' { parts = append(parts, format[last:index]) for inner := index + 1; index < len(format); inner++ { if format[inner] == ']' { current := format[index : inner+1] parts = append(parts, current) bracketParts = append(bracketParts, len(parts)-1) last = inner + 1 continue outer } } parts = append(parts, format[index:]) } } parts = append(parts, format[last:]) for bracketIndex, color := range colors { if bracketIndex >= len(bracketParts) { continue } parts[bracketParts[bracketIndex]] = color.String(parts[bracketParts[bracketIndex]]) } return strings.Join(parts, "") } func (l Log) Log(level string, args ...any) { line := fmt.Sprint(args...) fmt.Fprintf(l.output, printFmt, l.time(), level, line) } func (l Log) Infof(format string, args ...any) { l.Logf(info, format, args...) } func (l Log) Info(args ...any) { l.Log(info, args...) } func (l Log) Warnf(format string, args ...any) { l.Logf(warn, format, args...) } func (l Log) Warn(args ...any) { l.Log(warn, args...) } func (l Log) Errorf(format string, args ...any) { l.Logf(error_, format, args...) } func (l Log) Error(args ...any) { l.Log(error_, args...) } func (l Log) Fatalf(format string, args ...any) { l.Logf(fatal, format, args...) os.Exit(1) } func (l Log) Fatal(args ...any) { l.Log(fatal, args...) os.Exit(1) }