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
|
||||
//
|
||||
// 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"
|
||||
"fmt"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
)
|
||||
|
||||
type moveResult struct {
|
||||
Result
|
||||
cursor *Cursor
|
||||
}
|
||||
|
||||
func (worker *Worker) move(ctx context.Context, cursor *Cursor, direction Direction) moveResult {
|
||||
var err error
|
||||
cursor, err = worker.syncLatestFocus(ctx, cursor, true)
|
||||
if err != nil {
|
||||
return moveResult{Result: Result{Direction: direction, Err: err}, cursor: cursor}
|
||||
}
|
||||
if cursor == nil {
|
||||
return moveResult{Result: Result{Direction: direction, Err: ErrNoCurrent}}
|
||||
}
|
||||
position, moved, err := cursor.Move(direction)
|
||||
if err == nil {
|
||||
return moveResult{Result: Result{Direction: direction, Position: position, Moved: moved}, cursor: cursor}
|
||||
}
|
||||
if !errors.Is(err, atspi.ErrStaleObject) && !errors.Is(err, atspi.ErrIncompleteTree) {
|
||||
return moveResult{Result: Result{Direction: direction, Err: err}, cursor: cursor}
|
||||
}
|
||||
|
||||
// An incomplete child list needs recapture without abandoning an otherwise
|
||||
// live independent review position. Stale objects still recover from focus.
|
||||
var retained *Cursor
|
||||
if errors.Is(err, atspi.ErrIncompleteTree) {
|
||||
retained = cursor
|
||||
}
|
||||
recovered, recoveryErr := worker.captureCursor(ctx, retained)
|
||||
if recoveryErr != nil {
|
||||
return moveResult{Result: Result{Direction: direction, Err: recoveryErr}, cursor: retained}
|
||||
}
|
||||
if recovered == nil {
|
||||
return moveResult{Result: Result{Direction: direction, Err: err}}
|
||||
}
|
||||
position, moved, err = recovered.Move(direction)
|
||||
return moveResult{Result: Result{Direction: direction, Position: position, Moved: moved, Err: err}, cursor: recovered}
|
||||
}
|
||||
|
||||
func (worker *Worker) syncLatestFocus(ctx context.Context, cursor *Cursor, captureIfMissing bool) (*Cursor, error) {
|
||||
focus := worker.focus()
|
||||
if !focus.Valid() {
|
||||
return cursor, nil
|
||||
}
|
||||
if cursor != nil {
|
||||
_, currentErr := cursor.Current()
|
||||
_, syncErr := cursor.SyncFocus(focus)
|
||||
if syncErr == nil && !errors.Is(currentErr, atspi.ErrStaleObject) {
|
||||
return cursor, nil
|
||||
}
|
||||
if syncErr != nil && !errors.Is(syncErr, atspi.ErrObjectNotFound) && !errors.Is(syncErr, atspi.ErrStaleObject) {
|
||||
return cursor, syncErr
|
||||
}
|
||||
}
|
||||
if !captureIfMissing && cursor == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return worker.captureCursor(ctx, nil)
|
||||
}
|
||||
|
||||
func (worker *Worker) captureCursor(ctx context.Context, cursor *Cursor) (*Cursor, error) {
|
||||
captureContext, cancelCapture := context.WithTimeout(ctx, worker.captureTimeout)
|
||||
tree, err := worker.capturer.CaptureDesktop(captureContext)
|
||||
cancelCapture()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("capture object review tree: %w", err)
|
||||
}
|
||||
if tree == nil {
|
||||
return nil, errors.New("capture object review tree: nil tree")
|
||||
}
|
||||
if cursor == nil || cursor.tree != tree {
|
||||
cursor, err = NewCursor(tree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Focus can change while capture is underway. Always synchronize to the
|
||||
// newest identity after the capture returns.
|
||||
focus := worker.focus()
|
||||
if _, err := cursor.SyncFocus(focus); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cursor, nil
|
||||
}
|
||||
Reference in new issue
Block a user