Files
terminal/tui/textfield_state.go
T
2026-07-12 17:01:37 -04:00

280 lines
5.8 KiB
Go

package tui
import (
"unicode"
"github.com/lixenwraith/terminal"
)
// isWordChar returns true for word-constituent characters
func isWordChar(r rune) bool {
return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_'
}
// TextFieldState holds editable text field state
type TextFieldState struct {
Text []rune
Cursor int // Positions before which cursor sits (0 = before first char)
Scroll int // First visible rune index
}
// NewTextFieldState creates initialized text field state
func NewTextFieldState(initial string) *TextFieldState {
runes := []rune(initial)
return &TextFieldState{
Text: runes,
Cursor: len(runes),
Scroll: 0,
}
}
// --- Value access ---
// Value returns current text as string
func (t *TextFieldState) Value() string {
return string(t.Text)
}
// SetValue replaces text and moves cursor to end
func (t *TextFieldState) SetValue(s string) {
t.Text = []rune(s)
t.Cursor = len(t.Text)
t.Scroll = 0
}
// Clear empties the field
func (t *TextFieldState) Clear() {
t.Text = nil
t.Cursor = 0
t.Scroll = 0
}
// --- Character insertion ---
// Insert adds rune at cursor position
func (t *TextFieldState) Insert(r rune) {
t.Text = append(t.Text[:t.Cursor], append([]rune{r}, t.Text[t.Cursor:]...)...)
t.Cursor++
}
// InsertString adds string at cursor position
func (t *TextFieldState) InsertString(s string) {
runes := []rune(s)
t.Text = append(t.Text[:t.Cursor], append(runes, t.Text[t.Cursor:]...)...)
t.Cursor += len(runes)
}
// --- Character deletion ---
// DeleteBackward removes rune before cursor
func (t *TextFieldState) DeleteBackward() bool {
if t.Cursor > 0 {
t.Text = append(t.Text[:t.Cursor-1], t.Text[t.Cursor:]...)
t.Cursor--
return true
}
return false
}
// DeleteForward removes rune at cursor
func (t *TextFieldState) DeleteForward() bool {
if t.Cursor < len(t.Text) {
t.Text = append(t.Text[:t.Cursor], t.Text[t.Cursor+1:]...)
return true
}
return false
}
// --- Word deletion ---
// DeleteWordBackward removes word before cursor
func (t *TextFieldState) DeleteWordBackward() bool {
if t.Cursor == 0 {
return false
}
// Skip trailing non-word chars
end := t.Cursor
for end > 0 && !isWordChar(t.Text[end-1]) {
end--
}
// Skip word chars
start := end
for start > 0 && isWordChar(t.Text[start-1]) {
start--
}
if start == t.Cursor {
start = t.Cursor - 1
}
t.Text = append(t.Text[:start], t.Text[t.Cursor:]...)
t.Cursor = start
return true
}
// DeleteWordForward removes word after cursor
func (t *TextFieldState) DeleteWordForward() bool {
if t.Cursor >= len(t.Text) {
return false
}
// Skip word chars
end := t.Cursor
for end < len(t.Text) && isWordChar(t.Text[end]) {
end++
}
// Skip trailing non-word chars
for end < len(t.Text) && !isWordChar(t.Text[end]) {
end++
}
if end == t.Cursor {
end = t.Cursor + 1
}
t.Text = append(t.Text[:t.Cursor], t.Text[end:]...)
return true
}
// DeleteToEnd removes from cursor to end
func (t *TextFieldState) DeleteToEnd() bool {
if t.Cursor < len(t.Text) {
t.Text = t.Text[:t.Cursor]
return true
}
return false
}
// DeleteToStart removes from start to cursor
func (t *TextFieldState) DeleteToStart() bool {
if t.Cursor > 0 {
t.Text = t.Text[t.Cursor:]
t.Cursor = 0
t.Scroll = 0
return true
}
return false
}
// --- Character movement ---
// MoveLeft moves cursor left
func (t *TextFieldState) MoveLeft() {
if t.Cursor > 0 {
t.Cursor--
}
}
// MoveRight moves cursor right
func (t *TextFieldState) MoveRight() {
if t.Cursor < len(t.Text) {
t.Cursor++
}
}
// --- Word movement ---
// MoveWordLeft moves cursor to previous word boundary
func (t *TextFieldState) MoveWordLeft() {
if t.Cursor == 0 {
return
}
// Skip non-word chars
for t.Cursor > 0 && !isWordChar(t.Text[t.Cursor-1]) {
t.Cursor--
}
// Skip word chars
for t.Cursor > 0 && isWordChar(t.Text[t.Cursor-1]) {
t.Cursor--
}
}
// MoveWordRight moves cursor to next word boundary
func (t *TextFieldState) MoveWordRight() {
if t.Cursor >= len(t.Text) {
return
}
// Skip word chars
for t.Cursor < len(t.Text) && isWordChar(t.Text[t.Cursor]) {
t.Cursor++
}
// Skip non-word chars
for t.Cursor < len(t.Text) && !isWordChar(t.Text[t.Cursor]) {
t.Cursor++
}
}
// --- Line movement ---
// MoveToStart moves cursor to beginning
func (t *TextFieldState) MoveToStart() {
t.Cursor = 0
}
// MoveToEnd moves cursor to end
func (t *TextFieldState) MoveToEnd() {
t.Cursor = len(t.Text)
}
// --- Scroll management ---
// AdjustScroll updates scroll to keep cursor visible within viewport width
func (t *TextFieldState) AdjustScroll(viewportW int) {
if viewportW <= 0 {
return
}
if t.Cursor < t.Scroll {
t.Scroll = t.Cursor
}
if t.Cursor >= t.Scroll+viewportW {
t.Scroll = t.Cursor - viewportW + 1
}
if t.Scroll < 0 {
t.Scroll = 0
}
}
// --- Input handling ---
// HandleKey processes keyboard input, returns true if state changed
func (t *TextFieldState) HandleKey(key terminal.Key, r rune, mod terminal.Modifier) bool {
switch key {
case terminal.KeyLeft:
if mod&terminal.ModCtrl != 0 {
t.MoveWordLeft()
} else {
t.MoveLeft()
}
return true
case terminal.KeyRight:
if mod&terminal.ModCtrl != 0 {
t.MoveWordRight()
} else {
t.MoveRight()
}
return true
case terminal.KeyHome, terminal.KeyCtrlA:
t.MoveToStart()
return true
case terminal.KeyEnd, terminal.KeyCtrlE:
t.MoveToEnd()
return true
case terminal.KeyBackspace:
if mod&terminal.ModCtrl != 0 {
return t.DeleteWordBackward()
}
return t.DeleteBackward()
case terminal.KeyDelete:
if mod&terminal.ModCtrl != 0 {
return t.DeleteWordForward()
}
return t.DeleteForward()
case terminal.KeyCtrlK:
return t.DeleteToEnd()
case terminal.KeyCtrlU:
return t.DeleteToStart()
case terminal.KeyCtrlW:
return t.DeleteWordBackward()
case terminal.KeyRune:
if r >= 32 { // Printable
t.Insert(r)
return true
}
}
return false
}