Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions text.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"time"
"unicode"
"unicode/utf8"

"charm.land/lipgloss/v2"
)

const (
Expand All @@ -17,6 +19,7 @@ const (

func (l *Logger) writeIndent(w io.Writer, str string, indent string, newline bool, key string) {
st := l.styles
str = normalizeLogNewlines(str)

// kindly borrowed from hclog
for {
Expand All @@ -26,9 +29,9 @@ func (l *Logger) writeIndent(w io.Writer, str string, indent string, newline boo
_, _ = w.Write([]byte(indent))
val := escapeStringForOutput(str, false)
if valueStyle, ok := st.Values[key]; ok {
val = valueStyle.Render(val)
val = renderKeepTabs(valueStyle, val)
} else {
val = st.Value.Render(val)
val = renderKeepTabs(st.Value, val)
}
_, _ = w.Write([]byte(val))
if newline {
Expand All @@ -40,13 +43,27 @@ func (l *Logger) writeIndent(w io.Writer, str string, indent string, newline boo

_, _ = w.Write([]byte(indent))
val := escapeStringForOutput(str[:nl], false)
val = st.Value.Render(val)
val = renderKeepTabs(st.Value, val)
_, _ = w.Write([]byte(val))
_, _ = w.Write([]byte{'\n'})
str = str[nl+1:]
}
}

// normalizeLogNewlines turns CRLF and leftover CR into LF so Windows line
// endings don't show up as a literal \r in the text formatter.
func normalizeLogNewlines(s string) string {
s = strings.ReplaceAll(s, "\r\n", "\n")
return strings.ReplaceAll(s, "\r", "\n")
}

// renderKeepTabs styles s without converting tabs to spaces. Lip Gloss
// otherwise replaces tabs with 4 spaces (or removes them when TabWidth is 0),
// which breaks pretty-printed errors that indent with tabs.
func renderKeepTabs(style lipgloss.Style, s string) string {
return style.TabWidth(lipgloss.NoTabConversion).Render(s)
}

func needsEscaping(str string) bool {
for _, b := range str {
if !unicode.IsPrint(b) || b == '"' {
Expand Down Expand Up @@ -95,7 +112,9 @@ func escapeStringForOutput(str string, escapeQuotes bool) string {
case '\r':
bb.WriteString(`\r`)
case '\t':
bb.WriteString(`\t`)
// Keep tabs as tabs so multiline values (pretty-printed
// errors, source fragments) retain their indentation.
bb.WriteRune(r)
case '\v':
bb.WriteString(`\v`)
default:
Expand Down Expand Up @@ -209,8 +228,8 @@ func (l *Logger) textFormatter(keyvals ...any) {
}
case MessageKey:
if msg := keyvals[i+1]; msg != nil {
m := fmt.Sprint(msg)
m = st.Message.Render(m)
m := normalizeLogNewlines(fmt.Sprint(msg))
m = renderKeepTabs(st.Message, m)
writeSpace(&l.b, firstKey)
l.b.WriteString(m)
}
Expand Down Expand Up @@ -255,10 +274,10 @@ func (l *Logger) textFormatter(keyvals ...any) {
writeSpace(&l.b, firstKey)
l.b.WriteString(key)
l.b.WriteString(sep)
l.b.WriteString(valueStyle.Render(fmt.Sprintf(`"%s"`,
l.b.WriteString(renderKeepTabs(valueStyle, fmt.Sprintf(`"%s"`,
escapeStringForOutput(val, true))))
} else {
val = valueStyle.Render(val)
val = renderKeepTabs(valueStyle, val)
writeSpace(&l.b, firstKey)
l.b.WriteString(key)
l.b.WriteString(sep)
Expand Down
21 changes: 21 additions & 0 deletions text_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,27 @@ func TestTextLogger(t *testing.T) {
kvs: []any{"key1", "val1\nval2"},
f: logger.Error,
},
{
name: "multiline value preserves tabs",
expected: "ERROR oops\n error=\n │ src:\n │ \tline\n",
msg: "oops",
kvs: []any{"error", "src:\n\tline"},
f: logger.Error,
},
{
name: "multiline value treats CRLF as a line break",
expected: "ERROR oops\n error=\n │ src:\n │ \tline\n",
msg: "oops",
kvs: []any{"error", "src:\r\n\tline"},
f: logger.Error,
},
{
name: "message preserves tabs",
expected: "ERROR src:\tline\n",
msg: "src:\tline",
kvs: nil,
f: logger.Error,
},
{
name: "odd number of keyvals",
expected: "ERROR info key1=val1 key2=val2 key3=\"missing value\"\n",
Expand Down