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

+168
View File
@@ -0,0 +1,168 @@
// 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"
"strings"
"git.stormux.org/storm/cthulhu/internal/atspi"
)
// move_caret_line handles line and document boundaries under the cursor lock
// held by MoveCaret. Text-unit movement has its separate traversal helper.
func move_caret_object_line(ctx context.Context, reader DocumentCaretReader, doc atspi.DocumentSnapshot, place *documentPlace, indices []int, current int, command DocumentCaretCommand) (DocumentCaretMove, error) {
unit := atspi.DocumentCaretLine
if command == CaretReadLine {
unit = atspi.DocumentCaretLineRemainder
}
position := 0
for position < len(indices) && indices[position] < current {
position++
}
backwards := command == CaretPreviousLine || command == CaretDocumentEnd
if command == CaretDocumentStart {
position = 0
} else if command == CaretDocumentEnd {
position = len(indices) - 1
}
offset := place.offset
if position == len(indices) || indices[position] != current || command == CaretDocumentStart {
offset = 0
}
if command == CaretDocumentEnd {
offset = doc.Nodes[indices[position]].Snapshot.CharacterCount - 1
}
if backwards && command != CaretDocumentEnd && (position == len(indices) || indices[position] != current) {
position--
if position < 0 {
return DocumentCaretMove{Position: documentPosition(doc, *place)}, nil
}
offset = doc.Nodes[indices[position]].Snapshot.CharacterCount - 1
}
initialPosition := position
for attempts := 0; attempts < len(indices)+1; attempts++ {
if position < 0 || position >= len(indices) {
return DocumentCaretMove{Position: documentPosition(doc, *place)}, nil
}
node := doc.Nodes[indices[position]]
if node.Snapshot.CharacterCount == 0 || wholeCaretNode(node.Snapshot) {
if (command == CaretNextLine || command == CaretPreviousLine) && node.Reference == place.object {
if backwards {
position--
if position >= 0 {
offset = doc.Nodes[indices[position]].Snapshot.CharacterCount - 1
}
} else {
position++
offset = 0
}
continue
}
place.object, place.offset, place.origin = node.Reference, 0, CaretNavigation
return DocumentCaretMove{Position: documentPosition(doc, *place), Moved: true}, nil
}
if offset < 0 {
offset = 0
}
if command == CaretReadLine && offset >= node.Snapshot.CharacterCount {
position++
offset = 0
continue
}
if offset >= node.Snapshot.CharacterCount {
offset = node.Snapshot.CharacterCount - 1
}
piece, readErr := reader.ReadDocumentCaret(ctx, node.Reference, offset, unit)
if readErr != nil && !errors.Is(readErr, atspi.ErrNoDocumentCaret) {
return DocumentCaretMove{}, readErr
}
if readErr == nil && piece.End > piece.Start && strings.TrimSpace(piece.Text) != "\ufffc" {
nextOffset := offset
sameCurrent := node.Reference == place.object ||
place.origin == StructuralNavigation && position == initialPosition && indices[position] >= current
switch command {
case CaretNextLine:
if sameCurrent {
nextOffset = piece.End
if nextOffset < piece.CharacterCount {
piece, readErr = reader.ReadDocumentCaret(ctx, node.Reference, nextOffset, unit)
if readErr != nil {
return DocumentCaretMove{}, readErr
}
} else {
position++
if position >= len(indices) {
return DocumentCaretMove{Position: documentPosition(doc, *place)}, nil
}
offset = 0
continue
}
}
nextOffset = piece.Start
case CaretPreviousLine:
if sameCurrent {
if piece.Start > 0 {
piece, readErr = reader.ReadDocumentCaret(ctx, node.Reference, piece.Start-1, unit)
if readErr != nil {
return DocumentCaretMove{}, readErr
}
} else {
position--
if position < 0 {
return DocumentCaretMove{Position: documentPosition(doc, *place)}, nil
}
offset = doc.Nodes[indices[position]].Snapshot.CharacterCount - 1
continue
}
}
nextOffset = piece.Start
case CaretLineStart:
nextOffset = piece.Start
case CaretLineEnd, CaretDocumentEnd:
nextOffset = max(piece.Start, piece.End-1)
case CaretDocumentStart:
nextOffset = piece.Start
}
if piece.End <= piece.Start || strings.TrimSpace(piece.Text) == "\ufffc" {
// An embedded-object placeholder is a container, not spoken text.
if backwards {
position--
if position >= 0 {
offset = doc.Nodes[indices[position]].Snapshot.CharacterCount - 1
}
} else {
position++
offset = 0
}
continue
}
spoken := piece.Text
if command == CaretLineStart || command == CaretLineEnd {
character, charErr := reader.ReadDocumentCaret(ctx, node.Reference, nextOffset, atspi.DocumentCaretCharacter)
if charErr != nil {
return DocumentCaretMove{}, charErr
}
spoken = character.Text
}
place.object = node.Reference
place.offset = nextOffset
place.origin = CaretNavigation
return DocumentCaretMove{Position: documentPosition(doc, *place), Text: spoken, RawText: piece.RawText, Misspelled: piece.Misspelled, Moved: true, Start: piece.Start, End: piece.End}, nil
}
if backwards {
position--
if position >= 0 {
offset = doc.Nodes[indices[position]].Snapshot.CharacterCount - 1
}
} else {
position++
offset = 0
}
}
return DocumentCaretMove{Position: documentPosition(doc, *place)}, nil
}