Files
cthulhu/internal/app/runtime_remote.go

131 lines
4.6 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"
"log/slog"
"strconv"
"time"
"git.stormux.org/storm/cthulhu/internal/lifecycle"
"git.stormux.org/storm/cthulhu/internal/nvda2cthulhu"
"git.stormux.org/storm/cthulhu/internal/remote"
"git.stormux.org/storm/cthulhu/internal/wine"
)
// remoteServiceDependencies are held by the service closures for the lifetime
// of the runner. The input and adapter counters are read only on snapshot calls.
type remoteServiceDependencies struct {
logger *slog.Logger
stopRun context.CancelFunc
adapter Adapter
inputListener InputListener
speech *speechWorker
braille *brailleWorker
live *liveSettings
preferences *preferencesWorker
onPreferences func()
}
func newRemoteRuntimeServices(deps remoteServiceDependencies) []lifecycle.Service {
remoteHandler := remoteSpeech{braille: lockFeedbackBraille{deps.braille}}
remoteHandler.live = deps.live
remoteHandler.preferences = deps.preferences
remoteHandler.showPreferences = deps.onPreferences
remoteHandler.quit = func() { time.AfterFunc(100*time.Millisecond, deps.stopRun) }
remoteHandler.runtimeSnapshot = func() string {
snapshot := "CTHULHU RUNTIME STATE SNAPSHOT\n[event_manager]\noverflow: " + strconv.FormatUint(deps.adapter.Overflow(), 10)
if deps.inputListener != nil {
snapshot += "\n[input_event_manager]\noverflow: " + strconv.FormatUint(deps.inputListener.Overflow(), 10)
}
return snapshot
}
if deps.speech != nil {
remoteHandler.speech = deps.speech
}
wineEvents := newWineEventPresenter(func(message string) bool {
return remoteHandler.presentMessage(message, "wine-accessible-event")
})
wineEvents.logger = deps.logger
connectWineWindowTitles(wineEvents, deps.live, deps.adapter)
remoteHandler.wine = wineEvents
if deps.live != nil {
remoteHandler.wineReview = deps.live.wineReview
}
remoteService := remote.New(deps.logger, remoteHandler)
services := []lifecycle.Service{{
Name: "self-voice D-Bus",
Start: func(ctx context.Context) error {
if err := remoteService.Start(ctx); err != nil {
deps.logger.Warn("Cthulhu D-Bus service unavailable", "error", err)
}
return nil
},
Stop: func(ctx context.Context) error {
wineEvents.Close()
if err := remoteService.Stop(ctx); err != nil {
deps.logger.Warn("Cthulhu D-Bus service shutdown", "error", err)
}
return nil
},
}}
if remoteHandler.wineReview != nil {
registry := remoteHandler.wineReview
// Reverse shutdown cancels captures before the borrowed service bus closes.
services = append(services, lifecycle.Service{Name: "Wine flat review", Start: func(ctx context.Context) error {
if err := registry.Start(ctx); err != nil {
deps.logger.Warn("Wine flat review unavailable", "error", err)
}
return nil
}, Stop: registry.Stop})
}
orcaService := remote.NewOrca(deps.logger, remoteHandler)
services = append(services, lifecycle.Service{
Name: "Orca compatibility D-Bus",
Start: func(ctx context.Context) error {
if err := orcaService.Start(ctx); err != nil {
deps.logger.Warn("Orca compatibility D-Bus service unavailable", "error", err)
}
return nil
},
Stop: func(ctx context.Context) error {
if err := orcaService.Stop(ctx); err != nil {
deps.logger.Warn("Orca compatibility D-Bus service shutdown", "error", err)
}
return nil
},
})
bridgeServer := nvda2cthulhu.New(deps.logger, remoteHandler)
services = append(services, lifecycle.Service{
Name: "NVDA to Cthulhu bridge",
Start: func(ctx context.Context) error {
if err := bridgeServer.Start(ctx); err != nil {
deps.logger.Warn("NVDA to Cthulhu bridge unavailable", "error", err)
}
return nil
},
Stop: bridgeServer.Stop,
})
wineManager := wine.New(deps.logger)
services = append(services, lifecycle.Service{
Name: "Wine accessibility", Start: wineManager.Start, Stop: wineManager.Stop,
})
return services
}