Files

113 lines
3.7 KiB
Go

// 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
}