248 lines
6.4 KiB
Go
248 lines
6.4 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 (
|
|
"context"
|
|
"sync"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/atspi"
|
|
"git.stormux.org/storm/cthulhu/internal/input"
|
|
"git.stormux.org/storm/cthulhu/internal/sound"
|
|
)
|
|
|
|
type fakeSpeaker struct {
|
|
connectError error
|
|
connected bool
|
|
speakError error
|
|
spoken chan string
|
|
closed bool
|
|
canceled bool
|
|
canceledOnClose bool
|
|
}
|
|
|
|
func (speaker *fakeSpeaker) Connect(context.Context) error {
|
|
speaker.connected = true
|
|
return speaker.connectError
|
|
}
|
|
|
|
func (speaker *fakeSpeaker) Speak(_ context.Context, text string) error {
|
|
if speaker.speakError != nil {
|
|
return speaker.speakError
|
|
}
|
|
speaker.spoken <- text
|
|
return nil
|
|
}
|
|
|
|
func (speaker *fakeSpeaker) Cancel(context.Context) error { speaker.canceled = true; return nil }
|
|
|
|
func (speaker *fakeSpeaker) Close(context.Context) error {
|
|
speaker.canceledOnClose = speaker.canceled
|
|
speaker.closed = true
|
|
return nil
|
|
}
|
|
|
|
type fakeAdapter struct {
|
|
startError error
|
|
stopError error
|
|
started bool
|
|
stopped bool
|
|
events chan atspi.Event
|
|
tree *atspi.AccessibleTree
|
|
captureErr error
|
|
capture func(context.Context) (*atspi.AccessibleTree, error)
|
|
captureSoundGeometry bool
|
|
}
|
|
|
|
type fakeSpeechQueue struct {
|
|
spoken chan string
|
|
requests chan speechRequest
|
|
interrupt chan struct{}
|
|
}
|
|
|
|
type fixedInputActivity struct{ activity input.Activity }
|
|
|
|
func (source fixedInputActivity) RecentInput() input.Activity { return source.activity }
|
|
|
|
type sequencedInputActivity struct {
|
|
mutex sync.Mutex
|
|
sequence uint64
|
|
}
|
|
|
|
func (source *sequencedInputActivity) RecentInput() input.Activity {
|
|
source.mutex.Lock()
|
|
defer source.mutex.Unlock()
|
|
return input.Activity{Sequence: source.sequence}
|
|
}
|
|
|
|
func (source *sequencedInputActivity) set(sequence uint64) {
|
|
source.mutex.Lock()
|
|
source.sequence = sequence
|
|
source.mutex.Unlock()
|
|
}
|
|
|
|
func (queue *fakeSpeechQueue) Enqueue(request speechRequest) {
|
|
if queue.requests != nil {
|
|
queue.requests <- request
|
|
}
|
|
queue.spoken <- request.Text
|
|
}
|
|
|
|
func (queue *fakeSpeechQueue) Interrupt() {
|
|
if queue.interrupt != nil {
|
|
queue.interrupt <- struct{}{}
|
|
}
|
|
}
|
|
|
|
func (adapter *fakeAdapter) Events() <-chan atspi.Event { return adapter.events }
|
|
|
|
func (adapter *fakeAdapter) Overflow() uint64 { return 0 }
|
|
|
|
func (adapter *fakeAdapter) SetCaptureSoundGeometry(enabled bool) {
|
|
adapter.captureSoundGeometry = enabled
|
|
}
|
|
|
|
func (adapter *fakeAdapter) CaptureDesktop(ctx context.Context) (*atspi.AccessibleTree, error) {
|
|
if adapter.capture != nil {
|
|
return adapter.capture(ctx)
|
|
}
|
|
return adapter.tree, adapter.captureErr
|
|
}
|
|
|
|
func (adapter *fakeAdapter) CaptureReviewWindow(ctx context.Context, _ atspi.ObjectID) (*atspi.AccessibleTree, error) {
|
|
return adapter.CaptureDesktop(ctx)
|
|
}
|
|
|
|
func (adapter *fakeAdapter) CaptureDocument(ctx context.Context, _ atspi.ObjectID) (*atspi.AccessibleTree, error) {
|
|
return adapter.CaptureDesktop(ctx)
|
|
}
|
|
|
|
func (adapter *fakeAdapter) CaptureDocumentText(ctx context.Context, _ atspi.ObjectID) (*atspi.AccessibleTree, error) {
|
|
return adapter.CaptureDesktop(ctx)
|
|
}
|
|
|
|
func (adapter *fakeAdapter) Start(context.Context) error {
|
|
adapter.started = true
|
|
return adapter.startError
|
|
}
|
|
|
|
func (adapter *fakeAdapter) Stop(context.Context) error {
|
|
adapter.stopped = true
|
|
return adapter.stopError
|
|
}
|
|
|
|
type fakeInputListener struct {
|
|
events chan input.Event
|
|
modifierKeycodes input.ModifierKeycodes
|
|
startError error
|
|
started bool
|
|
stopped bool
|
|
}
|
|
|
|
func (listener *fakeInputListener) Events() <-chan input.Event {
|
|
return listener.events
|
|
}
|
|
|
|
func (listener *fakeInputListener) ModifierKeycodes() input.ModifierKeycodes {
|
|
return listener.modifierKeycodes
|
|
}
|
|
|
|
func (listener *fakeInputListener) Overflow() uint64 { return 0 }
|
|
|
|
func (listener *fakeInputListener) Start(context.Context) error {
|
|
listener.started = true
|
|
return listener.startError
|
|
}
|
|
|
|
func (listener *fakeInputListener) Stop(context.Context) error {
|
|
listener.stopped = true
|
|
return nil
|
|
}
|
|
|
|
func contains_string(values []string, wanted string) bool {
|
|
for _, value := range values {
|
|
if value == wanted {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type eventDiagnosticsWriter struct {
|
|
started chan struct{}
|
|
startOnce sync.Once
|
|
canceled chan struct{}
|
|
cancelOnce sync.Once
|
|
}
|
|
|
|
func (writer *eventDiagnosticsWriter) Write([]byte) (int, error) {
|
|
writer.startOnce.Do(func() { close(writer.started) })
|
|
<-writer.canceled
|
|
return 0, nil
|
|
}
|
|
|
|
func (writer *eventDiagnosticsWriter) Cancel() error {
|
|
writer.cancelOnce.Do(func() { close(writer.canceled) })
|
|
return nil
|
|
}
|
|
|
|
func (writer *eventDiagnosticsWriter) Close() error { return nil }
|
|
|
|
type silentSound struct{}
|
|
|
|
type rejectingSound struct{ silentSound }
|
|
|
|
type recordingSound struct {
|
|
started chan struct{}
|
|
stopped bool
|
|
stopError error
|
|
}
|
|
|
|
func (sound *recordingSound) Start(context.Context) error {
|
|
close(sound.started)
|
|
return nil
|
|
}
|
|
|
|
func (sound *recordingSound) Stop(context.Context) error {
|
|
sound.stopped = true
|
|
return sound.stopError
|
|
}
|
|
|
|
func (sound *recordingSound) Enqueue(sound.Cue) bool { return true }
|
|
|
|
func (sound *recordingSound) EnqueueBatch([]sound.Cue) bool { return true }
|
|
|
|
func (sound *recordingSound) Available(string) bool { return true }
|
|
|
|
type silentCommands struct{}
|
|
|
|
type fakeCommands struct {
|
|
commands chan input.CommandEvent
|
|
lockStates chan input.LockState
|
|
}
|
|
|
|
func (commands *fakeCommands) Commands() <-chan input.CommandEvent { return commands.commands }
|
|
|
|
func (commands *fakeCommands) LockStates() <-chan input.LockState { return commands.lockStates }
|
|
|
|
func (commands *fakeCommands) Failures() <-chan error { return nil }
|
|
|
|
func (commands *fakeCommands) Start(context.Context) error { return nil }
|
|
|
|
func (commands *fakeCommands) Stop(context.Context) error { return nil }
|