diff --git a/handler.go b/handler.go index 878dfa1..e8bddbf 100644 --- a/handler.go +++ b/handler.go @@ -79,20 +79,24 @@ import ( "path/filepath" "runtime" "strconv" + "strings" "sync" "time" "unicode" + "unicode/utf8" ) // ANSI modes const ( - ansiReset = "\033[0m" - ansiFaint = "\033[2m" - ansiResetFaint = "\033[22m" - ansiBrightRed = "\033[91m" - ansiBrightGreen = "\033[92m" - ansiBrightYellow = "\033[93m" - ansiBrightRedFaint = "\033[91;2m" + ansiReset = "\u001b[0m" + ansiFaint = "\u001b[2m" + ansiResetFaint = "\u001b[22m" + ansiBrightRed = "\u001b[91m" + ansiBrightGreen = "\u001b[92m" + ansiBrightYellow = "\u001b[93m" + ansiBrightRedFaint = "\u001b[91;2m" + + ansiEsc = '\u001b' ) const errKey = "err" @@ -378,7 +382,7 @@ func (h *handler) appendAttr(buf *buffer, attr slog.Attr, groupsPrefix string, g func (h *handler) appendKey(buf *buffer, key, groups string) { buf.WriteStringIf(!h.noColor, ansiFaint) - appendString(buf, groups+key, true) + appendString(buf, groups+key, true, !h.noColor) buf.WriteByte('=') buf.WriteStringIf(!h.noColor, ansiReset) } @@ -386,7 +390,7 @@ func (h *handler) appendKey(buf *buffer, key, groups string) { func (h *handler) appendValue(buf *buffer, v slog.Value, quote bool) { switch v.Kind() { case slog.KindString: - appendString(buf, v.String(), quote) + appendString(buf, v.String(), quote, !h.noColor) case slog.KindInt64: *buf = strconv.AppendInt(*buf, v.Int64(), 10) case slog.KindUint64: @@ -396,9 +400,9 @@ func (h *handler) appendValue(buf *buffer, v slog.Value, quote bool) { case slog.KindBool: *buf = strconv.AppendBool(*buf, v.Bool()) case slog.KindDuration: - appendString(buf, v.Duration().String(), quote) + appendString(buf, v.Duration().String(), quote, !h.noColor) case slog.KindTime: - appendString(buf, v.Time().String(), quote) + appendString(buf, v.Time().String(), quote, !h.noColor) case slog.KindAny: switch cv := v.Any().(type) { case slog.Level: @@ -408,44 +412,196 @@ func (h *handler) appendValue(buf *buffer, v slog.Value, quote bool) { if err != nil { break } - appendString(buf, string(data), quote) + appendString(buf, string(data), quote, !h.noColor) case *slog.Source: h.appendSource(buf, cv) default: - appendString(buf, fmt.Sprintf("%+v", v.Any()), quote) + appendString(buf, fmt.Sprintf("%+v", v.Any()), quote, !h.noColor) } } } func (h *handler) appendTintError(buf *buffer, err tintError, attrKey, groupsPrefix string) { buf.WriteStringIf(!h.noColor, ansiBrightRedFaint) - appendString(buf, groupsPrefix+attrKey, true) + appendString(buf, groupsPrefix+attrKey, true, !h.noColor) buf.WriteByte('=') buf.WriteStringIf(!h.noColor, ansiResetFaint) - appendString(buf, err.Error(), true) + appendString(buf, err.Error(), true, !h.noColor) buf.WriteStringIf(!h.noColor, ansiReset) } -func appendString(buf *buffer, s string, quote bool) { - if quote && needsQuoting(s) { +func appendString(buf *buffer, s string, quote, color bool) { + if quote && !color { + // trim ANSI escape sequences + var inEscape bool + s = cut(s, func(r rune) bool { + if r == ansiEsc { + inEscape = true + } else if inEscape && unicode.IsLetter(r) { + inEscape = false + return true + } + + return inEscape + }) + } + + quote = quote && needsQuoting(s) + switch { + case color && quote: + s = strconv.Quote(s) + s = strings.ReplaceAll(s, `\x1b`, string(ansiEsc)) + buf.WriteString(s) + case !color && quote: *buf = strconv.AppendQuote(*buf, s) - } else { + default: buf.WriteString(s) } } +func cut(s string, f func(r rune) bool) string { + var res []rune + for i := 0; i < len(s); { + r, size := utf8.DecodeRuneInString(s[i:]) + if r == utf8.RuneError { + break + } + if !f(r) { + res = append(res, r) + } + i += size + } + return string(res) +} + +// Copied from log/slog/text_handler.go. func needsQuoting(s string) bool { if len(s) == 0 { return true } - for _, r := range s { - if unicode.IsSpace(r) || r == '"' || r == '=' || !unicode.IsPrint(r) { + for i := 0; i < len(s); { + b := s[i] + if b < utf8.RuneSelf { + // Quote anything except a backslash that would need quoting in a + // JSON string, as well as space and '=' + if b != '\\' && (b == ' ' || b == '=' || !safeSet[b]) { + return true + } + i++ + continue + } + r, size := utf8.DecodeRuneInString(s[i:]) + if r == utf8.RuneError || unicode.IsSpace(r) || !unicode.IsPrint(r) { return true } + i += size } return false } +// Copied from log/slog/json_handler.go. +// +// safeSet is extended by the ANSI escape code "\u001b". +var safeSet = [utf8.RuneSelf]bool{ + ' ': true, + '!': true, + '"': false, + '#': true, + '$': true, + '%': true, + '&': true, + '\'': true, + '(': true, + ')': true, + '*': true, + '+': true, + ',': true, + '-': true, + '.': true, + '/': true, + '0': true, + '1': true, + '2': true, + '3': true, + '4': true, + '5': true, + '6': true, + '7': true, + '8': true, + '9': true, + ':': true, + ';': true, + '<': true, + '=': true, + '>': true, + '?': true, + '@': true, + 'A': true, + 'B': true, + 'C': true, + 'D': true, + 'E': true, + 'F': true, + 'G': true, + 'H': true, + 'I': true, + 'J': true, + 'K': true, + 'L': true, + 'M': true, + 'N': true, + 'O': true, + 'P': true, + 'Q': true, + 'R': true, + 'S': true, + 'T': true, + 'U': true, + 'V': true, + 'W': true, + 'X': true, + 'Y': true, + 'Z': true, + '[': true, + '\\': false, + ']': true, + '^': true, + '_': true, + '`': true, + 'a': true, + 'b': true, + 'c': true, + 'd': true, + 'e': true, + 'f': true, + 'g': true, + 'h': true, + 'i': true, + 'j': true, + 'k': true, + 'l': true, + 'm': true, + 'n': true, + 'o': true, + 'p': true, + 'q': true, + 'r': true, + 's': true, + 't': true, + 'u': true, + 'v': true, + 'w': true, + 'x': true, + 'y': true, + 'z': true, + '{': true, + '|': true, + '}': true, + '~': true, + '\u007f': true, + '\u001b': true, +} + type tintError struct{ error } // Err returns a tinted (colorized) [slog.Attr] that will be written in red color diff --git a/handler_test.go b/handler_test.go index ee20c37..c463629 100644 --- a/handler_test.go +++ b/handler_test.go @@ -95,15 +95,17 @@ func TestHandler(t *testing.T) { { Opts: &tint.Options{ AddSource: true, + NoColor: true, }, F: func(l *slog.Logger) { l.Info("test", "key", "val") }, - Want: `Nov 10 23:00:00.000 INF tint/handler_test.go:100 test key=val`, + Want: `Nov 10 23:00:00.000 INF tint/handler_test.go:101 test key=val`, }, { Opts: &tint.Options{ TimeFormat: time.Kitchen, + NoColor: true, }, F: func(l *slog.Logger) { l.Info("test", "key", "val") @@ -113,6 +115,7 @@ func TestHandler(t *testing.T) { { Opts: &tint.Options{ ReplaceAttr: drop(slog.TimeKey), + NoColor: true, }, F: func(l *slog.Logger) { l.Info("test", "key", "val") @@ -122,6 +125,7 @@ func TestHandler(t *testing.T) { { Opts: &tint.Options{ ReplaceAttr: drop(slog.LevelKey), + NoColor: true, }, F: func(l *slog.Logger) { l.Info("test", "key", "val") @@ -131,6 +135,7 @@ func TestHandler(t *testing.T) { { Opts: &tint.Options{ ReplaceAttr: drop(slog.MessageKey), + NoColor: true, }, F: func(l *slog.Logger) { l.Info("test", "key", "val") @@ -140,6 +145,7 @@ func TestHandler(t *testing.T) { { Opts: &tint.Options{ ReplaceAttr: drop(slog.TimeKey, slog.LevelKey, slog.MessageKey), + NoColor: true, }, F: func(l *slog.Logger) { l.Info("test", "key", "val") @@ -149,6 +155,7 @@ func TestHandler(t *testing.T) { { Opts: &tint.Options{ ReplaceAttr: drop("key"), + NoColor: true, }, F: func(l *slog.Logger) { l.Info("test", "key", "val") @@ -158,6 +165,7 @@ func TestHandler(t *testing.T) { { Opts: &tint.Options{ ReplaceAttr: drop("key"), + NoColor: true, }, F: func(l *slog.Logger) { l.WithGroup("group").Info("test", "key", "val", "key2", "val2") @@ -172,6 +180,7 @@ func TestHandler(t *testing.T) { } return a }, + NoColor: true, }, F: func(l *slog.Logger) { l.WithGroup("group").Info("test", "key", "val", "key2", "val2") @@ -181,6 +190,7 @@ func TestHandler(t *testing.T) { { Opts: &tint.Options{ ReplaceAttr: replace(slog.IntValue(42), slog.TimeKey), + NoColor: true, }, F: func(l *slog.Logger) { l.Info("test", "key", "val") @@ -190,6 +200,7 @@ func TestHandler(t *testing.T) { { Opts: &tint.Options{ ReplaceAttr: replace(slog.StringValue("INFO"), slog.LevelKey), + NoColor: true, }, F: func(l *slog.Logger) { l.Info("test", "key", "val") @@ -199,6 +210,7 @@ func TestHandler(t *testing.T) { { Opts: &tint.Options{ ReplaceAttr: replace(slog.IntValue(42), slog.MessageKey), + NoColor: true, }, F: func(l *slog.Logger) { l.Info("test", "key", "val") @@ -208,6 +220,7 @@ func TestHandler(t *testing.T) { { Opts: &tint.Options{ ReplaceAttr: replace(slog.IntValue(42), "key"), + NoColor: true, }, F: func(l *slog.Logger) { l.With("key", "val").Info("test", "key2", "val2") @@ -219,6 +232,7 @@ func TestHandler(t *testing.T) { ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { return slog.Attr{} }, + NoColor: true, }, F: func(l *slog.Logger) { l.Info("test", "key", "val") @@ -252,7 +266,8 @@ func TestHandler(t *testing.T) { }, { Opts: &tint.Options{ - Level: slog.LevelDebug - 1, + Level: slog.LevelDebug - 1, + NoColor: true, }, F: func(l *slog.Logger) { l.Log(context.TODO(), slog.LevelDebug-1, "test") @@ -279,6 +294,7 @@ func TestHandler(t *testing.T) { } return a }, + NoColor: true, }, F: func(l *slog.Logger) { l.Error("test") @@ -296,6 +312,7 @@ func TestHandler(t *testing.T) { Opts: &tint.Options{ ReplaceAttr: drop(slog.TimeKey, slog.LevelKey, slog.MessageKey, slog.SourceKey), AddSource: true, + NoColor: true, }, F: func(l *slog.Logger) { l.WithGroup("group").Info("test", "key", "val") @@ -310,6 +327,7 @@ func TestHandler(t *testing.T) { } return a }, + NoColor: true, }, F: func(l *slog.Logger) { l.Info("test") @@ -322,11 +340,12 @@ func TestHandler(t *testing.T) { ReplaceAttr: func(g []string, a slog.Attr) slog.Attr { return a }, + NoColor: true, }, F: func(l *slog.Logger) { l.Info("test") }, - Want: `Nov 10 23:00:00.000 INF tint/handler_test.go:327 test`, + Want: `Nov 10 23:00:00.000 INF tint/handler_test.go:346 test`, }, { // https://github.com/lmittmann/tint/issues/44 F: func(l *slog.Logger) { @@ -344,6 +363,42 @@ func TestHandler(t *testing.T) { }, Want: `Nov 10 23:00:00.000 INF test key="{A:123 B:}"`, }, + { // https://github.com/lmittmann/tint/issues/59 + Opts: &tint.Options{ + NoColor: false, + }, + F: func(l *slog.Logger) { + l.Info("test", "color", "\033[92mgreen\033[0m") + }, + Want: "\033[2mNov 10 23:00:00.000\033[0m \033[92mINF\033[0m test \033[2mcolor=\033[0m\033[92mgreen\033[0m", + }, + { + Opts: &tint.Options{ + NoColor: false, + }, + F: func(l *slog.Logger) { + l.Info("test", "color", "\033[92mgreen quoted\033[0m") + }, + Want: "\033[2mNov 10 23:00:00.000\033[0m \033[92mINF\033[0m test \033[2mcolor=\033[0m\"\033[92mgreen quoted\033[0m\"", + }, + { + Opts: &tint.Options{ + NoColor: true, + }, + F: func(l *slog.Logger) { + l.Info("test", "color", "\033[92mgreen\033[0m") + }, + Want: `Nov 10 23:00:00.000 INF test color=green`, + }, + { + Opts: &tint.Options{ + NoColor: true, + }, + F: func(l *slog.Logger) { + l.Info("test", "color", "\033[92mgreen quoted\033[0m") + }, + Want: `Nov 10 23:00:00.000 INF test color="green quoted"`, + }, { // https://github.com/lmittmann/tint/pull/66 F: func(l *slog.Logger) { errAttr := tint.Err(errors.New("fail")) @@ -358,9 +413,8 @@ func TestHandler(t *testing.T) { t.Run(strconv.Itoa(i), func(t *testing.T) { var buf bytes.Buffer if test.Opts == nil { - test.Opts = &tint.Options{} + test.Opts = &tint.Options{NoColor: true} } - test.Opts.NoColor = true l := slog.New(tint.NewHandler(&buf, test.Opts)) test.F(l)