// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package review import ( "context" "errors" "git.stormux.org/storm/cthulhu/internal/atspi" ) // Interest belongs to the review worker. Zone bounds are provider coordinates; // callers must resolve fresh source/window geometry before screen capture. // A flat zone must never silently fall back to the larger object's bounds. type Interest struct { Offset int // -1 means use the native caret; virtual browser carets supply an offset. Object atspi.ObjectID Zone atspi.Rectangle HasZone bool } type interestMode uint8 const ( implicitInterest interestMode = iota objectInterest flatInterest ) type interestReply struct { interest Interest err error } type interestRequest struct { ctx context.Context result chan interestReply } // ObjectOfInterest is ordered with navigation, and never captures a tree. Call // it on a worker. Explicit review wins over a virtual browser caret, then focus. // Failed/stale explicit review returns an error instead of capturing focus. func (worker *Worker) ObjectOfInterest(ctx context.Context) (Interest, error) { if err := ctx.Err(); err != nil { return Interest{}, err } worker.stateMutex.Lock() done, started := worker.done, worker.started worker.stateMutex.Unlock() if !started { return Interest{}, errors.New("review worker is not running") } select { case <-done: return Interest{}, ErrNoCurrent default: } query := &interestRequest{ctx: ctx, result: make(chan interestReply, 1)} select { case worker.requests <- request{interest: query}: case <-ctx.Done(): return Interest{}, ctx.Err() case <-done: return Interest{}, ErrNoCurrent } select { case reply := <-query.result: return reply.interest, reply.err case <-ctx.Done(): return Interest{}, ctx.Err() case <-done: return Interest{}, ErrNoCurrent } } func resolve_interest(ctx context.Context, focus atspi.ObjectID, explicit interestMode, cursor *Cursor, flat *FlatCursor, flatTree *atspi.AccessibleTree, document *DocumentCursor) (Interest, error) { if err := ctx.Err(); err != nil { return Interest{}, err } if !focus.Valid() { return Interest{}, ErrNoCurrent } switch explicit { case objectInterest: if cursor == nil { return Interest{}, ErrNoCurrent } position, err := cursor.Current() return Interest{Object: position.Current.ID(), Offset: -1}, err case flatInterest: if err := flat.Refresh(ctx, flatTree, focus); err != nil { return Interest{}, err } position := flat.position(Character) if !position.Bounds.Valid() { return Interest{}, atspi.ErrStaleGeometry } return Interest{Object: position.Object, Offset: -1, Zone: position.Bounds, HasZone: true}, nil } if document != nil { position, err := document.SyncFocus(ctx, focus) if err == nil && position.Origin != NativeNavigation { return Interest{Object: position.Object.ID(), Offset: position.Offset}, nil } if err != nil && !errors.Is(err, atspi.ErrNotInDocument) && !errors.Is(err, atspi.ErrObjectNotFound) { return Interest{}, err } } return Interest{Object: focus, Offset: -1}, nil }