Files

144 lines
4.1 KiB
Go

// Cthulhu screen reader.
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
package presentation
import (
"strings"
"unicode"
"unicode/utf8"
"git.stormux.org/storm/cthulhu/internal/atspi"
)
func TypingEchoText(event atspi.Event, previousText string, options Options) string {
text, _ := TypingEcho(event, previousText, options)
return text
}
func TypingEcho(event atspi.Event, previousText string, options Options) (string, string) {
kind := ""
if event.Type == atspi.TextInsertedEvent && utf8.RuneCountInString(event.Payload) == 1 {
kind = "key"
if options.EnableEchoByCharacter {
kind = "character"
}
}
characterEcho := EventTextWithOptions(event, options)
if event.Type != atspi.TextInsertedEvent || !options.EnableKeyEcho || utf8.RuneCountInString(event.Payload) != 1 {
return characterEcho, kind
}
context := event.Source.TextBeforeCaret
if context == "" {
context = event.Source.Text
}
if (event.Payload == "\n" || event.Payload == "\t") && strings.TrimSpace(context) == "" {
context = previousText
}
if options.EnableEchoBySentence {
if sentence := completedSentence(context, event.Payload); sentence != "" {
return sentence, "sentence"
}
}
if options.EnableEchoByWord {
if word := completedWord(context, event.Payload); word != "" {
return word, "word"
}
}
return characterEcho, kind
}
func completedWord(context, inserted string) string {
if !isWordDelimiter(inserted) {
return ""
}
context = strings.TrimSuffix(context, inserted)
characters := []rune(context)
if len(characters) == 0 || isWordDelimiter(string(characters[len(characters)-1])) {
return ""
}
start := len(characters) - 1
for start >= 0 && !isWordDelimiter(string(characters[start])) {
start--
}
return string(characters[start+1:])
}
func completedSentence(context, inserted string) string {
context = strings.TrimSuffix(context, inserted)
characters := []rune(context)
if len(characters) == 0 {
return ""
}
if inserted != "\n" && inserted != "\r" {
if !unicode.IsSpace(firstRune(inserted)) || !strings.ContainsRune("!.?:;", characters[len(characters)-1]) {
return ""
}
}
start := 0
for index := 1; index < len(characters); index++ {
if characters[index] == '\n' || characters[index] == '\r' ||
unicode.IsSpace(characters[index]) && strings.ContainsRune("!.?:;", characters[index-1]) {
start = index + 1
}
}
return strings.TrimSpace(string(characters[start:]))
}
func isWordDelimiter(value string) bool {
if utf8.RuneCountInString(value) != 1 {
return false
}
r := firstRune(value)
return unicode.IsSpace(r) || strings.ContainsRune("!*+,-./:;<=>?@[\\]^_{|}", r) || r == '\u00a0'
}
func firstRune(value string) rune {
r, _ := utf8.DecodeRuneInString(value)
return r
}
func shouldEchoInsertion(text string, options Options) bool {
if text == "" {
return false
}
if utf8.RuneCountInString(text) != 1 {
return true
}
if !options.EnableKeyEcho {
return false
}
r, _ := utf8.DecodeRuneInString(text)
if options.EnableEchoByCharacter && unicode.IsPrint(r) {
return true
}
switch {
case unicode.IsLetter(r):
return options.EnableAlphabeticKeys
case unicode.IsNumber(r):
return options.EnableNumericKeys
case unicode.IsPunct(r) || unicode.IsSymbol(r):
return options.EnablePunctuationKeys
case r == ' ':
return options.EnableSpace
case unicode.Is(unicode.Mn, r) || unicode.Is(unicode.Mc, r) || unicode.Is(unicode.Me, r):
return options.EnableDiacriticalKeys
default:
return false
}
}