v0.1.0 initial commit
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user