144 lines
4.1 KiB
Go
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
|
|
}
|
|
}
|