package tui import ( "github.com/rivo/uniseg" "zgo.at/termfo/keys" "strings" "unicode" ) type TextInput struct { id string beforeCursor string selection string selectionBefore bool afterCursor string Private bool } var selectedStyle = Style {Bg: Blue, Fg: White, selected: true} func (t *TextInput) Text() string { return t.beforeCursor + t.selection + t.afterCursor } func (t *TextInput) SetText(text string) { t.beforeCursor = text t.afterCursor = "" t.selection = "" } func toGraphemes(s string) []string { g := uniseg.NewGraphemes(s) var result []string for g.Next() { result = append(result, g.Str()) } return result } func splitRight(s string, word bool) (string, string) { if s == "" { return "", "" } gs := toGraphemes(s) if !word { return strings.Join(gs[:len(gs) - 1], ""), gs[len(gs) - 1] } else { i := len(gs) for unicode.IsSpace([]rune(gs[i - 1])[0]) { i-- if i < 0 { return "", s } } for !unicode.IsSpace([]rune(gs[i - 1])[0]) { i-- if i < 1 { break } } return strings.Join(gs[:i], ""), strings.Join(gs[i:], "") } } func splitLeft(s string, word bool) (string, string) { if s == "" { return "", "" } gs := toGraphemes(s) if !word { return gs[0], strings.Join(gs[1:], "") } else { i := 0 for unicode.IsSpace([]rune(gs[i])[0]) { i++ if i >= len(gs) { return "", s } } for !unicode.IsSpace([]rune(gs[i])[0]) { i++ if i >= len(gs) { break } } return strings.Join(gs[:i], ""), strings.Join(gs[i:], "") } } func (t *TextInput) Left(selection bool, word bool) { if !selection { t.Deselect() } if selection && t.selection == "" { t.selectionBefore = false } var right string if selection && t.selectionBefore { t.selection, right = splitRight(t.selection, word) } else { t.beforeCursor, right = splitRight(t.beforeCursor, word) } if !selection || t.selectionBefore { t.afterCursor = right + t.afterCursor } else { t.selection = right + t.selection } } func (t *TextInput) Right(selection bool, word bool) { if !selection { t.Deselect() } if selection && t.selection == "" { t.selectionBefore = true } var left string if selection && !t.selectionBefore { left, t.selection = splitLeft(t.selection, word) } else { left, t.afterCursor = splitLeft(t.afterCursor, word) } if !selection || !t.selectionBefore { t.beforeCursor += left } else { t.selection += left } } func (t *TextInput) Start(selection bool) { if !selection { t.Deselect() } if selection { t.selection = t.beforeCursor + t.selection t.selectionBefore = false } else { t.afterCursor = t.beforeCursor + t.afterCursor } t.beforeCursor = "" } func (t *TextInput) End(selection bool) { if !selection { t.Deselect() } if selection { t.selection = t.selection + t.afterCursor t.selectionBefore = true } else { t.beforeCursor = t.beforeCursor + t.afterCursor } t.afterCursor = "" } func (t *TextInput) Selection() string { return t.selection } func (t *TextInput) Deselect() { if t.selectionBefore { t.beforeCursor += t.selection } else { t.afterCursor = t.selection + t.afterCursor } t.selection = "" } func (t *TextInput) Write(text string) { t.selection = "" t.beforeCursor += text } func (t *TextInput) IsEmpty() bool { return t.beforeCursor == "" && t.selection == "" && t.afterCursor == "" } func (t *TextInput) Update(ev Event) (usedKeybind bool) { if Selected != t.id { t.Deselect() return } if ev.TextInput != 0 { t.Write(string(ev.TextInput)) } if ev.Key == keys.Tab { t.Write("\t") } selection := ev.Key & keys.Shift != 0 word := ev.Key & keys.Ctrl != 0 if ev.Key & keys.Alt != 0 { selection = true word = true } switch ev.Key.WithoutMods() { case keys.Left: t.Left(selection, word) case keys.Right: t.Right(selection, word) case 'a': if ev.Key & (keys.Ctrl | keys.Alt) != 0 { t.Start(selection) } case 'e': if ev.Key & (keys.Ctrl | keys.Alt) != 0 { t.End(selection) } case 'h': if ev.Key & keys.Ctrl == 0 { break } word = false fallthrough case keys.Backspace: if t.selection != "" { t.selection = "" } else { t.beforeCursor, _ = splitRight(t.beforeCursor, word) } case 'j': if ev.Key & keys.Ctrl != 0 { t.Write("\n") } default: return false } return true } func (t *TextInput) showText(text string, style *Style) { if !t.Private { Text(text, style) } else { Text(strings.Repeat("*", len([]rune(text))), style) } } func (t *TextInput) Show(id string) { t.id = id Push(id, Box {Width: Fill, Height: TextSize}) t.showText(t.beforeCursor, nil) if t.selectionBefore { t.showText(t.selection, &selectedStyle) } if t.selection == "" { Cursor() } if !t.selectionBefore { t.showText(t.selection, &selectedStyle) } t.showText(t.afterCursor, nil) Pop() }