Files

119 lines
3.0 KiB
Go

// 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 <https://www.gnu.org/licenses/gpl-3.0.html>.
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)
}