Files

164 lines
4.0 KiB
Go

// 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 <https://www.gnu.org/licenses/gpl-3.0.html>.
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
}