280 lines
5.8 KiB
Go
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
|
|
} |