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

+121
View File
@@ -0,0 +1,121 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package review
import (
"context"
"git.stormux.org/storm/cthulhu/internal/atspi"
)
// Native selections can report their caret on a label/paragraph rather than
// its static text child. Transfer offsets only when both expose the identical
// text range; embedded or ambiguous child offsets must never be guessed.
func (cursor *DocumentCursor) syncNativeCaret(ctx context.Context, reader DocumentCaretReader, focus atspi.ObjectID, offset int) error {
if offset < 0 {
return atspi.ErrNoDocumentCaret
}
if _, err := cursor.SyncFocus(ctx, focus); err != nil {
return err
}
cursor.mu.Lock()
defer cursor.mu.Unlock()
place := &cursor.places[len(cursor.places)-1]
doc, err := cursor.tree.Document(ctx, place.focus)
if err != nil {
return err
}
node, err := native_caret_node(ctx, reader, doc, focus, offset)
if err != nil {
return err
}
place.object, place.offset, place.origin = node.Reference, offset, CaretNavigation
place.tableCell = atspi.ObjectID{}
return nil
}
func native_caret_node(ctx context.Context, reader DocumentCaretReader, doc atspi.DocumentSnapshot, focus atspi.ObjectID, offset int) (atspi.DocumentNode, error) {
nodes := make(map[atspi.ObjectID]atspi.DocumentNode, len(doc.Nodes))
for _, node := range doc.Nodes {
nodes[node.Reference.ID()] = node
}
node := nodes[focus]
if !node.Reference.Valid() || offset < 0 || offset > node.Snapshot.CharacterCount || node.Snapshot.Editable || node.Snapshot.Role == "password text" {
return atspi.DocumentNode{}, atspi.ErrNoDocumentCaret
}
for len(node.Children) != 0 && !wholeCaretNode(node.Snapshot) {
if len(node.Children) != 1 {
return atspi.DocumentNode{}, atspi.ErrNoDocumentCaret
}
child, ok := nodes[node.Children[0]]
if !ok || child.Hidden || child.Snapshot.Editable || child.Snapshot.CharacterCount != node.Snapshot.CharacterCount {
return atspi.DocumentNode{}, atspi.ErrNoDocumentCaret
}
probeOffset := max(0, min(offset, node.Snapshot.CharacterCount-1))
parentText, err := reader.ReadDocumentCaret(ctx, node.Reference, probeOffset, atspi.DocumentCaretBrailleLine)
if err != nil {
return atspi.DocumentNode{}, err
}
childText, err := reader.ReadDocumentCaret(ctx, child.Reference, probeOffset, atspi.DocumentCaretBrailleLine)
if err != nil {
return atspi.DocumentNode{}, err
}
if parentText.Start != childText.Start || parentText.End != childText.End || parentText.Text == "" || parentText.Text != childText.Text {
return atspi.DocumentNode{}, atspi.ErrNoDocumentCaret
}
node = child
}
return node, nil
}
// Use the navigation FIFO so a subsequent intercepted arrow sees the native
// selection endpoint, even if capturing the document is still in progress.
func (worker *Worker) SyncDocumentCaret(focus atspi.ObjectID, offset int) error {
if !focus.Valid() || offset < 0 {
return atspi.ErrNoDocumentCaret
}
select {
case worker.requests <- request{documentEpoch: worker.documentEpoch.Load(), nativeCaret: true, nativeCaretFocus: focus, nativeCaretOffset: offset}:
return nil
default:
return ErrRequestQueueFull
}
}
func (worker *Worker) syncDocumentCaret(ctx context.Context, cursor *DocumentCursor, request request) (Result, *DocumentCursor) {
result := Result{DocumentNativeCaret: true, DocumentEpoch: request.documentEpoch, DocumentFocus: request.nativeCaretFocus}
if worker.focus() != request.nativeCaretFocus {
result.Err = atspi.ErrStaleObject
return result, cursor
}
reader, ok := worker.nativeCapturerFor(request.nativeCaretFocus).(DocumentCaretReader)
if !ok {
result.Err = atspi.ErrNoDocumentCaret
return result, cursor
}
ctx, cancel := context.WithTimeout(ctx, worker.captureTimeout)
defer cancel()
if cursor == nil {
cursor, result.Err = worker.refreshDocumentCursor(ctx, nil, request.nativeCaretFocus)
if result.Err != nil {
return result, cursor
}
}
result.Err = cursor.syncNativeCaret(ctx, reader, request.nativeCaretFocus, request.nativeCaretOffset)
if documentNeedsCapture(result.Err) {
fresh, captureErr := worker.refreshDocumentCursor(ctx, cursor, request.nativeCaretFocus)
if captureErr == nil && worker.focus() == request.nativeCaretFocus {
cursor = fresh
result.Err = cursor.syncNativeCaret(ctx, reader, request.nativeCaretFocus, request.nativeCaretOffset)
} else if captureErr != nil {
result.Err = captureErr
}
}
if result.Err == nil {
result.DocumentPosition, result.Err = cursor.SyncFocus(ctx, request.nativeCaretFocus)
}
return result, cursor
}