// 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" "unsafe" ) // CharacterAttributes is a copy of the current AT-SPI text attribute run. // Native handles remain on the GLib thread. type CharacterAttributes struct { Object ObjectID Values map[string]string } var spokenCharacterAttributeKeys = [...]string{ "size", "family-name", "weight", "indent", "underline", "strikethrough", "justification", "style", "paragraph-style", "text-spelling", "fg-color", "bg-color", "invalid", "invalid-spelling", "spelling", } func (adapter *Adapter) ReadCharacterAttributes(ctx context.Context, object ObjectID) (CharacterAttributes, error) { if !object.Valid() { return CharacterAttributes{}, ErrStaleObject } result, err := adapter.documentOperation(ctx, treeCaptureRequest{reviewFocus: object, readAttributes: true}) return result.attributes, err } func (adapter *Adapter) readNativeCharacterAttributes(ctx context.Context, object ObjectID) (CharacterAttributes, error) { if err := ctx.Err(); err != nil { return CharacterAttributes{}, err } if len(adapter.nativeFocus) == 0 || adapter.nativeFocus[len(adapter.nativeFocus)-1].id.BusName != object.BusName { return CharacterAttributes{}, ErrStaleObject } var source *C.AtspiAccessible if adapter.nativeFocusIs(object) { source = adapter.nativeFocus[len(adapter.nativeFocus)-1].source } else { source = adapter.nativeDocument[object] } if source == nil { return CharacterAttributes{}, ErrStaleObject } if C.atspi_accessible_get_role(source, nil) == C.ATSPI_ROLE_PASSWORD_TEXT { return CharacterAttributes{Object: object}, nil } text := C.atspi_accessible_get_text_iface(source) if text == nil { return CharacterAttributes{Object: object}, nil } defer C.g_object_unref(C.gpointer(text)) var nativeErr *C.GError count := C.atspi_text_get_character_count(text, &nativeErr) if nativeErr != nil { err := nativeError("read character attribute text length", nativeErr) C.g_error_free(nativeErr) return CharacterAttributes{}, err } if count <= 0 { return CharacterAttributes{Object: object}, nil } offset := C.atspi_text_get_caret_offset(text, &nativeErr) if nativeErr != nil { err := nativeError("read character attribute caret", nativeErr) C.g_error_free(nativeErr) return CharacterAttributes{}, err } if offset < 0 || offset >= count { offset = count - 1 } var start, end C.gint attributes := C.atspi_text_get_attribute_run(text, offset, 1, &start, &end, &nativeErr) if nativeErr != nil { err := nativeError("read character attributes", nativeErr) C.g_error_free(nativeErr) return CharacterAttributes{}, err } if attributes == nil { return CharacterAttributes{Object: object}, nil } defer C.g_hash_table_unref(attributes) result := CharacterAttributes{Object: object, 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 { result.Values[key] = sanitizeSnapshotField(C.GoString((*C.char)(value))) } } return result, ctx.Err() }