diff --git a/internal/ansiext/ansi.go b/internal/ansiext/ansi.go new file mode 100644 index 0000000000000000000000000000000000000000..d1c636afd4b1cf757dc9a89ebe179d6bcfa5f1d6 --- /dev/null +++ b/internal/ansiext/ansi.go @@ -0,0 +1,24 @@ +package ansiext + +import ( + "strings" + + "github.com/charmbracelet/x/ansi" +) + +// Escape replaces control characters with their Unicode Control Picture +// representations to ensure they are displayed correctly in the UI. +func Escape(content string) string { + var sb strings.Builder + for _, r := range content { + switch { + case r >= 0 && r <= 0x1f: // Control characters 0x00-0x1F + sb.WriteRune('\u2400' + r) + case r == ansi.DEL: + sb.WriteRune('\u2421') + default: + sb.WriteRune(r) + } + } + return sb.String() +} diff --git a/internal/tui/components/chat/messages/renderer.go b/internal/tui/components/chat/messages/renderer.go index ad9cd475704a84f2c400b611f592d25b5a78eddc..ace42420a26a47854313029e48ca4b3f495525c4 100644 --- a/internal/tui/components/chat/messages/renderer.go +++ b/internal/tui/components/chat/messages/renderer.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/charmbracelet/crush/internal/ansiext" "github.com/charmbracelet/crush/internal/fsext" "github.com/charmbracelet/crush/internal/llm/agent" "github.com/charmbracelet/crush/internal/llm/tools" @@ -702,7 +703,7 @@ func renderPlainContent(v *toolCallCmp, content string) string { if i >= responseContextHeight { break } - ln = escapeContent(ln) + ln = ansiext.Escape(ln) ln = " " + ln // left padding if len(ln) > width { ln = v.fit(ln, width) @@ -740,7 +741,7 @@ func renderCodeContent(v *toolCallCmp, path, content string, offset int) string lines := strings.Split(truncated, "\n") for i, ln := range lines { - lines[i] = escapeContent(ln) + lines[i] = ansiext.Escape(ln) } highlighted, _ := highlight.SyntaxHighlight(strings.Join(lines, "\n"), path, t.BgBase) @@ -816,20 +817,3 @@ func prettifyToolName(name string) string { return name } } - -// escapeContent replaces control characters with their Unicode Control Picture -// representations to ensure they are displayed correctly in the UI. -func escapeContent(content string) string { - var sb strings.Builder - for _, r := range content { - switch { - case r >= 0 && r <= 0x1f: // Control characters 0x00-0x1F - sb.WriteRune('\u2400' + r) - case r == ansi.DEL: - sb.WriteRune('\u2421') - default: - sb.WriteRune(r) - } - } - return sb.String() -}