// 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 ( "fmt" "strings" "time" ) func nativeError(operation string, nativeError *C.GError) error { if nativeError == nil { return fmt.Errorf("%s failed", operation) } return fmt.Errorf("%s: %s", operation, C.GoString(nativeError.message)) } func snapshotFromNative(source *C.AtspiAccessible) Snapshot { return snapshotFromNativeWithCaret(source, true, false, false, true) } func snapshotFromNativeWithCaret(source *C.AtspiAccessible, text, caretCharacter, documentMetadata, soundGeometry bool, captureAttributes ...bool) Snapshot { return snapshotFromNativeWithOptions(source, text, caretCharacter, documentMetadata, soundGeometry, true, captureAttributes...) } func snapshotFromNativeWithOptions(source *C.AtspiAccessible, text, caretCharacter, documentMetadata, soundGeometry, applicationName bool, captureAttributes ...bool) Snapshot { captureText := C.gboolean(0) if text { captureText = 1 } captureCaretCharacter := C.gboolean(0) if caretCharacter { captureCaretCharacter = 1 } captureDocumentMetadata := C.gboolean(0) if documentMetadata { captureDocumentMetadata = 1 } captureSoundGeometry := C.gboolean(0) if soundGeometry { captureSoundGeometry = 1 } captureApplicationName := C.gboolean(0) if applicationName { captureApplicationName = 1 } captured := C.cthulhu_capture_snapshot(source, captureText, captureCaretCharacter, captureDocumentMetadata, captureSoundGeometry, captureApplicationName) textContent := C.GoString(captured.text) snapshot := NewSnapshotWithCaretCharacter( C.GoString(captured.path), C.GoString(captured.name), C.GoString(captured.role), C.GoString(captured.description), textContent, C.GoString(captured.caretCharacter), ) snapshot.TextCaretKnown = captured.text != nil && captured.textStartOffset >= 0 if text && snapshot.Role != "password text" { switch snapshot.Role { case "button", "push button", "check box", "radio button", "toggle button", "menu item", "check menu item", "radio menu item": snapshot.Mnemonic = native_mnemonic(source) } } if snapshot.TextCaretKnown { captureBrailleLine(&snapshot, textContent, C.GoString(captured.textBeforeCaret), int(captured.textStartOffset)) if text := C.atspi_accessible_get_text(source); text != nil { snapshot.TextAttributeRun = native_text_attribute_run(text, snapshot.BrailleCaret) snapshot.Misspelled = TextAttributesMisspelled(snapshot.TextAttributeRun.Values) if len(captureAttributes) != 0 && captureAttributes[0] { start := snapshot.BrailleLineStart snapshot.TextAttributeRuns = native_text_attribute_runs(text, start, start+len([]rune(snapshot.BrailleLine)), snapshot.TextAttributeRun) } if C.atspi_text_get_n_selections(text, nil) > 0 { if selection := C.atspi_text_get_selection(text, 0, nil); selection != nil { snapshot.SelectionStart, snapshot.SelectionEnd = int(selection.start_offset), int(selection.end_offset) if snapshot.Role != "password text" && snapshot.SelectionEnd > snapshot.SelectionStart { end := min(snapshot.SelectionEnd, snapshot.SelectionStart+maxSnapshotFieldLength) selected := C.atspi_text_get_text(text, C.gint(snapshot.SelectionStart), C.gint(end), nil) snapshot.SelectionText = strings.ToValidUTF8(C.GoString(selected), "�") C.g_free(C.gpointer(selected)) } C.g_free(C.gpointer(selection)) } } C.g_object_unref(C.gpointer(text)) } } if caretCharacter && captured.editable == 0 && snapshot.Role != "terminal" && strings.ContainsRune(textContent, '\ufffc') { budget := embeddedTextBudget{nodes: 32, characters: maxSnapshotFieldLength, deadline: time.Now().Add(100 * time.Millisecond)} // Hypertext offsets refer to raw characters, before whitespace folding. snapshot.Text = sanitizeSnapshotField(expandNativeEmbeddedText(source, textContent, int(captured.textStartOffset), 0, &budget)) } snapshot.BusName = sanitizeSnapshotField(C.GoString(captured.busName)) if snapshot.Role == "radio button" { group := C.cthulhu_radio_group_name(source) snapshot.RadioGroupName = sanitizeSnapshotField(C.GoString(group)) C.g_free(C.gpointer(group)) } snapshot.ApplicationName = sanitizeSnapshotField(C.GoString(captured.applicationName)) snapshot.Visible = captured.visible != 0 snapshot.Showing = captured.showing != 0 snapshot.StatesKnown = captured.statesKnown != 0 snapshot.Active = captured.active != 0 snapshot.Autocomplete = captured.autocomplete != 0 && snapshot.Role == "entry" snapshot.Focusable = captured.focusable != 0 snapshot.Focused = captured.focused != 0 snapshot.HasPopup = captured.hasPopup != 0 snapshot.Checked = captured.checked != 0 snapshot.Defunct = captured.defunct != 0 snapshot.Editable = captured.editable != 0 snapshot.Expandable = captured.expandable != 0 snapshot.Expanded = captured.expanded != 0 snapshot.Indeterminate = captured.indeterminate != 0 snapshot.Invalid = captured.invalid != 0 snapshot.Multiselectable = captured.multiselectable != 0 snapshot.ReadOnly = captured.readOnly != 0 snapshot.Required = captured.required != 0 snapshot.Selected = captured.selected != 0 snapshot.Sensitive = captured.sensitive != 0 snapshot.SensitiveKnown = captured.sensitiveKnown != 0 snapshot.ValueKnown = captured.valueKnown != 0 snapshot.Visited = captured.visited != 0 snapshot.ExplicitName = captured.explicitName != 0 snapshot.HeadingLevel = int(captured.headingLevel) snapshot.PositionInSet = int(captured.positionInSet) snapshot.SetSize = int(captured.setSize) snapshot.SupportsClick = captured.supportsClick != 0 snapshot.LiveRegion = captured.liveRegion != 0 snapshot.LayoutTable = captured.layoutTable != 0 snapshot.ChildCount = int(captured.childCount) snapshot.ChildCountKnown = captured.childCountKnown != 0 snapshot.TableRowCount = int(captured.tableRowCount) snapshot.TableColumnCount = int(captured.tableColumnCount) snapshot.TableRowIndex = int(captured.tableRowIndex) snapshot.TableColumnIndex = int(captured.tableColumnIndex) snapshot.TableRowSpan = int(captured.tableRowSpan) snapshot.TableColumnSpan = int(captured.tableColumnSpan) snapshot.TablePositionKnown = captured.tablePositionKnown != 0 snapshot.TableRowHeaders = sanitizeSnapshotField(C.GoString(captured.tableRowHeaders)) snapshot.TableColumnHeaders = sanitizeSnapshotField(C.GoString(captured.tableColumnHeaders)) C.g_free(C.gpointer(captured.tableRowHeaders)) C.g_free(C.gpointer(captured.tableColumnHeaders)) snapshot.CharacterCount = int(captured.characterCount) snapshot.EmbeddedObjectCount = int(captured.embeddedObjectCount) snapshot.XMLRoles = sanitizeSnapshotField(C.GoString(captured.xmlRoles)) snapshot.HTMLTag = sanitizeSnapshotField(C.GoString(captured.htmlTag)) snapshot.ElementID = sanitizeSnapshotField(C.GoString(captured.elementID)) snapshot.CurrentValue = float64(captured.currentValue) snapshot.MaximumValue = float64(captured.maximumValue) snapshot.MinimumValue = float64(captured.minimumValue) snapshot.MinimumIncrement = float64(captured.minimumIncrement) snapshot.ValueText = sanitizeSnapshotField(C.GoString(captured.valueText)) snapshot.TextBeforeCaret = sanitizeTextContext(C.GoString(captured.textBeforeCaret)) snapshot.SoundGeometry = soundGeometryFromNative(captured) C.g_free(C.gpointer(captured.applicationName)) C.g_free(C.gpointer(captured.name)) C.g_free(C.gpointer(captured.role)) C.g_free(C.gpointer(captured.description)) C.g_free(C.gpointer(captured.text)) C.g_free(C.gpointer(captured.textBeforeCaret)) C.g_free(C.gpointer(captured.caretCharacter)) C.g_free(C.gpointer(captured.valueText)) C.g_free(C.gpointer(captured.xmlRoles)) C.g_free(C.gpointer(captured.htmlTag)) C.g_free(C.gpointer(captured.elementID)) return snapshot } func soundGeometryFromNative(captured C.cthulhu_snapshot) SoundGeometry { return SoundGeometry{ Object: Rectangle{X: int32(captured.objectX), Y: int32(captured.objectY), Width: int32(captured.objectWidth), Height: int32(captured.objectHeight)}, Reference: Rectangle{X: int32(captured.referenceX), Y: int32(captured.referenceY), Width: int32(captured.referenceWidth), Height: int32(captured.referenceHeight)}, ObjectKnown: captured.objectGeometryKnown != 0, ReferenceKnown: captured.referenceGeometryKnown != 0, } } func nativeObjectID(source *C.AtspiAccessible) ObjectID { return NewObjectID( C.GoString(C.cthulhu_accessible_bus_name(source)), C.GoString(C.cthulhu_accessible_path(source)), ) }