Files

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)
}
}