119 lines
3.0 KiB
Go
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)
|
|
}
|