113 lines
3.7 KiB
Go
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
|
|
}
|