Files
cthulhu/internal/atspi/adapter_snapshot_native.go

190 lines
8.4 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 (
"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)),
)
}