// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package atspi /* #cgo pkg-config: atspi-2 glib-2.0 gobject-2.0 #include "adapter_native.h" */ import "C" import ( "context" "unicode/utf8" ) // BrailleRoute refers to the exact source span which produced the display. // Revalidate it on the GLib thread so edits or focus changes cannot route a // delayed hardware key into different content. type BrailleRoute struct { Focus ObjectID Position ObjectReference // Valid only for document browse routing. Line string Start, Offset int } func (adapter *Adapter) RouteBraille(ctx context.Context, route BrailleRoute) error { _, err := adapter.documentOperation(ctx, treeCaptureRequest{brailleRoute: &route}) return err } func validBrailleRoute(snapshot Snapshot, route BrailleRoute) bool { return route.Focus.Valid() && snapshot.ID() == route.Focus && !snapshot.Defunct && snapshot.StatesKnown && snapshot.Focused && snapshot.Role != "password text" && snapshot.TextCaretKnown && snapshot.BrailleLineStart == route.Start && snapshot.BrailleLine == route.Line && route.Start >= 0 && route.Offset >= route.Start && route.Offset-route.Start <= utf8.RuneCountInString(route.Line) } func (adapter *Adapter) routeNativeBraille(ctx context.Context, route BrailleRoute) error { if err := ctx.Err(); err != nil { return err } if route.Position.Valid() { return adapter.route_document_braille(ctx, route) } if !adapter.nativeFocusIs(route.Focus) { return ErrStaleObject } source := adapter.nativeFocus[len(adapter.nativeFocus)-1].source // Request caret capture even for read-only text. Ordinary focus snapshots // deliberately avoid querying non-editable text on the event hot path. snapshot := snapshotFromNativeWithCaret(source, true, true, false, false) if !validBrailleRoute(snapshot, route) { return ErrStaleObject } text := C.atspi_accessible_get_text_iface(source) if text == nil { return ErrNoDocumentCaret } defer C.g_object_unref(C.gpointer(text)) if err := ctx.Err(); err != nil { return err } var nativeErr *C.GError placed := C.atspi_text_set_caret_offset(text, C.gint(route.Offset), &nativeErr) if nativeErr != nil { err := nativeError("route braille caret", nativeErr) C.g_error_free(nativeErr) return err } if placed == 0 { return ErrNoDocumentCaret } return nil } func (adapter *Adapter) route_document_braille(ctx context.Context, route BrailleRoute) error { adapter.treeMutex.Lock() tree := adapter.documentTree adapter.treeMutex.Unlock() if tree == nil || len(adapter.nativeFocus) == 0 { return ErrStaleObject } snapshot, err := tree.Resolve(route.Position) if err != nil || snapshot.Role == "password text" || snapshot.Defunct { return ErrStaleObject } if err = documentPositionMatchesFocus(tree, route.Position, route.Focus); err != nil { return err } latest := adapter.nativeFocus[len(adapter.nativeFocus)-1].id if err = documentPositionMatchesFocus(tree, route.Position, latest); err != nil { return err } source := adapter.nativeDocument[route.Position.ID()] if source == nil { return ErrStaleObject } // Recheck the protected state and foreground before reading fresh content. C.atspi_accessible_clear_cache_single(source) fresh := snapshotFromNativeWithCaret(source, false, false, false, false) if fresh.Role != snapshot.Role || fresh.Defunct || fresh.Role == "password text" { return ErrStaleObject } window := C.cthulhu_window_ancestor(source) if window == nil { return ErrStaleObject } foreground := adapter.matchesForeground(window, nativeObjectID(window)) C.g_object_unref(C.gpointer(window)) if !foreground { return ErrStaleObject } text := C.atspi_accessible_get_text_iface(source) if text == nil { return ErrNoDocumentCaret } defer C.g_object_unref(C.gpointer(text)) length := utf8.RuneCountInString(route.Line) if route.Start < 0 || route.Offset < route.Start || route.Offset > route.Start+length || length == 0 { return ErrStaleObject } count := int(C.atspi_text_get_character_count(text, nil)) if route.Start+length > count { return ErrStaleObject } var nativeErr *C.GError content := C.atspi_text_get_text(text, C.gint(route.Start), C.gint(route.Start+length), &nativeErr) if nativeErr != nil { err := nativeError("validate document braille text", nativeErr) C.g_error_free(nativeErr) return err } unchanged := C.GoString(content) == route.Line C.g_free(C.gpointer(content)) if !unchanged { return ErrStaleObject } return adapter.setNativeDocumentPosition(ctx, route.Position, route.Focus, route.Offset, true) }