// 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" "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 }