// Fenrir console screen reader. // Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later // // 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 review import ( "git.stormux.org/storm/fenrir/internal/i18n" "unicode" "git.stormux.org/storm/fenrir/internal/presentation" ) // word_start selects the containing word, or searches backward through whitespace. // Coordinates are screen cells, so Unicode never becomes a byte offset. func (r *Review) word_start(position int) (int, bool) { s := r.screen for position >= 0 && unicode.IsSpace(s.Cells[position]) { position-- } if position < 0 { return 0, false } for position%s.Cols > 0 && !unicode.IsSpace(s.Cells[position-1]) { position-- } return position, true } func (r *Review) word_at(position int) string { s := r.screen line := s.Line(position / s.Cols) start, end := position%s.Cols, position%s.Cols for end < len(line) && !unicode.IsSpace(line[end]) { end++ } return string(line[start:end]) } func (r *Review) screen_blank() bool { for _, char := range r.screen.Cells { if !unicode.IsSpace(char) { return false } } return true } func (r *Review) blank_word() string { r.result.Cue = presentation.Blank return i18n.T("blank") } func (r *Review) word() string { if r.screen_blank() { return r.blank_word() } start, ok := r.word_start(r.y*r.screen.Cols + r.x) if !ok { return r.screen_boundary(false) } return r.select_word(start) } // raw_word shares selection with spelling without moving the review cursor. func (r *Review) raw_word() string { start, ok := r.word_start(r.y*r.screen.Cols + r.x) if !ok { return "" } return r.word_at(start) } func (r *Review) select_word(position int) string { if r.y != position/r.screen.Cols { r.line_break() } r.x, r.y = position%r.screen.Cols, position/r.screen.Cols return r.word_at(position) } func (r *Review) moveWord(forward bool) string { if r.screen_blank() { return r.blank_word() } s := r.screen position := r.y*s.Cols + r.x if !forward { start, ok := r.word_start(position) if !ok { return r.screen_boundary(false) } previous, ok := r.word_start(start - 1) if !ok { return r.screen_boundary(false) } return r.select_word(previous) } for target := position + 1; target < s.Rows*s.Cols; target++ { if !unicode.IsSpace(s.Cells[target]) && (target%s.Cols == 0 || unicode.IsSpace(s.Cells[target-1])) { return r.select_word(target) } } return r.screen_boundary(true) }