From f75ae60e740b5593542f1ce9f45de037ffd0bbb3 Mon Sep 17 00:00:00 2001 From: Niklas Ye Date: Fri, 7 Aug 2026 13:31:51 +0200 Subject: [PATCH] fix: truncate in runes rather than bytes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit truncate measures and slices by byte, so a string cut inside a multi-byte rune both mis-measures the fixed-width column it is being laid out against and emits a broken character. Everything it is handed is server-supplied — alert names, label values, annotations — and none of that is guaranteed to be ASCII. Identical behaviour for the ASCII case. --- internal/tui/view.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/tui/view.go b/internal/tui/view.go index aa3de9c..b916eb2 100644 --- a/internal/tui/view.go +++ b/internal/tui/view.go @@ -808,12 +808,19 @@ func renderBarWidth(count, maxCount, maxWidth int) int { return w } +// truncate shortens s to max terminal cells, marking the cut with an ellipsis. +// +// Counted in runes rather than bytes: these strings are laid out against +// fixed-width columns, and a byte cut through a multi-byte rune would both +// mis-measure the column and emit a broken character. Server-supplied text — +// labels, annotations, delivery errors — is not guaranteed to be ASCII. func truncate(s string, max int) string { if max < 1 { return "" } - if len(s) <= max { + r := []rune(s) + if len(r) <= max { return s } - return s[:max-1] + "…" + return string(r[:max-1]) + "…" }