fupie/logie/logie.go

247 lines
4.9 KiB
Go

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)
}