Cthulhu is no longer a fork. Now it is a screen reader written in Golang.

This commit is contained in:
Storm Dragon committed 2026-10-14 00:16:34 -04:00
1 parent a998fe67de
commit 069fc674dc
708 files changed
+112482 -532

No files matched your search

+161
View File
@@ -0,0 +1,161 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package atspi
/*
#include "adapter_native.h"
*/
import "C"
import (
"context"
"errors"
)
type AccessibleAction struct{ Name, Label string }
type ActionTarget struct {
Object, Window, Focus ObjectID
Epoch uint64
Role string
}
type ActionSet struct {
Target ActionTarget
Actions []AccessibleAction
}
func (adapter *Adapter) AccessibleActions(ctx context.Context, object ObjectID) (ActionSet, error) {
result, err := adapter.documentOperation(ctx, treeCaptureRequest{listActions: true, reviewFocus: object})
return result.actions, err
}
func (adapter *Adapter) PerformAccessibleAction(ctx context.Context, target ActionTarget, name string) error {
_, err := adapter.documentOperation(ctx, treeCaptureRequest{actionTarget: &target, actionName: name})
return err
}
// Native action work stays on the GLib owner, using retained review/document
// handles as well as focus. Never redirect a stale action to a new focus.
func (adapter *Adapter) native_action_source(object ObjectID) (*C.AtspiAccessible, ActionTarget, error) {
var source *C.AtspiAccessible
for _, item := range adapter.nativeFocus {
if item.id == object {
source = item.source
break
}
}
if source == nil {
source = adapter.nativeReview[object]
}
if source == nil {
source = adapter.nativeDocument[object]
}
if source == nil {
return nil, ActionTarget{}, ErrObjectNotFound
}
C.atspi_accessible_clear_cache_single(source)
states := C.atspi_accessible_get_state_set(source)
if states == nil {
return nil, ActionTarget{}, ErrStaleObject
}
defunct := C.atspi_state_set_contains(states, C.ATSPI_STATE_DEFUNCT) != 0
C.g_object_unref(C.gpointer(states))
if defunct {
return nil, ActionTarget{}, ErrStaleObject
}
window := C.cthulhu_window_ancestor(source)
if window == nil {
return nil, ActionTarget{}, ErrStaleObject
}
defer C.g_object_unref(C.gpointer(window))
adapter.refreshForeground()
windowID := nativeObjectID(window)
if !adapter.matchesForeground(window, windowID) {
return nil, ActionTarget{}, ErrStaleObject
}
role := C.atspi_accessible_get_role_name(source, nil)
defer C.g_free(C.gpointer(role))
target := ActionTarget{Object: object, Window: windowID, Epoch: adapter.Foreground().Epoch, Role: C.GoString(role)}
if len(adapter.nativeFocus) != 0 {
target.Focus = adapter.nativeFocus[len(adapter.nativeFocus)-1].id
}
return source, target, nil
}
func (adapter *Adapter) native_actions(ctx context.Context, object ObjectID, expected *ActionTarget, name string) (ActionSet, error) {
if err := ctx.Err(); err != nil {
return ActionSet{}, err
}
source, target, err := adapter.native_action_source(object)
if err != nil {
return ActionSet{}, err
}
if expected != nil && target != *expected {
return ActionSet{}, ErrStaleObject
}
action := C.atspi_accessible_get_action_iface(source)
if action == nil {
return ActionSet{Target: target}, nil
}
defer C.g_object_unref(C.gpointer(action))
var nativeErr *C.GError
count := int(C.atspi_action_get_n_actions(action, &nativeErr))
if nativeErr != nil {
C.g_error_free(nativeErr)
return ActionSet{}, errors.New("could not read accessible actions")
}
if count < 0 || count > 256 {
return ActionSet{}, errors.New("invalid accessible action count")
}
result := ActionSet{Target: target}
seen := map[string]bool{}
for index := 0; index < count; index++ {
if err := ctx.Err(); err != nil {
return ActionSet{}, err
}
rawName := C.atspi_action_get_name(action, C.gint(index), nil)
actionName := sanitizeSnapshotField(C.GoString(rawName))
C.g_free(C.gpointer(rawName))
if actionName == "" || seen[actionName] {
continue
}
seen[actionName] = true
if expected != nil && actionName == name {
if adapter.secure.Load() || ctx.Err() != nil {
return ActionSet{}, ErrStaleObject
}
// Recheck ownership immediately before the external side effect.
_, current, checkErr := adapter.native_action_source(object)
if checkErr != nil || current != target {
return ActionSet{}, ErrStaleObject
}
if C.atspi_action_do_action(action, C.gint(index), &nativeErr) == 0 {
if nativeErr != nil {
C.g_error_free(nativeErr)
}
return ActionSet{}, errors.New("accessible action failed")
}
if nativeErr != nil {
C.g_error_free(nativeErr)
return ActionSet{}, errors.New("accessible action failed")
}
return result, nil
}
labelRaw := C.atspi_action_get_localized_name(action, C.gint(index), nil)
label := sanitizeSnapshotField(C.GoString(labelRaw))
C.g_free(C.gpointer(labelRaw))
if label == "" {
description := C.atspi_action_get_description(action, C.gint(index), nil)
label = sanitizeSnapshotField(C.GoString(description))
C.g_free(C.gpointer(description))
}
if label == "" {
label = actionName
}
result.Actions = append(result.Actions, AccessibleAction{Name: actionName, Label: label})
}
if expected != nil {
return ActionSet{}, ErrStaleObject
}
return result, nil
}