// 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 . package app import ( "context" "errors" "log/slog" "git.stormux.org/storm/cthulhu/internal/input" "git.stormux.org/storm/cthulhu/internal/presentation" "git.stormux.org/storm/cthulhu/internal/review" ) func flatReviewCommand(command input.Command) (review.FlatUnit, int) { switch command { case input.ReviewWindowStart: return review.WindowStart, 0 case input.ReviewWindowEnd: return review.WindowEnd, 0 case input.ReviewPreviousLine: return review.Line, -1 case input.ReviewCurrentLine: return review.Line, 0 case input.ReviewNextLine: return review.Line, 1 case input.ReviewPreviousWord: return review.Word, -1 case input.ReviewCurrentWord, input.ReviewSpellWord, input.ReviewPhoneticWord: return review.Word, 0 case input.ReviewNextWord: return review.Word, 1 case input.ReviewPreviousCharacter: return review.Character, -1 case input.ReviewCurrentCharacter, input.ReviewPhoneticCharacter: return review.Character, 0 case input.ReviewNextCharacter: return review.Character, 1 case input.ReviewWordAbove: return review.WordAbove, 0 case input.ReviewWordBelow: return review.WordBelow, 0 case input.ReviewLineEnd: return review.LineEnd, 0 default: return 0, 0 } } func presentFlatReviewResult(logger *slog.Logger, speech speechEnqueuer, result review.Result) { text := "" if result.Err != nil { logger.Warn("flat review failed", "error", result.Err) switch { case errors.Is(result.Err, review.ErrLimitedReview): text = "This window exceeds the current flat review text limit." case errors.Is(result.Err, review.ErrNoReviewContent): text = "No visible content." default: text = "Could not find current location." } } else if !result.Moved && (result.FlatStep != 0 || result.FlatUnit == review.WordAbove || result.FlatUnit == review.WordBelow) { // Python's sequential review stops silently at window boundaries. return } else { text = presentation.FlatReviewText(result.FlatPosition.Text, result.FlatUnit == review.Character || result.FlatUnit == review.LineEnd) } speech.Interrupt() request := speechRequest{Text: text, Role: "text", Cause: "flat-review", Character: result.Err == nil && (result.FlatUnit == review.Character || result.FlatUnit == review.LineEnd)} if result.Err == nil && result.FlatUnit == review.Line { request.Line = &speechLine{Object: result.FlatPosition.Object, Raw: result.FlatPosition.Text} } if result.Err == nil { set_review_spelling(&request, result.FlatPosition.Text, result.FlatSpeech) } speech.Enqueue(request) if logger.Enabled(context.Background(), slog.LevelDebug) { logger.Debug("queued flat review presentation", "speech_text", request.Text, "spelling", request.Spelling, "speech_style", result.FlatSpeech, "unit", result.FlatUnit, "moved", result.Moved, "bus", result.FlatPosition.Object.BusName, "object_path", result.FlatPosition.Object.Path, "offset", result.FlatPosition.Offset, "has_offset", result.FlatPosition.HasOffset, "capture_duration", result.CaptureDuration, "capture_reason", result.CaptureReason, ) } }