163 lines
5.9 KiB
Go
163 lines
5.9 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 owns the review cursor and actions on a visible screen.
|
|
// All state is used by the reader's event loop; actions never perform device or speech I/O.
|
|
package review
|
|
|
|
import (
|
|
"git.stormux.org/storm/fenrir/internal/command"
|
|
"git.stormux.org/storm/fenrir/internal/i18n"
|
|
"git.stormux.org/storm/fenrir/internal/presentation"
|
|
"git.stormux.org/storm/fenrir/internal/screen"
|
|
)
|
|
|
|
// Review has a cursor independent of the application. Its zero value is ready for Update.
|
|
type Review struct {
|
|
result presentation.CommandResult
|
|
attributeFormat string
|
|
screen screen.Snapshot
|
|
suppressLineBreak bool
|
|
suppressScreenBoundary bool
|
|
keepOnScreenChange bool
|
|
ready bool
|
|
reviewing bool
|
|
x, y int
|
|
markCount, startMark, endMark int
|
|
tableMode bool
|
|
tableColumn int
|
|
tableHeaders []string
|
|
tableLayout tableLayout
|
|
tableManual bool
|
|
tableAutoDisabled bool
|
|
}
|
|
|
|
func (r *Review) Configure(lineBreak, endOfScreen, leaveOnScreenChange bool) {
|
|
r.suppressLineBreak = !lineBreak
|
|
r.suppressScreenBoundary = !endOfScreen
|
|
r.keepOnScreenChange = !leaveOnScreenChange
|
|
}
|
|
|
|
// handlers is the explicit list of review actions. Add an action file and an
|
|
// entry here; input bindings live separately in package input.
|
|
var handlers = map[command.ID]func(*Review) string{
|
|
command.AttributeCursor: (*Review).attribute_cursor,
|
|
command.CursorLine: (*Review).cursor_line,
|
|
command.CursorColumn: (*Review).cursor_column,
|
|
command.LineToCursor: (*Review).line_to_cursor,
|
|
command.LineFromCursor: (*Review).line_from_cursor,
|
|
command.ScreenFirstChar: (*Review).screen_first_character,
|
|
command.ScreenLastChar: (*Review).screen_last_character,
|
|
command.ScreenBeforeCursor: (*Review).screen_before_cursor,
|
|
command.ScreenAfterCursor: (*Review).screen_after_cursor,
|
|
command.ReviewUp: (*Review).review_up,
|
|
command.ReviewDown: (*Review).review_down,
|
|
|
|
command.SpellWord: (*Review).spell_word,
|
|
command.PhoneticWord: (*Review).phonetic_word,
|
|
command.SpellLine: (*Review).spell_line,
|
|
command.PhoneticLine: (*Review).phonetic_line,
|
|
command.PhoneticChar: (*Review).phonetic_character,
|
|
command.PreviousLine: (*Review).previousLine,
|
|
command.CurrentLine: (*Review).currentLine,
|
|
command.NextLine: (*Review).nextLine,
|
|
command.Top: (*Review).top,
|
|
command.Bottom: (*Review).bottom,
|
|
command.PreviousChar: (*Review).previousCharacter,
|
|
command.CurrentChar: (*Review).currentCharacter,
|
|
command.NextChar: (*Review).nextCharacter,
|
|
command.LineStart: (*Review).lineStart,
|
|
command.LineEnd: (*Review).lineEnd,
|
|
command.FirstText: (*Review).firstText,
|
|
command.LastText: (*Review).lastText,
|
|
command.PreviousWord: (*Review).previousWord,
|
|
command.CurrentWord: (*Review).currentWord,
|
|
command.NextWord: (*Review).nextWord,
|
|
command.Position: (*Review).position,
|
|
command.ReadScreen: (*Review).readScreen,
|
|
command.FollowCursor: (*Review).followCursor,
|
|
}
|
|
|
|
// Update refreshes the screen, applying the configured review-exit policy.
|
|
// It reports whether the screen session changed, for automatic reading.
|
|
func (r *Review) Update(next screen.Snapshot) bool {
|
|
previous := r.screen
|
|
switched := !r.ready || previous.Terminal != next.Terminal || previous.Generation != next.Generation || previous.Active != next.Active || previous.Rows != next.Rows || previous.Cols != next.Cols
|
|
r.screen = next
|
|
r.ready = next.Active
|
|
if switched {
|
|
r.Clear_marks()
|
|
r.tableHeaders = nil
|
|
r.tableLayout = tableLayout{}
|
|
r.tableManual, r.tableAutoDisabled = false, false
|
|
r.tableColumn = 0
|
|
}
|
|
if !next.Active {
|
|
r.reviewing = false
|
|
return switched
|
|
}
|
|
if switched && !r.keepOnScreenChange || !r.reviewing {
|
|
r.x, r.y = next.X, next.Y
|
|
if switched {
|
|
r.reviewing = false
|
|
}
|
|
}
|
|
r.x, r.y = max(0, min(r.x, next.Cols-1)), max(0, min(r.y, next.Rows-1))
|
|
if r.tableMode && (switched || !r.reviewing) {
|
|
r.sync_table_column()
|
|
}
|
|
return switched
|
|
}
|
|
|
|
// Active reports whether review is detached from the application cursor.
|
|
func (r *Review) Active() bool { return r.reviewing }
|
|
|
|
// Execute checks screen availability before invoking an action. Action methods
|
|
// share this guard; the caller owns presentation and interruption.
|
|
func (r *Review) Execute(id command.ID) presentation.CommandResult {
|
|
r.result = presentation.CommandResult{}
|
|
r.result.Text = r.execute(id)
|
|
return r.result
|
|
}
|
|
|
|
func (r *Review) execute(id command.ID) string {
|
|
if !r.ready {
|
|
r.result.Cue = presentation.Error
|
|
return i18n.T("no text console")
|
|
}
|
|
action, ok := handlers[id]
|
|
if !ok {
|
|
r.result.Cue = presentation.Error
|
|
return i18n.T("unbound")
|
|
}
|
|
switch id {
|
|
case command.AttributeCursor, command.CursorLine, command.CursorColumn, command.LineToCursor, command.LineFromCursor, command.FollowCursor:
|
|
// Reports use the active cursor without entering detached review.
|
|
default:
|
|
if !r.reviewing {
|
|
r.start_table_review()
|
|
}
|
|
r.reviewing = true
|
|
}
|
|
if r.tableMode {
|
|
if text, handled := r.table_command(id); handled {
|
|
return text
|
|
}
|
|
}
|
|
return action(r)
|
|
}
|