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,132 @@
|
||||
// Cthulhu screen reader.
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
|
||||
|
||||
package review
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
)
|
||||
|
||||
// SetFlatOrigin starts a new flat-review context at the browse caret without
|
||||
// changing native focus. Once review begins, its cursor moves independently.
|
||||
func (worker *Worker) SetFlatOrigin(object atspi.ObjectID, offset int) {
|
||||
worker.stateMutex.Lock()
|
||||
worker.flatOrigin, worker.flatOffset = object, offset
|
||||
worker.stateMutex.Unlock()
|
||||
}
|
||||
|
||||
func (worker *Worker) flatStart() (atspi.ObjectID, int) {
|
||||
worker.stateMutex.Lock()
|
||||
defer worker.stateMutex.Unlock()
|
||||
return worker.flatOrigin, worker.flatOffset
|
||||
}
|
||||
|
||||
// FlatSpeech controls the speech of a review result without moving its cursor.
|
||||
type FlatSpeech uint8
|
||||
|
||||
const (
|
||||
FlatSpeak FlatSpeech = iota
|
||||
FlatSpell
|
||||
FlatPhonetic
|
||||
)
|
||||
|
||||
// ReadFlat queues sequential review on the same bounded FIFO as object review.
|
||||
// Capacity is 16; overflow rejects the newest request and is reported to the
|
||||
// caller. Results retain FIFO order in a 16-result queue; a stalled consumer
|
||||
// blocks only this worker. Cancellation discards pending work and joins it.
|
||||
func (worker *Worker) ReadFlat(unit FlatUnit, step int) error {
|
||||
return worker.ReadFlatWithSpeech(unit, step, FlatSpeak)
|
||||
}
|
||||
|
||||
// ReadFlatWithSpeech retains presentation policy with its FIFO request, so a
|
||||
// later press cannot change the speech of an earlier asynchronous result.
|
||||
func (worker *Worker) ReadFlatWithSpeech(unit FlatUnit, step int, style FlatSpeech) error {
|
||||
if unit < Line || unit > LineEnd || step < -1 || step > 1 || (unit > Character && step != 0) {
|
||||
return ErrInvalidDirection
|
||||
}
|
||||
if style > FlatPhonetic || (style != FlatSpeak && (step != 0 || (unit != Word && unit != Character))) {
|
||||
return ErrInvalidDirection
|
||||
}
|
||||
select {
|
||||
case worker.requests <- request{documentEpoch: worker.documentEpoch.Load(), unit: unit, step: step, flatSpeech: style}:
|
||||
return nil
|
||||
default:
|
||||
return ErrRequestQueueFull
|
||||
}
|
||||
}
|
||||
|
||||
func (worker *Worker) readFlat(ctx context.Context, cursor *FlatCursor, tree *atspi.AccessibleTree, unit FlatUnit, step int) (Result, *atspi.AccessibleTree) {
|
||||
focus := worker.focus()
|
||||
result := Result{FlatUnit: unit, FlatStep: step, FlatFocus: focus}
|
||||
if !focus.Valid() {
|
||||
result.Err = ErrNoCurrent
|
||||
return result, tree
|
||||
}
|
||||
fresh := false
|
||||
if policy, ok := worker.capturer.(FreshReviewWindowCapturer); ok {
|
||||
fresh = policy.FreshReviewWindow(focus)
|
||||
}
|
||||
var err error
|
||||
if !fresh {
|
||||
err = cursor.Refresh(ctx, tree, focus)
|
||||
}
|
||||
if fresh || errors.Is(err, ErrNoCurrent) || errors.Is(err, atspi.ErrObjectNotFound) || errors.Is(err, atspi.ErrStaleObject) || errors.Is(err, atspi.ErrIncompleteTree) || errors.Is(err, atspi.ErrStaleText) || errors.Is(err, atspi.ErrStaleGeometry) {
|
||||
result.CaptureReason = err
|
||||
timeout := worker.flatCaptureTimeout
|
||||
if fresh {
|
||||
timeout = min(timeout, 500*time.Millisecond)
|
||||
}
|
||||
captureContext, cancel := context.WithTimeout(ctx, timeout)
|
||||
captureStarted := time.Now()
|
||||
captured, captureErr := worker.capturer.CaptureReviewWindow(captureContext, focus)
|
||||
result.CaptureDuration = time.Since(captureStarted)
|
||||
cancel()
|
||||
if captureErr != nil {
|
||||
result.Err = captureErr
|
||||
return result, tree
|
||||
}
|
||||
if captured == nil {
|
||||
result.Err = errors.New("capture flat review tree: nil tree")
|
||||
return result, tree
|
||||
}
|
||||
tree = captured
|
||||
// Capture can outlive a focus transition. Use the newest identity and
|
||||
// refresh only once: repeated invalidation cannot cause a retry loop.
|
||||
result.FlatFocus = worker.focus()
|
||||
err = cursor.Refresh(ctx, tree, result.FlatFocus)
|
||||
}
|
||||
if err != nil {
|
||||
result.Err = err
|
||||
return result, tree
|
||||
}
|
||||
if cursor.focus == focus && !cursor.started {
|
||||
if origin, offset := worker.flatStart(); origin.Valid() {
|
||||
if !cursor.seek(origin, offset, true) {
|
||||
if !cursor.seek(origin, 0, false) {
|
||||
cursor.seekDescendant(origin)
|
||||
}
|
||||
}
|
||||
}
|
||||
cursor.started = true
|
||||
}
|
||||
result.FlatPosition, result.Moved, result.Err = cursor.Read(unit, step)
|
||||
return result, tree
|
||||
}
|
||||
Reference in new issue
Block a user