168 lines
4.5 KiB
Go
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
|
|
}
|