Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,112 @@
|
||||
// Cthulhu screen reader.
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package review
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
)
|
||||
|
||||
// Python caret_navigation starts a word move at the adjacent non-space
|
||||
// character, rather than the next/previous word boundary. In particular,
|
||||
// reversing at the end of a word must read that word, not skip over it.
|
||||
func moveCaretText(ctx context.Context, reader DocumentCaretReader, doc atspi.DocumentSnapshot, place *documentPlace, indices []int, current int, backwards, byWord bool) (DocumentCaretMove, error) {
|
||||
step := 1
|
||||
if backwards {
|
||||
step = -1
|
||||
}
|
||||
position := 0
|
||||
for position < len(indices) && indices[position] < current {
|
||||
position++
|
||||
}
|
||||
offset := 0
|
||||
if position < len(indices) && indices[position] == current {
|
||||
offset = place.offset + step
|
||||
} else if backwards {
|
||||
position--
|
||||
if position >= 0 {
|
||||
offset = doc.Nodes[indices[position]].Snapshot.CharacterCount - 1
|
||||
}
|
||||
} else if place.origin == StructuralNavigation {
|
||||
// Structural jumps speak the first descendant at offset zero.
|
||||
offset = 1
|
||||
}
|
||||
for position >= 0 && position < len(indices) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return DocumentCaretMove{}, err
|
||||
}
|
||||
node := doc.Nodes[indices[position]]
|
||||
if node.Snapshot.CharacterCount == 0 || wholeCaretNode(node.Snapshot) {
|
||||
if node.Reference != place.object {
|
||||
place.object, place.offset, place.origin = node.Reference, 0, CaretNavigation
|
||||
return DocumentCaretMove{Position: documentPosition(doc, *place), Moved: true}, nil
|
||||
}
|
||||
position += step
|
||||
offset = 0
|
||||
if backwards && position >= 0 {
|
||||
offset = doc.Nodes[indices[position]].Snapshot.CharacterCount - 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
if offset >= 0 && offset < node.Snapshot.CharacterCount {
|
||||
character, err := reader.ReadDocumentCaret(ctx, node.Reference, offset, atspi.DocumentCaretCharacter)
|
||||
if err != nil && !errors.Is(err, atspi.ErrNoDocumentCaret) {
|
||||
return DocumentCaretMove{}, err
|
||||
}
|
||||
if err == nil && character.End > character.Start {
|
||||
value, _ := utf8.DecodeRuneInString(character.Text)
|
||||
if character.Text == "" || byWord && unicode.IsSpace(value) || value == '\ufffc' || value == '\ufeff' {
|
||||
offset += step
|
||||
continue
|
||||
}
|
||||
if !byWord {
|
||||
place.object, place.offset, place.origin = node.Reference, character.Start, CaretNavigation
|
||||
return DocumentCaretMove{Position: documentPosition(doc, *place), Text: character.Text, Misspelled: character.Misspelled, Moved: true, Start: character.Start, End: character.End}, nil
|
||||
}
|
||||
word, err := reader.ReadDocumentCaret(ctx, node.Reference, offset, atspi.DocumentCaretWord)
|
||||
if err != nil {
|
||||
return DocumentCaretMove{}, err
|
||||
}
|
||||
if word.Text == "" || word.Start < 0 || word.Start > offset || word.End <= offset || word.End > word.CharacterCount {
|
||||
// Chromium can expose an emoji as a valid character but
|
||||
// return a broken WORD range (for example, end = -1).
|
||||
// Read that character as a unit rather than trapping word
|
||||
// navigation at the same position on every key press.
|
||||
if character.Start < 0 || character.Start > offset || character.End <= offset || character.End > character.CharacterCount {
|
||||
return DocumentCaretMove{}, atspi.ErrNoDocumentCaret
|
||||
}
|
||||
word = character
|
||||
}
|
||||
nextOffset := word.Start
|
||||
if !backwards {
|
||||
nextOffset = word.End
|
||||
// WORD ranges may contain a trailing delimiter. Query its
|
||||
// actual character; spoken word text can be normalized.
|
||||
last, err := reader.ReadDocumentCaret(ctx, node.Reference, word.End-1, atspi.DocumentCaretCharacter)
|
||||
if err != nil {
|
||||
return DocumentCaretMove{}, err
|
||||
}
|
||||
value, _ := utf8.DecodeRuneInString(last.Text)
|
||||
if unicode.IsSpace(value) {
|
||||
nextOffset--
|
||||
}
|
||||
}
|
||||
place.object, place.offset, place.origin = node.Reference, nextOffset, CaretNavigation
|
||||
return DocumentCaretMove{Position: documentPosition(doc, *place), Text: word.Text, Misspelled: word.Misspelled, Moved: true, Start: word.Start, End: word.End}, nil
|
||||
}
|
||||
}
|
||||
position += step
|
||||
offset = 0
|
||||
if backwards && position >= 0 {
|
||||
offset = doc.Nodes[indices[position]].Snapshot.CharacterCount - 1
|
||||
}
|
||||
}
|
||||
return DocumentCaretMove{Position: documentPosition(doc, *place)}, nil
|
||||
}
|
||||
Reference in new issue
Block a user