// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package review import ( "context" "time" "git.stormux.org/storm/cthulhu/internal/atspi" ) // ReadCurrentLine leaves both the native caret and review cursors untouched. func (worker *Worker) ReadCurrentLine(browse bool, style FlatSpeech) error { if style > FlatPhonetic { return ErrInvalidDirection } select { case worker.requests <- request{currentLine: true, currentLineBrowse: browse, flatSpeech: style, documentEpoch: worker.documentEpoch.Load(), queuedAt: time.Now()}: return nil default: return ErrRequestQueueFull } } func (worker *Worker) read_current_line(ctx context.Context, cursor *DocumentCursor, browse bool) Result { result := Result{CurrentLine: true, CurrentLineBrowse: browse, DocumentFocus: worker.focus()} if !result.DocumentFocus.Valid() { result.Err = ErrNoCurrent return result } deadline, cancel := context.WithTimeout(ctx, worker.captureTimeout) defer cancel() if !browse { reader, ok := worker.nativeCapturerFor(result.DocumentFocus).(interface { ReadFocusedLine(context.Context, atspi.ObjectID) (atspi.DocumentContent, error) }) if !ok { result.Err = atspi.ErrNoDocumentCaret return result } content, err := reader.ReadFocusedLine(deadline, result.DocumentFocus) result.Err = err result.DocumentCaretText, result.DocumentCaretRawText = content.Text, content.RawText result.DocumentPosition.Snapshot = content.Snapshot result.DocumentCaretContext = content.SourceContext return result } // Start a private cursor at the saved browse position. Going to line start // before reading avoids Say All's deliberate "rest of this line" behavior. if cursor != nil { cursor = &DocumentCursor{tree: cursor.tree, active: cursor.active, places: append([]documentPlace(nil), cursor.places...)} _, result.Err = cursor.syncActionFocus(deadline, result.DocumentFocus) } else { result.Err = ErrNoCurrent } if documentNeedsCapture(result.Err) { cursor, result.Err = worker.refreshDocumentCursor(deadline, cursor, result.DocumentFocus) } if result.Err != nil { return result } reader, ok := worker.nativeCapturerFor(result.DocumentFocus).(DocumentCaretReader) if !ok { result.Err = atspi.ErrNoDocumentCaret return result } _, cursor, result.Err = worker.readAllMove(deadline, cursor, reader, CaretLineStart, result.DocumentFocus) if result.Err != nil { return result } movement, cursor, err := worker.readAllMove(deadline, cursor, reader, CaretReadLine, result.DocumentFocus) result.Err = err result.DocumentPosition, result.DocumentCaretText = movement.Position, movement.Text result.DocumentCaretRawText, result.DocumentCaretLine = movement.RawText, movement.Line if err == nil { // Retain the unfiltered ancestry from the tree that produced this // movement, including after recapture. Missing metadata keeps export // private without preventing the local current-line announcement. if owner, contextErr := cursor.tree.DocumentContext(movement.Position.Object); contextErr == nil { result.DocumentCaretContext = owner.Ancestors } } if result.DocumentCaretText == "" && wholeCaretNode(movement.Position.Snapshot) { result.DocumentCaretText = movement.Position.Snapshot.Name } return result }