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