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,228 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package wine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/sessionbus"
|
||||
"github.com/godbus/dbus/v5"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type reviewProvider struct {
|
||||
path dbus.ObjectPath
|
||||
notice ReviewFocusNotice
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
trees map[string]*atspi.AccessibleTree
|
||||
}
|
||||
type ReviewRegistry struct {
|
||||
logger *slog.Logger
|
||||
mutex sync.Mutex
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
monitor, service *dbus.Conn
|
||||
signals chan *dbus.Signal
|
||||
done chan struct{}
|
||||
changes chan struct{}
|
||||
providers map[string]*reviewProvider
|
||||
captureGate chan struct{}
|
||||
captureCall func(context.Context, string, dbus.ObjectPath, ReviewFocusNotice) (map[string]dbus.Variant, error)
|
||||
}
|
||||
|
||||
func NewReviewRegistry(logger *slog.Logger) *ReviewRegistry {
|
||||
return &ReviewRegistry{logger: logger, changes: make(chan struct{}, 1), providers: make(map[string]*reviewProvider), captureGate: make(chan struct{}, 1)}
|
||||
}
|
||||
func (r *ReviewRegistry) notify() {
|
||||
select {
|
||||
case r.changes <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
func (r *ReviewRegistry) Changes() <-chan struct{} { return r.changes }
|
||||
func (r *ReviewRegistry) Start(ctx context.Context) error {
|
||||
if ctx == nil {
|
||||
return errors.New("Wine review requires a context")
|
||||
}
|
||||
conn, err := sessionbus.Connect(r.logger, "wine-review")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options := []dbus.MatchOption{dbus.WithMatchSender("org.freedesktop.DBus"), dbus.WithMatchInterface("org.freedesktop.DBus"), dbus.WithMatchMember("NameOwnerChanged")}
|
||||
signals := make(chan *dbus.Signal, 64)
|
||||
conn.Signal(signals)
|
||||
if err = conn.AddMatchSignal(options...); err != nil {
|
||||
conn.RemoveSignal(signals)
|
||||
conn.Close()
|
||||
return err
|
||||
}
|
||||
r.mutex.Lock()
|
||||
if r.ctx != nil {
|
||||
r.mutex.Unlock()
|
||||
conn.RemoveSignal(signals)
|
||||
conn.Close()
|
||||
return errors.New("Wine review already started")
|
||||
}
|
||||
r.ctx, r.cancel = context.WithCancel(ctx)
|
||||
r.monitor = conn
|
||||
r.signals = signals
|
||||
r.done = make(chan struct{})
|
||||
lifetime, done := r.ctx, r.done
|
||||
r.mutex.Unlock()
|
||||
go func() {
|
||||
defer close(done)
|
||||
for {
|
||||
select {
|
||||
case <-lifetime.Done():
|
||||
return
|
||||
case <-conn.Context().Done():
|
||||
r.retireAll()
|
||||
return
|
||||
case signal := <-signals:
|
||||
if signal != nil && len(signal.Body) == 3 {
|
||||
name, ok := signal.Body[0].(string)
|
||||
old, _ := signal.Body[1].(string)
|
||||
next, _ := signal.Body[2].(string)
|
||||
if ok && name == old && next == "" {
|
||||
r.disappeared(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// The service connection is borrowed because native capture accepts only the
|
||||
// current owner of Cthulhu's well-known name. Registry never closes it.
|
||||
func (r *ReviewRegistry) BindWineReviewConnection(conn *dbus.Conn) {
|
||||
r.mutex.Lock()
|
||||
r.service = conn
|
||||
if conn == nil {
|
||||
for key, p := range r.providers {
|
||||
p.cancel()
|
||||
delete(r.providers, key)
|
||||
}
|
||||
}
|
||||
r.notify()
|
||||
r.mutex.Unlock()
|
||||
}
|
||||
func (r *ReviewRegistry) Stop(ctx context.Context) error {
|
||||
r.mutex.Lock()
|
||||
if r.cancel != nil {
|
||||
r.cancel()
|
||||
}
|
||||
conn, signals, done := r.monitor, r.signals, r.done
|
||||
for key, p := range r.providers {
|
||||
p.cancel()
|
||||
delete(r.providers, key)
|
||||
}
|
||||
r.ctx = nil
|
||||
r.cancel = nil
|
||||
r.monitor = nil
|
||||
r.service = nil
|
||||
r.signals = nil
|
||||
r.done = nil
|
||||
r.notify()
|
||||
r.mutex.Unlock()
|
||||
if conn != nil {
|
||||
conn.RemoveSignal(signals)
|
||||
conn.RemoveMatchSignal(dbus.WithMatchSender("org.freedesktop.DBus"), dbus.WithMatchInterface("org.freedesktop.DBus"), dbus.WithMatchMember("NameOwnerChanged"))
|
||||
conn.Close()
|
||||
}
|
||||
if done != nil {
|
||||
select {
|
||||
case <-done:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (r *ReviewRegistry) RegisterProvider(sender string, path dbus.ObjectPath, version uint32) bool {
|
||||
if !strings.HasPrefix(sender, ":") || !dbus.ObjectPath("/"+strings.ReplaceAll(sender[1:], ".", "/")).IsValid() || path != ReviewObjectPath || version != ReviewProtocolVersion {
|
||||
return false
|
||||
}
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
if r.ctx == nil || r.ctx.Err() != nil {
|
||||
return false
|
||||
}
|
||||
if old := r.providers[sender]; old != nil {
|
||||
old.cancel()
|
||||
}
|
||||
ctx, cancel := context.WithCancel(r.ctx)
|
||||
r.providers[sender] = &reviewProvider{path: path, ctx: ctx, cancel: cancel, trees: make(map[string]*atspi.AccessibleTree)}
|
||||
r.notify()
|
||||
return true
|
||||
}
|
||||
func (r *ReviewRegistry) PresentFocus(sender string, notice ReviewFocusNotice) bool {
|
||||
if !reviewString(notice.Instance, 256, false) || !reviewString(notice.Window, 256, false) || !reviewString(notice.Focus, 256, false) || notice.Revision == 0 || !reviewString(notice.Title, 4096, true) || !reviewString(notice.Role, 4096, false) || !notice.Bounds.Valid() {
|
||||
return false
|
||||
}
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
p := r.providers[sender]
|
||||
if p == nil || p.ctx.Err() != nil {
|
||||
return false
|
||||
}
|
||||
notice.Sender = sender
|
||||
notice.Path = p.path
|
||||
if p.notice == notice {
|
||||
return true
|
||||
}
|
||||
if p.notice.Instance == notice.Instance && p.notice.Revision > notice.Revision {
|
||||
return false
|
||||
}
|
||||
if p.notice.Instance != notice.Instance || p.notice.Window != notice.Window {
|
||||
// Only the active native window can be captured. Retiring older
|
||||
// windows bounds retained text and preserves the current tree's identity.
|
||||
p.trees = make(map[string]*atspi.AccessibleTree)
|
||||
}
|
||||
p.cancel()
|
||||
p.ctx, p.cancel = context.WithCancel(r.ctx)
|
||||
p.notice = notice
|
||||
r.notify()
|
||||
return true
|
||||
}
|
||||
func (r *ReviewRegistry) Focuses() []ReviewFocusNotice {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
var notices []ReviewFocusNotice
|
||||
for _, p := range r.providers {
|
||||
if p.notice.Focus != "" && p.ctx.Err() == nil {
|
||||
notices = append(notices, p.notice)
|
||||
}
|
||||
}
|
||||
sort.Slice(notices, func(i, j int) bool { return notices[i].Sender < notices[j].Sender })
|
||||
return notices
|
||||
}
|
||||
func (r *ReviewRegistry) Current(id atspi.ObjectID) bool {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
p := r.providers[id.BusName]
|
||||
return IsReviewID(id) && p != nil && p.ctx.Err() == nil && p.notice.ID() == id
|
||||
}
|
||||
func (r *ReviewRegistry) disappeared(sender string) {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
if p := r.providers[sender]; p != nil {
|
||||
p.cancel()
|
||||
delete(r.providers, sender)
|
||||
r.notify()
|
||||
}
|
||||
}
|
||||
func (r *ReviewRegistry) retireAll() {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
for sender, p := range r.providers {
|
||||
p.cancel()
|
||||
delete(r.providers, sender)
|
||||
}
|
||||
r.notify()
|
||||
}
|
||||
Reference in new issue
Block a user