Phase 2 of the Wine/Proton stage (decisions 3, 4, 16, 17, 19 in docs/testing/wine-proton.md): replace the Wine manager's startup snapshot with a live settings source read once per poll, so accepted global settings take effect with no reader restart. The source publishes only the active/global profile's Wine values; per-application profile switching never changes the governing list. A newly listed running program restarts its helper once in the same mode; removals and clearing the global switch never downgrade a running helper.
157 lines
5.7 KiB
Go
157 lines
5.7 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()
|
|
}
|
|
|
|
// newWineManager wires the Wine manager to the reader's live settings. The
|
|
// manager reads this source at each poll and helper spawn instead of a
|
|
// startup snapshot, so preferences Apply and equivalent global-settings
|
|
// paths take effect with no reader restart. The source returns only the
|
|
// active/global profile's Wine values: per-application profile switching
|
|
// publishes through liveSettings.Store and never reaches this source.
|
|
func newWineManager(logger *slog.Logger, live *liveSettings) *wine.Manager {
|
|
manager := wine.New(logger)
|
|
manager.SettingsSource = func() wine.Settings {
|
|
if live == nil {
|
|
return wine.Settings{}
|
|
}
|
|
settings := live.LoadGlobalWine()
|
|
return wine.Settings{ControllerOnly: settings.ControllerOnly, ExcludedPrograms: settings.ExcludedPrograms}
|
|
}
|
|
return manager
|
|
}
|
|
|
|
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
|
|
wineEvents.foreground, _ = deps.adapter.(foregroundMonitor)
|
|
wineEvents.titles, _ = deps.adapter.(windowTitleSource)
|
|
if deps.live != nil {
|
|
nativeEditField := deps.live.editFieldFocused
|
|
deps.live.editFieldFocused = func() bool {
|
|
return wineEvents.EditFieldFocused() || nativeEditField != nil && nativeEditField()
|
|
}
|
|
}
|
|
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 := newWineManager(deps.logger, deps.live)
|
|
services = append(services, lifecycle.Service{
|
|
Name: "Wine accessibility", Start: wineManager.Start, Stop: wineManager.Stop,
|
|
})
|
|
return services
|
|
}
|