194 lines
6.4 KiB
Go
194 lines
6.4 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 (
|
|
"log/slog"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/fenrir/internal/command"
|
|
"git.stormux.org/storm/fenrir/internal/datetime"
|
|
"git.stormux.org/storm/fenrir/internal/diagnostic"
|
|
"git.stormux.org/storm/fenrir/internal/i18n"
|
|
"git.stormux.org/storm/fenrir/internal/input"
|
|
"git.stormux.org/storm/fenrir/internal/presentation"
|
|
"git.stormux.org/storm/fenrir/internal/scripts"
|
|
"git.stormux.org/storm/fenrir/internal/version"
|
|
)
|
|
|
|
func (a *application) handle_input(decision input.Decision) {
|
|
started := time.Now()
|
|
defer func() { diagnostic.Timing("input action", started, "command", decision.Command) }()
|
|
if a.quitting {
|
|
return
|
|
}
|
|
diagnostic.Record("input decision", "command", decision.Command, "forward", decision.Forward,
|
|
"interrupt", decision.Interrupt, "keypress", decision.Keypress, "typing", decision.Typing, "deletion", decision.Deletion,
|
|
"ui_active", a.ui.active)
|
|
if decision.Interrupt {
|
|
if decision.Forward && !a.ui.active && a.overrideCurrent != nil {
|
|
if observer, ok := a.overrideCurrent.adapter.(interface{ Note_key(uint16) }); ok {
|
|
observer.Note_key(decision.Code)
|
|
}
|
|
}
|
|
a.spellFeedback++
|
|
a.engine.Reset_progress()
|
|
if a.cues != nil {
|
|
a.cues.Clear_progress()
|
|
}
|
|
a.clipboards.feedback++
|
|
a.voice.Interrupt()
|
|
a.engine.Note_input(decision.Forward && !a.ui.active, decision.Typing, decision.WordBoundary)
|
|
if decision.Forward && decision.Deletion && !a.ui.active {
|
|
a.engine.Note_deletion()
|
|
}
|
|
}
|
|
if decision.Keypress && a.temporarySilence {
|
|
a.temporarySilence = false
|
|
a.present(i18n.T("speech enabled"), presentation.SpeechOn)
|
|
}
|
|
if a.ui.active {
|
|
if a.ui.tutorial != nil {
|
|
if decision.Learn != nil {
|
|
if !decision.Interrupt {
|
|
a.voice.Interrupt()
|
|
}
|
|
a.ui.handle_tutorial(*decision.Learn)
|
|
}
|
|
} else if decision.UI != nil {
|
|
a.ui.handle(*decision.UI)
|
|
}
|
|
return
|
|
}
|
|
a.handle_command(decision.Command)
|
|
}
|
|
|
|
func (a *application) handle_command(id command.ID) {
|
|
if a.overrideTransition && id != "" && id != command.Quit && id != command.Mute {
|
|
a.voice.Say(i18n.T("Settings are busy"))
|
|
return
|
|
}
|
|
if selected := a.overrideCurrent; selected != nil && selected.recognized {
|
|
if result, handled := selected.adapter.Command(id); handled {
|
|
a.present(result.Text, result.Cue)
|
|
return
|
|
}
|
|
}
|
|
switch id {
|
|
case "":
|
|
case command.Help:
|
|
a.ui.open_tutorial(a.options.Bindings, a.options.Scripts, a.settings.live.General.ScriptKeys)
|
|
case command.LastNotification:
|
|
a.last_notification()
|
|
case command.NotificationHistory:
|
|
a.open_notifications()
|
|
case command.Version:
|
|
a.present(i18n.Format("Fenrir screen reader version {0}-{1}", version.Version, version.Channel), "")
|
|
case command.Time:
|
|
text, _ := datetime.Format_time(a.settings.live.Presentation.DateTime.TimeFormat, time.Now())
|
|
a.present(text, "")
|
|
case command.Date:
|
|
text, _ := datetime.Format_date(a.settings.live.Presentation.DateTime.DateFormat, time.Now())
|
|
a.present(text, "")
|
|
case command.Quit:
|
|
if a.saver.busy {
|
|
a.quitAfterSave = true
|
|
} else {
|
|
a.quitting = true
|
|
}
|
|
case command.EmojiMenu:
|
|
a.ui.open_emoji(a.clipboards)
|
|
case command.SpellcheckMenu:
|
|
a.spellMenu.open(a.engine.Spellcheck_word())
|
|
case command.FenrirMenu:
|
|
a.open_settings()
|
|
case command.ApplicationsMenu:
|
|
a.open_applications()
|
|
case command.SaveSettings:
|
|
a.save_settings()
|
|
case command.ProgressMonitor:
|
|
a.quick.mark_changed("presentation.progress.enabled")
|
|
a.settings.live.Presentation.Progress.Enabled = !a.settings.live.Presentation.Progress.Enabled
|
|
a.settings.saveDraft.Presentation.Progress = a.settings.live.Presentation.Progress
|
|
a.engine.Configure_progress(a.settings.live.Presentation.Progress.Enabled)
|
|
if a.settings.live.Presentation.Progress.Enabled {
|
|
a.voice.Say(i18n.T("Progress monitoring enabled"))
|
|
} else {
|
|
a.voice.Say(i18n.T("Progress monitoring disabled"))
|
|
}
|
|
case command.Mute:
|
|
// This toggle lasts only for this session, including when settings
|
|
// are saved. Speech enablement belongs to the settings menu/file.
|
|
a.muted = a.voice.Toggle_mute()
|
|
a.present_speech_state()
|
|
case command.TempDisableSpeech:
|
|
if !a.muted && !a.temporarySilence {
|
|
a.present(i18n.T("speech temporary disabled"), presentation.SpeechOff)
|
|
a.temporarySilence = true
|
|
}
|
|
case command.PassNext:
|
|
a.keyboards.Pass_next()
|
|
a.voice.Say(i18n.T("Forward next keypress"))
|
|
default:
|
|
if a.quick.command(id, &a.settings.live, a.voice) {
|
|
a.apply_quick_settings()
|
|
return
|
|
}
|
|
if script, exists := a.options.Scripts[id]; exists {
|
|
if a.options.ScriptRunner == nil || !a.options.ScriptRunner.Submit(scripts.Invocation{Script: script, Terminal: a.lastTerminal, Generation: a.scriptGeneration}) {
|
|
slog.Warn("script invocation queue full", "at", time.Now())
|
|
a.present(i18n.T("Script failed"), presentation.Error)
|
|
}
|
|
return
|
|
}
|
|
if a.clipboards.command(id) {
|
|
return
|
|
}
|
|
a.review_command(id)
|
|
}
|
|
}
|
|
|
|
func (a *application) review_command(id command.ID) {
|
|
regionRevision := a.engine.Region_revision()
|
|
result := a.engine.Command(id)
|
|
if regionRevision != a.engine.Region_revision() {
|
|
a.reading_regions_changed()
|
|
}
|
|
if message := result.Attributes; message != nil {
|
|
a.present_attributes(*message)
|
|
}
|
|
if message := result.Indentation; message != nil {
|
|
a.present_indentation(*message)
|
|
}
|
|
switch id {
|
|
case command.ToggleTracking:
|
|
a.settings.saveDraft.Reading.Mode = a.engine.Tracking_mode()
|
|
}
|
|
if spelling := result.Spelling; spelling != nil && !a.temporarySilence {
|
|
a.voice.say_spelling(*spelling)
|
|
} else if char := result.Character; char != nil && !a.temporarySilence {
|
|
a.voice.say_character(*char, strings.TrimPrefix(result.Text, presentation.Character(*char)))
|
|
} else {
|
|
a.present(result.Text, result.Cue)
|
|
}
|
|
if after := result.After.Text; after != "" {
|
|
a.present(after, result.After.Cue)
|
|
}
|
|
}
|