Describe the bug
textinput.Model.Cursor() computes the cursor X offset using m.Position(), which returns a rune index. For wide characters (CJK, fullwidth, etc.) that occupy 2 terminal columns, this results in the cursor being placed at the wrong screen position.
In contrast, textarea.Model.Cursor() correctly uses lineInfo.CharOffset, which is computed via uniseg.StringWidth (display width).
// textinput.go — current (buggy)
func (m Model) Cursor() *tea.Cursor {
// ...
xOffset := m.Position() + promptWidth // m.Position() = rune index, not display width
// ...
}
// textarea.go — correct
func (m Model) Cursor() *tea.Cursor {
// ...
lineInfo := m.LineInfo()
xOffset := lineInfo.CharOffset + ... // CharOffset = uniseg.StringWidth(...)
// ...
}
For example, with the value "あいう" (3 CJK runes, 6 display columns):
m.Position() returns 3 (rune count)
- Correct cursor X should be
6 (display columns)
- The cursor is placed 3 columns too far to the left
This bug was introduced in v2 with the real cursor API. v1 only had virtual cursors (rendered inline in View()), which are not affected.
Setup
- OS: macOS
- Shell: zsh
- Terminal Emulator: WezTerm
- Locale: en_US.UTF-8
To Reproduce
- Create a
textinput.Model with SetVirtualCursor(false)
- Type CJK characters (e.g. Japanese hiragana "あいう")
- Observe that the terminal cursor position does not match the end of the typed text
Source Code
package main
import (
"fmt"
"os"
"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
)
type model struct {
ti textinput.Model
}
func initialModel() model {
ti := textinput.New()
ti.Placeholder = "Type CJK text here"
ti.SetVirtualCursor(false)
ti.Focus()
ti.SetWidth(40)
return model{ti: ti}
}
func (m model) Init() tea.Cmd { return nil }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyPressMsg:
if msg.String() == "ctrl+c" {
return m, tea.Quit
}
}
var cmd tea.Cmd
m.ti, cmd = m.ti.Update(msg)
return m, cmd
}
func (m model) View() tea.View {
var v tea.View
v.SetContent(fmt.Sprintf("Input: %s\n\nPress Ctrl+C to quit.", m.ti.View()))
if c := m.ti.Cursor(); c != nil {
promptW := len([]rune(m.ti.Prompt))
c.X += len("Input: ") - promptW
v.Cursor = c
}
return v
}
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Expected behavior
The cursor X position returned by Cursor() should reflect the display width (terminal columns) of the text before the cursor, not the rune count. The fix would be to use display-width-aware calculation, similar to what textarea.Cursor() already does:
// Suggested fix
xOffset := uniseg.StringWidth(string(m.value[m.offset:m.pos])) + promptWidth
Additional context
textarea.Cursor() handles this correctly using lineInfo.CharOffset (computed with uniseg.StringWidth)
- The virtual cursor mode (
SetVirtualCursor(true)) is not affected since it renders the cursor inline in View()
- Only the real cursor mode (
SetVirtualCursor(false)) is affected
- Bubbles version: v2.0.0 (latest as of writing, also present on master at
363089b)
Describe the bug
textinput.Model.Cursor()computes the cursor X offset usingm.Position(), which returns a rune index. For wide characters (CJK, fullwidth, etc.) that occupy 2 terminal columns, this results in the cursor being placed at the wrong screen position.In contrast,
textarea.Model.Cursor()correctly useslineInfo.CharOffset, which is computed viauniseg.StringWidth(display width).For example, with the value "あいう" (3 CJK runes, 6 display columns):
m.Position()returns3(rune count)6(display columns)This bug was introduced in v2 with the real cursor API. v1 only had virtual cursors (rendered inline in
View()), which are not affected.Setup
To Reproduce
textinput.ModelwithSetVirtualCursor(false)Source Code
Expected behavior
The cursor X position returned by
Cursor()should reflect the display width (terminal columns) of the text before the cursor, not the rune count. The fix would be to use display-width-aware calculation, similar to whattextarea.Cursor()already does:Additional context
textarea.Cursor()handles this correctly usinglineInfo.CharOffset(computed withuniseg.StringWidth)SetVirtualCursor(true)) is not affected since it renders the cursor inline inView()SetVirtualCursor(false)) is affected363089b)