171 lines
5.8 KiB
Go
171 lines
5.8 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 app
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/fenrir/internal/diagnostic"
|
|
"git.stormux.org/storm/fenrir/internal/i18n"
|
|
"git.stormux.org/storm/fenrir/internal/presentation"
|
|
"git.stormux.org/storm/fenrir/internal/spellcheck"
|
|
)
|
|
|
|
func (a *application) read_screen() error {
|
|
started := time.Now()
|
|
snapshot, err := a.source.Read()
|
|
diagnostic.Timing("screen capture", started)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.share_notification_history(snapshot.Terminal, snapshot.Active)
|
|
if a.lastTerminal == 0 {
|
|
if snapshot.Active {
|
|
a.present(i18n.T("Start Fenrir"), presentation.Ready)
|
|
} else {
|
|
a.play(presentation.Ready)
|
|
}
|
|
}
|
|
sessionChanged := a.lastTerminal != 0 && (snapshot.Terminal != a.lastTerminal || snapshot.Active != a.lastActive)
|
|
if a.lastTerminal != 0 && (sessionChanged || snapshot.Generation != a.lastGeneration || snapshot.Rows != a.lastRows || snapshot.Cols != a.lastCols) {
|
|
a.spellSession++
|
|
}
|
|
if a.cues != nil && (sessionChanged || a.lastTerminal != 0 &&
|
|
(snapshot.Generation != a.lastGeneration || snapshot.Rows != a.lastRows || snapshot.Cols != a.lastCols)) {
|
|
a.cues.Clear_progress()
|
|
}
|
|
if sessionChanged {
|
|
a.voice.Interrupt()
|
|
a.ui.close()
|
|
a.clipboards.session_changed(snapshot.Terminal)
|
|
if a.options.ScriptRunner != nil {
|
|
a.scriptGeneration = a.options.ScriptRunner.Cancel()
|
|
}
|
|
}
|
|
a.clipboards.terminal = snapshot.Terminal
|
|
a.lastSnapshot = snapshot
|
|
frame, ready := a.observe_override(snapshot)
|
|
if !ready {
|
|
a.lastTerminal, a.lastActive = snapshot.Terminal, snapshot.Active
|
|
a.lastGeneration, a.lastRows, a.lastCols = snapshot.Generation, snapshot.Rows, snapshot.Cols
|
|
a.lastNumLock = snapshot.NumLock
|
|
return nil
|
|
}
|
|
updated := time.Now()
|
|
output := a.engine.Update_application(snapshot, frame)
|
|
if a.options.Spellcheck != nil && a.settings.live.Reading.Spellcheck && !a.ui.active && !a.muted && !a.temporarySilence {
|
|
for _, word := range a.engine.Completed_words() {
|
|
a.options.Spellcheck.Submit(spellcheck.Request{Operation: spellcheck.Automatic, Word: word, Generation: a.spellSession, Revision: a.spellFeedback})
|
|
}
|
|
}
|
|
restoreLine := a.prompts.Observe(a.engine.Automatic_snapshot())
|
|
restored := a.temporarySilence && !a.muted && !a.ui.active && restoreLine != ""
|
|
if restored {
|
|
a.temporarySilence = false
|
|
a.present(i18n.T("Speech restored"), presentation.SpeechOn)
|
|
if a.engine.Automatic_reading() {
|
|
a.present(restoreLine, "")
|
|
}
|
|
}
|
|
diagnostic.Timing("screen processing", updated)
|
|
soundAvailable := a.cues != nil
|
|
if player, ok := a.cues.(interface{ Available() bool }); ok {
|
|
soundAvailable = player.Available()
|
|
}
|
|
diagnostic.Record("reader presentation", "terminal", snapshot.Terminal, "generation", snapshot.Generation,
|
|
"messages", len(output), "ui_active", a.ui.active, "temporary_silence", a.temporarySilence,
|
|
"muted", a.muted, "speech_available", a.settings.live.Presentation.Speech.Enabled,
|
|
"progress_enabled", a.settings.live.Presentation.Progress.Enabled, "sound_enabled", a.settings.live.Presentation.Sound.Enabled, "sound_available", soundAvailable)
|
|
// VT sounds remain available while automatic reading is disabled.
|
|
if sessionChanged && snapshot.Active && len(output) == 0 {
|
|
a.play(presentation.TTYChanged)
|
|
}
|
|
if !a.ui.active && !restored {
|
|
for _, message := range output {
|
|
if message.Attributes != nil {
|
|
a.present_attributes(message)
|
|
} else if message.Indentation != nil {
|
|
a.present_indentation(message)
|
|
} else if message.Character != nil && !a.temporarySilence {
|
|
a.voice.say_character(*message.Character)
|
|
} else {
|
|
a.present(message.Text, message.Cue)
|
|
}
|
|
}
|
|
}
|
|
a.lastTerminal, a.lastActive = snapshot.Terminal, snapshot.Active
|
|
a.lastGeneration, a.lastRows, a.lastCols = snapshot.Generation, snapshot.Rows, snapshot.Cols
|
|
a.lastNumLock = snapshot.NumLock
|
|
a.play_progress()
|
|
return nil
|
|
}
|
|
|
|
func (a *application) poll_input(inputReady bool) error {
|
|
tty, active, numLock, err := a.source.State()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
changed := tty != a.lastTerminal || active != a.lastActive
|
|
if changed {
|
|
if err := a.read_screen(); err != nil {
|
|
return err
|
|
}
|
|
active, numLock = a.lastActive, a.lastNumLock
|
|
}
|
|
if inputReady || changed {
|
|
if err := a.keyboards.Poll(active, numLock, a.handle_input); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
a.apply_pending_modifiers()
|
|
return nil
|
|
}
|
|
|
|
func (a *application) screen_event(err error) error {
|
|
if err != nil {
|
|
return fmt.Errorf("screen watcher failed: %w", err)
|
|
}
|
|
// Interrupt for any pending key before presenting the resulting
|
|
// screen echo. This prevents that same key from canceling its echo
|
|
// just because the kernel reported screen readiness first.
|
|
select {
|
|
case err := <-a.keyboards.Events():
|
|
if err != nil {
|
|
return fmt.Errorf("keyboard watcher failed: %w", err)
|
|
}
|
|
if err := a.poll_input(true); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
}
|
|
if a.quitting {
|
|
return nil
|
|
}
|
|
previousTTY, previousText := a.lastTerminal, a.lastActive
|
|
if err := a.read_screen(); err != nil {
|
|
return err
|
|
}
|
|
if previousTTY != a.lastTerminal || previousText != a.lastActive {
|
|
if err := a.keyboards.Poll(a.lastActive, a.lastNumLock, a.handle_input); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|