164 lines
4.0 KiB
Go
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
|
|
}
|