Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,247 @@
|
||||
// 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 }
|
||||
Reference in new issue
Block a user