Cthulhu is no longer a fork. Now it is a screen reader written in Golang.

This commit is contained in:
Storm Dragon committed 2026-10-14 00:16:34 -04:00
1 parent a998fe67de
commit 069fc674dc
708 files changed
+112482 -532

No files matched your search

+91
View File
@@ -0,0 +1,91 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"strings"
"unicode/utf8"
"git.stormux.org/storm/cthulhu/internal/atspi"
)
// textSelection tracks only the focused text object. Its cached content is
// bounded by the native snapshot and is retired when focus changes.
type textSelection struct {
id atspi.ObjectID
start, end int
text string
}
const selectionFragmentLimit = 120
const lineSelectionFragmentLimit = 4096
func (selection *textSelection) reset(snapshot atspi.Snapshot) {
selection.id = snapshot.ID()
selection.start, selection.end = snapshot.SelectionStart, snapshot.SelectionEnd
selection.text = snapshot.SelectionText
}
func (selection *textSelection) changed(snapshot atspi.Snapshot) string {
return selection.changedWithLimit(snapshot, selectionFragmentLimit)
}
func (selection *textSelection) changedWithLimit(snapshot atspi.Snapshot, fragmentLimit int) string {
start, end := snapshot.SelectionStart, snapshot.SelectionEnd
if selection.id != snapshot.ID() {
selection.reset(snapshot)
return ""
}
oldStart, oldEnd, oldText := selection.start, selection.end, selection.text
selection.reset(snapshot)
if start == oldStart && end == oldEnd && oldText == snapshot.SelectionText {
return ""
}
if end <= start {
if oldEnd > oldStart {
return "selection removed"
}
return ""
}
if oldEnd <= oldStart {
return selectionFragment(snapshot.SelectionText, "selected", fragmentLimit)
}
var changes []string
if start > oldStart {
changes = append(changes, selectionFragment(selectionSlice(oldText, 0, min(oldEnd, start)-oldStart), "unselected", fragmentLimit))
}
if end < oldEnd {
changes = append(changes, selectionFragment(selectionSlice(oldText, max(oldStart, end)-oldStart, oldEnd-oldStart), "unselected", fragmentLimit))
}
if start < oldStart {
changes = append(changes, selectionFragment(selectionSlice(snapshot.SelectionText, 0, min(end, oldStart)-start), "selected", fragmentLimit))
}
if end > oldEnd {
changes = append(changes, selectionFragment(selectionSlice(snapshot.SelectionText, max(start, oldEnd)-start, end-start), "selected", fragmentLimit))
}
if len(changes) == 0 {
return "selection changed"
}
return strings.Join(changes, ", ")
}
func selectionSlice(value string, start, end int) string {
runes := []rune(value)
if start < 0 || end > len(runes) || start >= end {
return ""
}
return string(runes[start:end])
}
func selectionFragment(value, change string, limit int) string {
if utf8.RuneCountInString(value) > limit {
return change
}
value = strings.Join(strings.Fields(value), " ")
if value == "" {
return change
}
return value + " " + change
}