// 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 ( "sync" "sync/atomic" "git.stormux.org/storm/cthulhu/internal/input" ) type commandGrabController interface { SetCommandGrabs(bool) } type focusGate struct { tentacleOutput *tentacleOutput ocr *ocrSession mutex sync.Mutex speech speechEnqueuer suspended atomic.Bool secure atomic.Bool lockChanges chan struct{} commands commandGrabController internalUI interface{ InternalUIActive() bool } braille *brailleWorker } func newFocusGate(commands commandListener) *focusGate { gate := &focusGate{lockChanges: make(chan struct{}, 1)} if controller, ok := commands.(commandGrabController); ok { gate.commands = controller } gate.internalUI, _ = commands.(interface{ InternalUIActive() bool }) return gate } func (gate *focusGate) SetSuspended(suspended bool) bool { if gate == nil { return false } if suspended { gate.ocr.Retire() } gate.mutex.Lock() defer gate.mutex.Unlock() if !gate.suspended.CompareAndSwap(!suspended, suspended) { return false } gate.retireTentacleOutput() if suspended { gate.tentacleOutput.Stop() } if suspended && !gate.secure.Load() && gate.speech != nil { gate.speech.Interrupt() } if gate.commands != nil { gate.commands.SetCommandGrabs(!suspended && !gate.secure.Load()) } return true } func (gate *focusGate) InternalUIActive() bool { return gate != nil && gate.internalUI != nil && gate.internalUI.InternalUIActive() } func (gate *focusGate) Suspended() bool { return gate != nil && gate.suspended.Load() } func (gate *focusGate) retireTentacleOutput() { if gate != nil { gate.tentacleOutput.Retire() } } func (gate *focusGate) dismiss_confirmation() { if gate != nil && gate.braille != nil { gate.braille.DismissConfirmation() } } type gatedSpeech struct { speech speechEnqueuer gate *focusGate } func (speaker gatedSpeech) Enqueue(request speechRequest) { if speaker.gate != nil { speaker.gate.mutex.Lock() defer speaker.gate.mutex.Unlock() } if speaker.gate.InternalUIActive() && request.Cause != "internal-ui" { return } if speaker.gate.Locked() || speaker.gate.Suspended() && !request.BypassFocusGate { return } speaker.speech.Enqueue(request) } func (speaker gatedSpeech) Interrupt() { if speaker.gate.InternalUIActive() { return } speaker.InterruptInternalUI() } func (speaker gatedSpeech) InterruptInternalUI() { if speaker.gate != nil { speaker.gate.mutex.Lock() defer speaker.gate.mutex.Unlock() } if !speaker.gate.Locked() && !speaker.gate.Suspended() { speaker.speech.Interrupt() } } func (speaker gatedSpeech) InterruptForUser() { if speaker.gate.Locked() { return } speaker.gate.dismiss_confirmation() if interrupt, ok := speaker.speech.(interface{ InterruptForUser() }); ok { interrupt.InterruptForUser() } else { speaker.Interrupt() } } func (speaker gatedSpeech) InterruptForInput(event input.Event) bool { if speaker.gate.InternalUIActive() { return false } if speaker.gate.Locked() { return false } if speech, ok := speaker.speech.(interface{ InterruptForInput(input.Event) bool }); ok { interrupted := speech.InterruptForInput(event) if interrupted { speaker.gate.dismiss_confirmation() } return interrupted } speaker.InterruptForUser() return true }