Skip to content

Commit 81d66d7

Browse files
authored
fix: proper Unicode/Chinese character handling in UI (#234)
## Summary Fix garbled display and incorrect backspace behavior for Chinese/Unicode characters in the TUI. Closes #233 ## Problem Chinese and other multi-byte Unicode characters were displaying as garbled text (e.g., `是的分身艺术�`) because: 1. **String truncation used byte slicing** (`str[:n]`), which cuts UTF-8 characters in the middle 2. **Backspace deleted only 1 byte** instead of 1 character, corrupting multi-byte characters 3. **Width calculations used `len()`** which returns byte count, not display width ## Solution Use `github.com/mattn/go-runewidth` (already a dependency) for proper Unicode handling: - `runewidth.StringWidth()` - calculate actual display width - `runewidth.Truncate()` - safely truncate strings without breaking characters - `[]rune()` conversion - for character-based operations like backspace ## Changes | File | Change | |------|--------| | `ui/list.go` | Use `runewidth` for title/branch truncation and width calculation | | `ui/err.go` | Use `runewidth` for error message truncation | | `app/app.go` | Convert to `[]rune` for backspace, use `runewidth` for length check | ## Testing - [x] Tested with Chinese characters in instance names - [x] Tested backspace deletion of Chinese characters - [x] All existing tests pass (`go test ./...`) - [x] Build succeeds (`go build .`) ## Screenshots **Before (Bug):** Instance names with Chinese characters show garbled text when truncated or after backspace. **After (Fixed):** Chinese characters display correctly and backspace removes whole characters.
1 parent fc1b967 commit 81d66d7

File tree

3 files changed

+21
-15
lines changed

3 files changed

+21
-15
lines changed

app/app.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/charmbracelet/bubbles/spinner"
1616
tea "github.com/charmbracelet/bubbletea"
1717
"github.com/charmbracelet/lipgloss"
18+
"github.com/mattn/go-runewidth"
1819
)
1920

2021
const GlobalInstanceLimit = 10
@@ -360,17 +361,18 @@ func (m *home) handleKeyPress(msg tea.KeyMsg) (mod tea.Model, cmd tea.Cmd) {
360361

361362
return m, tea.Batch(tea.WindowSize(), m.instanceChanged())
362363
case tea.KeyRunes:
363-
if len(instance.Title) >= 32 {
364+
if runewidth.StringWidth(instance.Title) >= 32 {
364365
return m, m.handleError(fmt.Errorf("title cannot be longer than 32 characters"))
365366
}
366367
if err := instance.SetTitle(instance.Title + string(msg.Runes)); err != nil {
367368
return m, m.handleError(err)
368369
}
369370
case tea.KeyBackspace:
370-
if len(instance.Title) == 0 {
371+
runes := []rune(instance.Title)
372+
if len(runes) == 0 {
371373
return m, nil
372374
}
373-
if err := instance.SetTitle(instance.Title[:len(instance.Title)-1]); err != nil {
375+
if err := instance.SetTitle(string(runes[:len(runes)-1])); err != nil {
374376
return m, m.handleError(err)
375377
}
376378
case tea.KeySpace:

ui/err.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package ui
22

33
import (
4-
"github.com/charmbracelet/lipgloss"
54
"strings"
5+
6+
"github.com/charmbracelet/lipgloss"
7+
"github.com/mattn/go-runewidth"
68
)
79

810
type ErrBox struct {
@@ -38,8 +40,8 @@ func (e *ErrBox) String() string {
3840
err = e.err.Error()
3941
lines := strings.Split(err, "\n")
4042
err = strings.Join(lines, "//")
41-
if len(err) > e.width-3 && e.width-3 >= 0 {
42-
err = err[:e.width-3] + "..."
43+
if runewidth.StringWidth(err) > e.width-3 && e.width-3 >= 0 {
44+
err = runewidth.Truncate(err, e.width-3, "...")
4345
}
4446
}
4547
return lipgloss.Place(e.width, e.height, lipgloss.Center, lipgloss.Center, errStyle.Render(err))

ui/list.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/charmbracelet/bubbles/spinner"
1111
"github.com/charmbracelet/lipgloss"
12+
"github.com/mattn/go-runewidth"
1213
)
1314

1415
const readyIcon = "● "
@@ -139,9 +140,9 @@ func (r *InstanceRenderer) Render(i *session.Instance, idx int, selected bool, h
139140

140141
// Cut the title if it's too long
141142
titleText := i.Title
142-
widthAvail := r.width - 3 - len(prefix) - 1
143-
if widthAvail > 0 && widthAvail < len(titleText) && len(titleText) >= widthAvail-3 {
144-
titleText = titleText[:widthAvail-3] + "..."
143+
widthAvail := r.width - 3 - runewidth.StringWidth(prefix) - 1
144+
if widthAvail > 0 && runewidth.StringWidth(titleText) > widthAvail {
145+
titleText = runewidth.Truncate(titleText, widthAvail-3, "...")
145146
}
146147
title := titleS.Render(lipgloss.JoinHorizontal(
147148
lipgloss.Left,
@@ -171,10 +172,10 @@ func (r *InstanceRenderer) Render(i *session.Instance, idx int, selected bool, h
171172
}
172173

173174
remainingWidth := r.width
174-
remainingWidth -= len(prefix)
175-
remainingWidth -= len(branchIcon)
175+
remainingWidth -= runewidth.StringWidth(prefix)
176+
remainingWidth -= runewidth.StringWidth(branchIcon)
176177

177-
diffWidth := len(addedDiff) + len(removedDiff)
178+
diffWidth := runewidth.StringWidth(addedDiff) + runewidth.StringWidth(removedDiff)
178179
if diffWidth > 0 {
179180
diffWidth += 1
180181
}
@@ -192,17 +193,18 @@ func (r *InstanceRenderer) Render(i *session.Instance, idx int, selected bool, h
192193
}
193194
}
194195
// Don't show branch if there's no space for it. Or show ellipsis if it's too long.
196+
branchWidth := runewidth.StringWidth(branch)
195197
if remainingWidth < 0 {
196198
branch = ""
197-
} else if remainingWidth < len(branch) {
199+
} else if remainingWidth < branchWidth {
198200
if remainingWidth < 3 {
199201
branch = ""
200202
} else {
201203
// We know the remainingWidth is at least 4 and branch is longer than that, so this is safe.
202-
branch = branch[:remainingWidth-3] + "..."
204+
branch = runewidth.Truncate(branch, remainingWidth-3, "...")
203205
}
204206
}
205-
remainingWidth -= len(branch)
207+
remainingWidth -= runewidth.StringWidth(branch)
206208

207209
// Add spaces to fill the remaining width.
208210
spaces := ""

0 commit comments

Comments
 (0)