// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package review import ( "context" "time" "unicode/utf8" "git.stormux.org/storm/cthulhu/internal/atspi" ) // readDocumentBraille reads the accepted line and selection ranges for its // inline fragments. Optional display reads never fail or repeat navigation. func (worker *Worker) readDocumentBraille(ctx context.Context, result Result) Result { if !worker.documentBraille.Load() || !result.Moved || result.Err != nil || result.CaretErr != nil || !worker.CurrentDocumentResult(result) || worker.focus() != result.DocumentFocus || result.DocumentCaretCommand >= TableLeft || result.DocumentPosition.Snapshot.Role == "password text" { return result } // Explicitly named links are atomic browse objects. Their native text may // contain only a numeric badge; braille must use the same accessible label // as speech rather than enriching it with that incomplete child text. if snapshot := result.DocumentPosition.Snapshot; snapshot.Role == "link" && wholeCaretNode(snapshot) { return result } reader, ok := worker.nativeCapturerFor(result.DocumentFocus).(DocumentCaretReader) if !ok || !result.DocumentPosition.Object.Valid() { return result } // Display enrichment must not hold ordinary speech/navigation for the // longer whole-document capture timeout. deadline, cancel := context.WithTimeout(ctx, min(worker.captureTimeout, 100*time.Millisecond)) defer cancel() line, err := reader.ReadDocumentCaret(deadline, result.DocumentPosition.Object, result.DocumentPosition.Offset, atspi.DocumentCaretBrailleLine) if err != nil { result.DocumentBrailleErr = err return result } if line.Start < 0 || result.DocumentPosition.Offset < line.Start || result.DocumentPosition.Offset-line.Start > utf8.RuneCountInString(documentBrailleNativeText(line)) || line.Text == "" { result.DocumentBrailleErr = atspi.ErrNoDocumentCaret return result } result.DocumentBrailleLine = &line if assembled := result.DocumentCaretLine; assembled != nil { copyLine := *assembled copyLine.Contents = append([]atspi.DocumentContent(nil), assembled.Contents...) for index := range copyLine.Contents { content := ©Line.Contents[index] content.Snapshot.SelectionStart, content.Snapshot.SelectionEnd = 0, 0 selected := line if content.Snapshot.ID() != result.DocumentPosition.Object.ID() { if !content.Reference.Valid() { continue } selected, err = reader.ReadDocumentCaret(deadline, content.Reference, content.Start, atspi.DocumentCaretBrailleLine) if err != nil { continue } } start, end := content.Start-selected.Start, content.Start-selected.Start+utf8.RuneCountInString(content.RawText) runes := []rune(documentBrailleNativeText(selected)) if start < 0 || end > len(runes) || string(runes[start:end]) != content.RawText { continue } content.Snapshot.SelectionStart, content.Snapshot.SelectionEnd = selected.SelectionStart, selected.SelectionEnd content.Snapshot.TextAttributeRuns = selected.TextAttributeRuns } result.DocumentCaretLine = ©Line } return result } // Braille expansion changes display characters, never the native offset domain. func documentBrailleNativeText(line atspi.DocumentCaretText) string { if line.Expanded { return line.RawText } return line.Text }