Keep terminal textbox cursor on rune boundaries
This commit is contained in:
committed by
Brandon McGinty
parent
5bf3ed8b84
commit
2bdf9a8193
@@ -238,7 +238,7 @@ Priority 3: configuration, UI, and binding hardening
|
|||||||
fails, hiding startup failure. Return that error. Also stop/join the
|
fails, hiding startup failure. Return that error. Also stop/join the
|
||||||
PollEvent goroutine on UI shutdown and make Close nonblocking/idempotent.
|
PollEvent goroutine on UI shutdown and make Close nonblocking/idempotent.
|
||||||
|
|
||||||
31. Text UI is not Unicode-safe and timestamp parsing is fragile
|
[x] 31. Text UI is not Unicode-safe and timestamp parsing is fragile
|
||||||
Files: uiterm/textbox.go, uiterm/textview.go
|
Files: uiterm/textbox.go, uiterm/textview.go
|
||||||
Textbox cursor positions are byte offsets but editing/display iterates
|
Textbox cursor positions are byte offsets but editing/display iterates
|
||||||
runes, so non-ASCII input can be split into invalid UTF-8. Textview assumes
|
runes, so non-ASCII input can be split into invalid UTF-8. Textview assumes
|
||||||
|
|||||||
+19
-7
@@ -2,7 +2,7 @@ package uiterm
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
// "unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/nsf/termbox-go"
|
"github.com/nsf/termbox-go"
|
||||||
)
|
)
|
||||||
@@ -41,6 +41,9 @@ func (t *Textbox) uiSetBounds(x0, y0, x1, y1 int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Textbox) uiDraw() {
|
func (t *Textbox) uiDraw() {
|
||||||
|
if t.ui == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
t.ui.beginDraw()
|
t.ui.beginDraw()
|
||||||
defer t.ui.endDraw()
|
defer t.ui.endDraw()
|
||||||
|
|
||||||
@@ -51,6 +54,9 @@ func (t *Textbox) uiDraw() {
|
|||||||
if t.pos > len(t.Text) {
|
if t.pos > len(t.Text) {
|
||||||
t.pos = len(t.Text)
|
t.pos = len(t.Text)
|
||||||
}
|
}
|
||||||
|
for t.pos > 0 && t.pos < len(t.Text) && !utf8.RuneStart(t.Text[t.pos]) {
|
||||||
|
t.pos--
|
||||||
|
}
|
||||||
for y := t.y0; y < t.y1; y++ {
|
for y := t.y0; y < t.y1; y++ {
|
||||||
for x := t.x0; x < t.x1; x++ {
|
for x := t.x0; x < t.x1; x++ {
|
||||||
var chr rune
|
var chr rune
|
||||||
@@ -95,10 +101,16 @@ func (t *Textbox) uiKeyEvent(key Key) {
|
|||||||
t.pos = len(t.Text)
|
t.pos = len(t.Text)
|
||||||
redraw = true
|
redraw = true
|
||||||
case KeyArrowLeft:
|
case KeyArrowLeft:
|
||||||
t.pos -= 1
|
if t.pos > 0 {
|
||||||
|
_, size := utf8.DecodeLastRuneInString(t.Text[:t.pos])
|
||||||
|
t.pos -= size
|
||||||
|
}
|
||||||
redraw = true
|
redraw = true
|
||||||
case KeyArrowRight:
|
case KeyArrowRight:
|
||||||
t.pos += 1
|
if t.pos < len(t.Text) {
|
||||||
|
_, size := utf8.DecodeRuneInString(t.Text[t.pos:])
|
||||||
|
t.pos += size
|
||||||
|
}
|
||||||
redraw = true
|
redraw = true
|
||||||
case KeyCtrlC:
|
case KeyCtrlC:
|
||||||
t.Text = ""
|
t.Text = ""
|
||||||
@@ -119,12 +131,12 @@ func (t *Textbox) uiKeyEvent(key Key) {
|
|||||||
redraw = t.handleHistoryKey(key)
|
redraw = t.handleHistoryKey(key)
|
||||||
case KeySpace:
|
case KeySpace:
|
||||||
t.uiCharacterEvent(' ')
|
t.uiCharacterEvent(' ')
|
||||||
case KeyBackspace:
|
case KeyBackspace, KeyBackspace2:
|
||||||
case KeyBackspace2:
|
|
||||||
if len(t.Text) > 0 {
|
if len(t.Text) > 0 {
|
||||||
if t.pos > 0 {
|
if t.pos > 0 {
|
||||||
t.Text = t.Text[:t.pos-1] + t.Text[t.pos:]
|
_, size := utf8.DecodeLastRuneInString(t.Text[:t.pos])
|
||||||
t.pos -= 1
|
t.Text = t.Text[:t.pos-size] + t.Text[t.pos:]
|
||||||
|
t.pos -= size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if r, size := utf8.DecodeLastRuneInString(t.Text); r != utf8.RuneError {
|
// if r, size := utf8.DecodeLastRuneInString(t.Text); r != utf8.RuneError {
|
||||||
|
|||||||
@@ -2,6 +2,24 @@ package uiterm
|
|||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
|
// Regression: byte-based cursor movement split UTF-8 input, producing invalid
|
||||||
|
// text when deleting or inserting beside a non-ASCII character.
|
||||||
|
func TestTextboxEditsAtRuneBoundaries(t *testing.T) {
|
||||||
|
textbox := Textbox{Text: "aé", pos: len("aé")}
|
||||||
|
textbox.uiKeyEvent(KeyArrowLeft)
|
||||||
|
if textbox.pos != 1 {
|
||||||
|
t.Fatalf("cursor = %d, want rune boundary 1", textbox.pos)
|
||||||
|
}
|
||||||
|
textbox.uiKeyEvent(KeyBackspace)
|
||||||
|
if textbox.Text != "é" || textbox.pos != 0 {
|
||||||
|
t.Fatalf("after delete: %q at %d", textbox.Text, textbox.pos)
|
||||||
|
}
|
||||||
|
textbox.uiCharacterEvent('ß')
|
||||||
|
if textbox.Text != "ßé" {
|
||||||
|
t.Fatalf("insert produced %q", textbox.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTextboxHistoryNavigatesSubmittedText(t *testing.T) {
|
func TestTextboxHistoryNavigatesSubmittedText(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user