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

+133
View File
@@ -0,0 +1,133 @@
package review
import (
"context"
"time"
"git.stormux.org/storm/cthulhu/internal/atspi"
)
type documentBrailleRequest struct {
ctx context.Context
focus, document, target atspi.ObjectID
line string
start, offset int
done chan error
}
// RouteDocumentBraille shares the navigation FIFO. The display session may
// wait, but input and event callbacks never wait for native caret placement.
func (worker *Worker) RouteDocumentBraille(ctx context.Context, epoch uint64, focus, document, target atspi.ObjectID, line string, start, offset int) error {
if err := ctx.Err(); err != nil {
return err
}
route := &documentBrailleRequest{ctx: ctx, focus: focus, document: document, target: target, line: line, start: start, offset: offset, done: make(chan error, 1)}
select {
case worker.requests <- request{brailleRoute: route, documentEpoch: epoch, queuedAt: time.Now()}:
default:
return ErrRequestQueueFull
}
select {
case err := <-route.done:
return err
case <-ctx.Done():
return ctx.Err()
}
}
func (worker *Worker) route_document_braille(ctx context.Context, cursor *DocumentCursor, request request) (result Result) {
route := request.brailleRoute
result = Result{DocumentBrailleRoute: true, DocumentCaretCommand: CaretReadLine, DocumentFocus: route.focus, DocumentEpoch: request.documentEpoch}
defer func() { route.done <- result.Err }()
if cursor == nil || worker.focus() != route.focus || !worker.CurrentDocumentResult(result) {
result.Err = atspi.ErrStaleObject
return
}
ctx, cancel := context.WithTimeout(ctx, worker.captureTimeout)
stop := context.AfterFunc(route.ctx, cancel)
defer stop()
defer cancel()
if result.Err = route.ctx.Err(); result.Err != nil {
return
}
target, err := cursor.tree.Reference(route.target)
if err != nil {
result.Err = err
return
}
owner, err := cursor.tree.DocumentContext(target)
if err != nil || owner.TopLevel.ID() != route.document {
result.Err = atspi.ErrStaleObject
return
}
router, ok := worker.nativeCapturerFor(route.focus).(interface {
RouteBraille(context.Context, atspi.BrailleRoute) error
})
if !ok {
result.Err = atspi.ErrNoDocumentCaret
return
}
reader, ok := worker.nativeCapturerFor(route.focus).(DocumentCaretReader)
if !ok {
result.Err = atspi.ErrNoDocumentCaret
return
}
doc, err := cursor.tree.Document(ctx, target)
if err != nil {
result.Err = err
return
}
node, err := native_caret_node(ctx, reader, doc, route.target, route.offset)
if err != nil {
result.Err = err
return
}
if node.Hidden {
result.Err = atspi.ErrStaleObject
return
}
result.Err = router.RouteBraille(ctx, atspi.BrailleRoute{Focus: route.focus, Position: target, Line: route.line, Start: route.start, Offset: route.offset})
if result.Err != nil || worker.focus() != route.focus || !worker.CurrentDocumentResult(result) {
if result.Err == nil {
result.Err = atspi.ErrStaleObject
}
return
}
// Keep the document anchor; a routed inline child is not native focus.
cursor.mu.Lock()
if len(cursor.places) == 0 || cursor.active.ID() != route.document {
cursor.mu.Unlock()
result.Err = atspi.ErrStaleObject
return
}
place := &cursor.places[len(cursor.places)-1]
place.object, place.offset, place.origin = node.Reference, route.offset, CaretNavigation
place.tableCell = atspi.ObjectID{}
snapshot, err := cursor.tree.Resolve(node.Reference)
result.DocumentPosition = DocumentPosition{Document: owner.TopLevel, Object: node.Reference, Snapshot: snapshot, Offset: route.offset, Origin: CaretNavigation}
cursor.mu.Unlock()
result.Err, result.Moved = err, err == nil
result.DocumentCaretContext = owner.Ancestors
if result.Moved && worker.documentBraille.Load() {
// Optional assembly refresh preserves surrounding inline fragments while
// retaining the exact routed offset. It never repeats caret placement.
deadline, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()
indices, _ := caret_node_indices(doc.Nodes, node.Reference)
position := -1
for index, candidate := range indices {
if doc.Nodes[candidate].Reference == node.Reference {
position = index
break
}
}
if position >= 0 {
parts, err := collect_visual_line(deadline, reader, doc, indices, visual_line_owners(doc.Nodes), position, route.offset, true)
if err == nil && len(parts) > 1 {
line, _, _ := assemble_visual_line(cursor.tree, doc, parts)
result.DocumentCaretLine = &line
}
}
}
return
}