Files
cthulhu/internal/atspi/speech_metadata_native.go

152 lines
5.0 KiB
Go

// 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 (
"strings"
"time"
"unsafe"
)
func native_mnemonic(source *C.AtspiAccessible) string {
action := C.atspi_accessible_get_action_iface(source)
if action == nil {
return ""
}
defer C.g_object_unref(C.gpointer(action))
if C.atspi_action_get_n_actions(action, nil) <= 0 {
return ""
}
binding := C.atspi_action_get_key_binding(action, 0, nil)
defer C.g_free(C.gpointer(binding))
parts := strings.Split(C.GoString(binding), ";")
if len(parts) == 0 {
return ""
}
// AT-SPI's first field is the mnemonic. Empty mnemonic fields must not
// turn an unrelated accelerator into a fabricated mnemonic.
return sanitizeSnapshotField(parts[0])
}
func native_misspelled(text *C.AtspiText, offset int) bool {
run := native_text_attribute_run(text, offset)
return TextAttributesMisspelled(run.Values)
}
func native_text_attribute_run(text *C.AtspiText, offset int) TextAttributeRun {
if offset < 0 {
return TextAttributeRun{}
}
var start, end C.gint
attributes := C.atspi_text_get_attribute_run(text, C.gint(offset), 0, &start, &end, nil)
if attributes == nil {
return TextAttributeRun{}
}
defer C.g_hash_table_unref(attributes)
run := TextAttributeRun{Start: int(start), End: int(end), Values: make(map[string]string)}
for _, key := range spokenCharacterAttributeKeys {
name := C.CString(key)
value := C.g_hash_table_lookup(attributes, C.gconstpointer(unsafe.Pointer(name)))
C.free(unsafe.Pointer(name))
if value != nil {
run.Values[key] = sanitizeSnapshotField(C.GoString((*C.char)(value)))
}
}
return run
}
// Each provider request can block. Stop after a bounded number of runs and a
// short deadline; the caret run is still available if a provider is slow.
func native_text_attribute_runs(text *C.AtspiText, start, end int, caret TextAttributeRun) []TextAttributeRun {
if start < 0 || end <= start {
return nil
}
deadline := time.Now().Add(60 * time.Millisecond)
var runs []TextAttributeRun
for offset := start; offset < end && len(runs) < 32 && time.Now().Before(deadline); {
run := caret
if offset < run.Start || offset >= run.End {
run = native_text_attribute_run(text, offset)
}
if run.End <= offset {
break
}
runs = append(runs, run)
offset = run.End
}
return runs
}
// Queries stay on the GLib owner. This copies only container metadata, bounded
// by depth and time, and never scans children or retains native references.
func native_speech_context(source *C.AtspiAccessible) ([]Snapshot, []Snapshot) {
result := make([]Snapshot, 0)
brailleContext := make([]Snapshot, 0)
speechOpen := true
current := C.atspi_accessible_get_parent(source, nil)
deadline := time.Now().Add(100 * time.Millisecond)
for depth := 0; current != nil && depth < 16 && time.Now().Before(deadline); depth++ {
roleName := C.atspi_accessible_get_role_name(current, nil)
role := C.GoString(roleName)
C.g_free(C.gpointer(roleName))
if role == "application" || role == "frame" || role == "window" || role == "dialog" {
speechOpen = false
}
id := nativeObjectID(current)
item := Snapshot{BusName: id.BusName, Path: id.Path, Role: role}
name := C.atspi_accessible_get_name(current, nil)
item.Name = sanitizeSnapshotField(C.GoString(name))
C.g_free(C.gpointer(name))
if role == "application" {
brailleContext = append(brailleContext, item)
break
}
if speechOpen {
switch role {
case "block quote", "blockquote", "list", "list box", "table", "tree table", "panel", "grouping", "landmark", "form", "section", "document spreadsheet", "link":
if (role == "list" || role == "list box") && time.Now().Before(deadline) {
var childError *C.GError
count := C.atspi_accessible_get_child_count(current, &childError)
item.ChildCount, item.ChildCountKnown = int(count), childError == nil && count >= 0
if childError != nil {
C.g_error_free(childError)
}
}
if role == "section" || role == "form" || role == "landmark" || role == "table" {
attributes := C.atspi_accessible_get_attributes(current, nil)
if attributes != nil {
for _, field := range []struct {
key string
target *string
}{{"xml-roles", &item.XMLRoles}, {"tag", &item.HTMLTag}} {
key := C.CString(field.key)
*field.target = sanitizeSnapshotField(C.GoString((*C.char)(C.g_hash_table_lookup(attributes, C.gconstpointer(unsafe.Pointer(key))))))
C.free(unsafe.Pointer(key))
}
key := C.CString("layout-guess")
item.LayoutTable = C.GoString((*C.char)(C.g_hash_table_lookup(attributes, C.gconstpointer(unsafe.Pointer(key))))) == "true"
C.free(unsafe.Pointer(key))
C.g_hash_table_unref(attributes)
}
}
result = append(result, item)
}
}
brailleContext = append(brailleContext, item)
parent := C.atspi_accessible_get_parent(current, nil)
C.g_object_unref(C.gpointer(current))
current = parent
}
if current != nil {
C.g_object_unref(C.gpointer(current))
}
return result, brailleContext
}