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