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,131 @@
|
||||
// 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"
|
||||
"log/slog"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
"git.stormux.org/storm/cthulhu/internal/lifecycle"
|
||||
"git.stormux.org/storm/cthulhu/internal/platform/xorg"
|
||||
"git.stormux.org/storm/cthulhu/internal/sound"
|
||||
)
|
||||
|
||||
// runtimeBackends carries constructed backends only between assembly and Run.
|
||||
// The lifecycle runner owns their start/stop order after construction.
|
||||
type runtimeBackends struct {
|
||||
speechOutput speechEnqueuer
|
||||
brailleOutput *brailleWorker
|
||||
speechMessages *speechWorker
|
||||
history *speechHistory
|
||||
settingsUpdater any
|
||||
inputListener InputListener
|
||||
copyChanges <-chan struct{}
|
||||
sounds soundService
|
||||
services []lifecycle.Service
|
||||
}
|
||||
|
||||
// newRuntimeBackends registers sound, speech, speech worker, input, and the
|
||||
// optional clipboard monitor in that order. The speech worker has a lifetime
|
||||
// independent of the event-loop context so farewell can be delivered.
|
||||
func newRuntimeBackends(options *Options, settings config.Settings, logger *slog.Logger, adapter Adapter) (*runtimeBackends, error) {
|
||||
var speechOutput speechEnqueuer = discardSpeech{}
|
||||
brailleOutput, err := newBrailleWorker(logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
brailleOutput.router, _ = adapter.(brailleRouter)
|
||||
brailleOutput.SetContraction(settings.EnableContractedBraille, settings.BrailleContractionTable)
|
||||
brailleOutput.SetWordWrap(settings.Braille.WordWrap)
|
||||
backends := &runtimeBackends{
|
||||
speechOutput: speechOutput,
|
||||
brailleOutput: brailleOutput,
|
||||
services: make([]lifecycle.Service, 0, 5),
|
||||
}
|
||||
if options.newSound != nil {
|
||||
backends.sounds = options.newSound()
|
||||
} else {
|
||||
backends.sounds = sound.New(logger, sound.NativePlayer{Sink: settings.SoundSink}, settings.SoundTheme, settings.SoundVolume)
|
||||
}
|
||||
if updater, ok := backends.sounds.(soundSettingsUpdater); ok {
|
||||
updater.Configure(soundConfiguration(settings))
|
||||
}
|
||||
backends.services = append(backends.services, lifecycle.Service{Name: "sound", Start: backends.sounds.Start, Stop: func(context.Context) error {
|
||||
soundContext, cancelSound := context.WithTimeout(context.Background(), soundStopTimeout)
|
||||
defer cancelSound()
|
||||
logger.Debug("playing shutdown sound")
|
||||
if err := backends.sounds.Stop(soundContext); err != nil {
|
||||
logger.Warn("stop sound service", "error", err)
|
||||
return err
|
||||
}
|
||||
logger.Debug("shutdown sound completed")
|
||||
return nil
|
||||
}})
|
||||
if options.newSpeaker == nil {
|
||||
options.newSpeaker = func() Speaker {
|
||||
return newConfiguredSpeaker(options.SpeechSocket, settings)
|
||||
}
|
||||
}
|
||||
speaker := options.newSpeaker()
|
||||
backends.settingsUpdater = speaker
|
||||
backends.speechMessages = newSpeechWorker(logger, speaker)
|
||||
backends.speechMessages.SetEnabled(settings.EnableSpeech)
|
||||
if beeper, ok := backends.sounds.(interface{ SpeechBeep(context.Context) error }); ok {
|
||||
backends.speechMessages.beep = beeper.SpeechBeep
|
||||
}
|
||||
backends.history = newSpeechHistory(settings.SpeechHistoryLimit)
|
||||
backends.speechMessages.history = backends.history
|
||||
backends.speechOutput = backends.speechMessages
|
||||
if options.newInput == nil {
|
||||
options.newInput = func() InputListener { return xorg.New() }
|
||||
}
|
||||
backends.inputListener = options.newInput()
|
||||
backends.services = append(backends.services,
|
||||
lifecycle.Service{Name: "speech", Start: func(ctx context.Context) error {
|
||||
if !settings.EnableSpeech {
|
||||
return nil
|
||||
}
|
||||
return speaker.Connect(ctx)
|
||||
}, Stop: speaker.Close},
|
||||
lifecycle.Service{Name: "speech worker", Start: func(context.Context) error {
|
||||
// The runner owns this worker's lifetime. Keeping it independent of
|
||||
// the event-loop context lets graceful shutdown queue its farewell.
|
||||
return backends.speechMessages.Start(context.Background())
|
||||
}, Stop: backends.speechMessages.Stop},
|
||||
)
|
||||
backends.services = append(backends.services,
|
||||
desktopInputService("X11 listener", backends.inputListener, logger),
|
||||
)
|
||||
if _, native := backends.inputListener.(*xorg.Listener); native {
|
||||
clipboardMonitor := xorg.NewClipboardMonitor()
|
||||
backends.services = append(backends.services, lifecycle.Service{
|
||||
Name: "clipboard changes",
|
||||
Start: func(ctx context.Context) error {
|
||||
if err := clipboardMonitor.Start(ctx); err != nil {
|
||||
logger.Warn("copy feedback unavailable", "error", err)
|
||||
return nil
|
||||
}
|
||||
backends.copyChanges = clipboardMonitor.Changes()
|
||||
return nil
|
||||
},
|
||||
Stop: clipboardMonitor.Stop,
|
||||
})
|
||||
}
|
||||
return backends, nil
|
||||
}
|
||||
Reference in new issue
Block a user