Files
T

168 lines
4.5 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package internalui
import (
"strconv"
"strings"
"unicode"
"unicode/utf8"
)
func (s *Session) characterCount(n int) string {
if n == 1 {
return s.messages.T("1 character")
}
return s.messages.Format("{0} characters", strconv.Itoa(n))
}
func (s *Session) edit(control *Control, key Key) Result {
if key.Name == "enter" {
return Result{Action: control.Action}
}
characters := []rune(control.Text)
start, end := min(s.cursor, s.anchor), max(s.cursor, s.anchor)
if key.Ctrl && key.Name == "" {
switch strings.ToLower(key.Text) {
case "a":
s.anchor, s.cursor = 0, len(characters)
output := s.Presentation()
output.Speech = s.messages.T("Selected all")
return output
case "v":
return Result{Paste: true}
case "c", "x":
if control.Secret {
output := s.Presentation()
output.Speech = s.messages.T("Copy and cut are unavailable for password fields")
return output
}
if start == end {
return Result{}
}
text := string(characters[start:end])
// Cutting is completed by the host only after clipboard write succeeds.
return Result{Copy: &text}
default:
return Result{}
}
}
if key.Name == "left" || key.Name == "right" || key.Name == "home" || key.Name == "end" {
next := s.cursor
switch key.Name {
case "left":
if !key.Shift && start != end {
next = start
} else {
next = max(0, next-1)
if key.Ctrl {
for next > 0 && next < len(characters) && unicode.IsSpace(characters[next]) {
next--
}
for next > 0 && !unicode.IsSpace(characters[next-1]) {
next--
}
}
}
case "right":
if !key.Shift && start != end {
next = end
} else {
next = min(len(characters), next+1)
if key.Ctrl {
for next < len(characters) && !unicode.IsSpace(characters[next]) {
next++
}
for next < len(characters) && unicode.IsSpace(characters[next]) {
next++
}
}
}
case "home":
next = 0
case "end":
next = len(characters)
}
s.cursor = next
if !key.Shift {
s.anchor = next
}
output := s.Presentation()
if next == len(characters) {
output.Speech = s.messages.T("End of text")
} else if control.Secret {
output.Speech = s.messages.T("Hidden character")
} else {
output.Speech = string(characters[next])
}
if key.Shift {
output.Speech = s.messages.Format("{0}, selected", output.Speech)
}
return output
}
if key.Name == "backspace" || key.Name == "delete" {
if start == end {
if key.Name == "backspace" {
start = max(0, start-1)
} else {
end = min(len(characters), end+1)
}
}
if start == end {
return Result{}
}
removed := string(characters[start:end])
control.Text = string(append(characters[:start], characters[end:]...))
s.cursor, s.anchor = start, start
output := s.Presentation()
if control.Secret {
output.Speech = s.messages.Format("Deleted {0}", s.characterCount(end-start))
} else {
output.Speech = s.messages.Format("Deleted {0}", removed)
}
return output
}
if (key.Name == "" || key.Name == "space") && key.Text != "" && !key.Ctrl {
return s.Insert(key.Text)
}
return Result{}
}
// Insert handles typing and paste atomically; invalid/oversized input leaves
// the field and selection untouched.
func (s *Session) Insert(text string) Result {
if s.closed {
return Result{}
}
if s.focus < 0 {
return Result{}
}
control := &s.form.Tabs[s.tab].Controls[s.focus]
if control.Kind != Entry {
return Result{}
}
if !utf8.ValidString(text) {
return Result{Speech: s.messages.T("Invalid text"), Braille: s.messages.T("Invalid text"), Cursor: -1}
}
characters := []rune(control.Text)
start, end := min(s.cursor, s.anchor), max(s.cursor, s.anchor)
inserted := []rune(text)
for _, character := range inserted {
if unicode.IsControl(character) {
return Result{Speech: s.messages.T("Only single-line text is accepted"), Braille: s.messages.T("Only single-line text is accepted"), Cursor: -1}
}
}
if len(characters)-(end-start)+len(inserted) > control.MaxLength {
return Result{Speech: s.messages.T("Text limit reached"), Braille: s.messages.T("Text limit reached"), Cursor: -1}
}
control.Text = string(characters[:start]) + text + string(characters[end:])
s.cursor, s.anchor = start+len(inserted), start+len(inserted)
output := s.Presentation()
if control.Secret {
output.Speech = s.messages.Format("Inserted {0}", s.characterCount(len(inserted)))
} else {
output.Speech = text
}
return output
}