charmbracelet / charmbracelet/bubbles

viewport wrapping strings unnecessarily with UTF-8 characters

Open
#742 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
8.9k
Forks
457
Avg merge
1d 18h
Merged PRs (30d)
5

Description

**Describe the bug**
When a string has a character with a value larger than 0x7F, it gets encoded into multiple bytes in UTF-8 encoding. When using the viewport component of bubbles, text that should fit within the width of one screen gets wrapped to the next line I suspect because it's counting bytes rather than Runes.

**Setup**
macOS Sonoma
ghostty or iterm2

`locale`:

```text
LANG="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_CTYPE="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_ALL="en_US.UTF-8"
```

**To Reproduce**

1. Run wrap

```sh
go mod wrap
go build
./wrap
```
2. Change the width of the terminal. "B" should move to the next line only after the width of the terminal can't fit it on the same line.
3. Compare against behavior when string has no utf8 chars.

**Source Code**
main.go:
```go
package main

import (
"log"

"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
)

func main() {
p := tea.NewProgram(initialModel(), tea.WithAltScreen())

if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}

type (
errMsg error
)

type model struct {
viewport viewport.Model
messages []string
err error
}

func initialModel() model {
vp := viewport.New(30, 5)

// No UTF-8 Chars
// vp.SetContent(`A B`)

// UTF-8 Chars
vp.SetContent(`A                                                 B`)

return model{
messages: []string{},
viewport: vp,
err: nil,
}
}

func (m model) Init() tea.Cmd {
return nil
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
tiCmd tea.Cmd
vpCmd tea.Cmd
)

m.viewport, vpCmd = m.viewport.Update(msg)

switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.viewport.Width = msg.Width
m.viewport.Height = msg.Height

case tea.KeyMsg:
switch msg.Type {
case tea.KeyCtrlC, tea.KeyEsc:
return m, tea.Quit
}

// We handle errors just like any other message
case errMsg:
m.err = msg
return m, nil
}

return m, tea.Batch(tiCmd, vpCmd)
}

func (m model) View() string {
return m.viewport.View()
}
```

*MY* go.mod:
```text
module wrap

go 1.24.0

require (
github.com/charmbracelet/bubbles v0.20.0
github.com/charmbracelet/bubbletea v1.3.4
)

require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/lipgloss v1.0.0 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.3.8 // indirect
)
```

**Expected behavior**
Would expect text to fit on one line when the terminal is wide enough.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.