// Cthulhu screen reader. // Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // // 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 ( "context" "log/slog" "git.stormux.org/storm/cthulhu/internal/input" ) func processInputEvents( ctx context.Context, logger *slog.Logger, speech speechEnqueuer, listener InputListener, echoSettings input.EchoSettings, live *liveSettings, commands commandListener, ocrSessions ...*ocrSession, ) { echo := newKeyEchoPolicy(echoSettings) var lastOverflow uint64 for { select { case <-ctx.Done(): return case event, ok := <-listener.Events(): if !ok { return } if guarded, ok := speech.(gatedSpeech); ok && guarded.gate.Locked() { echo.Reset() continue } if modal, ok := commands.(interface{ InternalUIEchoActive() bool }); ok && modal.InternalUIEchoActive() { echo.Reset() continue } if learner, ok := commands.(interface{ LearnModeEchoActive() bool }); ok && learner.LearnModeEchoActive() { echo.Reset() continue } if overflow := listener.Overflow(); overflow > lastOverflow { logger.Warn("X11 event queue overflow", "dropped_events", overflow-lastOverflow) lastOverflow = overflow echo.Reset() } if len(ocrSessions) > 0 { ocrSessions[0].Input(event) } if input.ShouldInterruptSpeech(event) { if interrupt, ok := speech.(interface{ InterruptForInput(input.Event) bool }); ok { if !interrupt.InterruptForInput(event) { continue } } else if interrupt, ok := speech.(interface{ InterruptForUser() }); ok { interrupt.InterruptForUser() } else { speech.Interrupt() } logger.Debug("queued speech interrupted by key press", "input_timestamp", event.Timestamp, "input_keycode", event.Keycode, "input_repeat", event.Repeat) } custom := false if live != nil { settings := live.Load() echo.settings = inputEchoSettingsFromConfig(settings) custom = customEcho(settings, "key") } if text := echo.Text(event); text != "" { speech.Enqueue(speechRequest{Text: text, Role: "key", Cause: "key-echo", CustomEcho: custom}) logger.Debug("queued non-printable key echo", "kind", event.Kind) } } } } type keyEchoPolicy struct { settings input.EchoSettings cthulhuModifiers map[string]struct{} heldCthulhuKeys [256]bool } func newKeyEchoPolicy(settings input.EchoSettings) *keyEchoPolicy { modifiers := make(map[string]struct{}, len(settings.CthulhuModifierKeys)) for _, name := range settings.CthulhuModifierKeys { modifiers[name] = struct{}{} } return &keyEchoPolicy{settings: settings, cthulhuModifiers: modifiers} } func (policy *keyEchoPolicy) Text(event input.Event) string { if _, isCthulhuModifier := policy.cthulhuModifiers[event.Name]; isCthulhuModifier { policy.heldCthulhuKeys[event.Keycode] = event.Pressed return "" } for _, held := range policy.heldCthulhuKeys { if held { return "" } } return input.EchoText(event, policy.settings) } func (policy *keyEchoPolicy) Reset() { clear(policy.heldCthulhuKeys[:]) }