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