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