// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later #include "adapter_native.h" static gchar *cthulhu_capture_text(AtspiAccessible *source, gchar **beforeCaret, gint *startOffset); static void cthulhu_capture_document_metrics(AtspiAccessible *source, cthulhu_snapshot *snapshot); static gchar *cthulhu_table_cell_label(AtspiAccessible *source, guint depth, guint *remaining); static gchar *cthulhu_label_text(AtspiAccessible *source); // Some toolkits expose an edit field's label only as a relation, leaving its // accessible name empty. Follow direct targets only, with a shared query and // output budget; never infer a label from neighbouring controls or field text. static gchar *cthulhu_edit_field_label(AtspiAccessible *source) { GArray *relations = atspi_accessible_get_relation_set(source, NULL); if (relations == NULL) return NULL; GString *label = g_string_new(""); guint remaining = 16; for (guint i = 0; i < MIN(relations->len, 32) && remaining > 0 && label->len < 4096; i++) { AtspiRelation *relation = g_array_index(relations, AtspiRelation *, i); if (atspi_relation_get_relation_type(relation) != ATSPI_RELATION_LABELLED_BY) continue; gint count = atspi_relation_get_n_targets(relation); for (gint j = 0; j < count && remaining > 0 && label->len < 4096; j++) { remaining--; AtspiAccessible *target = atspi_relation_get_target(relation, j); if (target == NULL) continue; AtspiRole role = atspi_accessible_get_role(target, NULL); gchar *text = NULL; if (role != ATSPI_ROLE_PASSWORD_TEXT) { text = atspi_accessible_get_name(target, NULL); if ((text == NULL || *text == '\0') && (role == ATSPI_ROLE_LABEL || role == ATSPI_ROLE_STATIC)) { g_free(text); text = cthulhu_label_text(target); } } if (text != NULL && *text != '\0') { if (label->len != 0) g_string_append_c(label, ' '); const gchar *end = NULL; g_utf8_validate(text, MIN(strlen(text), 4096 - label->len), &end); g_string_append_len(label, text, end - text); } g_free(text); g_object_unref(target); } } for (guint i = 0; i < relations->len; i++) g_object_unref(g_array_index(relations, AtspiRelation *, i)); g_array_unref(relations); return g_string_free(label, FALSE); } // Collect only the selected row. Walking an entire table on every arrow key // would block input in large application tables. gchar *cthulhu_table_context(AtspiAccessible *source, gint *rows, gint *columns, gchar **tableName, gchar **tablePath, gint *rowIndex, gint *columnIndex, gboolean *guiTable) { AtspiAccessible *tableAccessible = NULL; gint row = -1; gint column = -1; if (atspi_accessible_get_role(source, NULL) == ATSPI_ROLE_TABLE) { tableAccessible = g_object_ref(source); } else if (atspi_accessible_get_role(source, NULL) == ATSPI_ROLE_TABLE_CELL) { AtspiTableCell *cell = atspi_accessible_get_table_cell(source); if (cell != NULL) { tableAccessible = atspi_table_cell_get_table(cell, NULL); atspi_table_cell_get_position(cell, &row, &column, NULL); g_object_unref(cell); } if (tableAccessible == NULL) { AtspiAccessible *parent = atspi_accessible_get_parent(source, NULL); if (parent != NULL && atspi_accessible_get_role(parent, NULL) == ATSPI_ROLE_TABLE) { tableAccessible = parent; gint index = atspi_accessible_get_index_in_parent(source, NULL); AtspiTable *parentTable = atspi_accessible_get_table_iface(parent); if (parentTable != NULL) { row = atspi_table_get_row_at_index(parentTable, index, NULL); column = atspi_table_get_column_at_index(parentTable, index, NULL); g_object_unref(parentTable); } } else if (parent != NULL) { g_object_unref(parent); } } } if (tableAccessible == NULL) return NULL; *guiTable = TRUE; AtspiAccessible *ancestor = g_object_ref(tableAccessible); for (gint depth = 0; depth < 16 && ancestor != NULL; depth++) { AtspiRole role = atspi_accessible_get_role(ancestor, NULL); if (role == ATSPI_ROLE_DOCUMENT_WEB || role == ATSPI_ROLE_DOCUMENT_FRAME || role == ATSPI_ROLE_DOCUMENT_TEXT || role == ATSPI_ROLE_DOCUMENT_SPREADSHEET) { *guiTable = FALSE; g_object_unref(ancestor); ancestor = NULL; break; } AtspiAccessible *parent = atspi_accessible_get_parent(ancestor, NULL); g_object_unref(ancestor); ancestor = parent; } if (ancestor != NULL) g_object_unref(ancestor); AtspiTable *table = atspi_accessible_get_table_iface(tableAccessible); if (table == NULL) { g_object_unref(tableAccessible); return NULL; } *rows = atspi_table_get_n_rows(table, NULL); *columns = atspi_table_get_n_columns(table, NULL); *tableName = atspi_accessible_get_name(tableAccessible, NULL); *tablePath = g_strdup(cthulhu_accessible_path(tableAccessible)); *rowIndex = row; *columnIndex = column; GString *result = g_string_new(NULL); if (row >= 0 && row < *rows && *columns > 0 && *columns <= 32) { guint remaining = 32; for (gint column = 0; column < *columns; column++) { AtspiAccessible *item = atspi_table_get_accessible_at(table, row, column, NULL); if (item == NULL) continue; gchar *name = atspi_accessible_get_name(item, NULL); if ((name == NULL || *name == '\0') && remaining != 0 && atspi_accessible_get_role(item, NULL) == ATSPI_ROLE_TABLE_CELL) { g_free(name); name = cthulhu_table_cell_label(item, 0, &remaining); } if (name != NULL && *name != '\0' && result->len + strlen(name) < 4096) { if (result->len > 0) g_string_append(result, ", "); g_string_append(result, name); } g_free(name); g_object_unref(item); } } g_object_unref(table); g_object_unref(tableAccessible); return g_string_free(result, FALSE); } static gboolean cthulhu_capture_sound_rect(AtspiAccessible *source, gint *x, gint *y, gint *width, gint *height) { AtspiStateSet *states = atspi_accessible_get_state_set(source); if (states == NULL) return FALSE; gboolean available = atspi_state_set_contains(states, ATSPI_STATE_VISIBLE) && atspi_state_set_contains(states, ATSPI_STATE_SHOWING); g_object_unref(states); if (!available) return FALSE; AtspiComponent *component = atspi_accessible_get_component_iface(source); if (component == NULL) return FALSE; GError *error = NULL; AtspiRect *rect = atspi_component_get_extents(component, ATSPI_COORD_TYPE_SCREEN, &error); g_object_unref(component); if (error != NULL) { g_clear_error(&error); if (rect != NULL) g_free(rect); return FALSE; } if (rect == NULL || rect->width <= 0 || rect->height <= 0 || rect->x == G_MININT || rect->y == G_MININT) { if (rect != NULL) g_free(rect); return FALSE; } *x = rect->x; *y = rect->y; *width = rect->width; *height = rect->height; g_free(rect); return TRUE; } void cthulhu_capture_sound_geometry(AtspiAccessible *source, cthulhu_snapshot *snapshot) { snapshot->objectGeometryKnown = cthulhu_capture_sound_rect( source, &snapshot->objectX, &snapshot->objectY, &snapshot->objectWidth, &snapshot->objectHeight); if (!snapshot->objectGeometryKnown) return; AtspiAccessible *current = g_object_ref(source); for (gint depth = 0; current != NULL && depth < 256; depth++) { GError *error = NULL; AtspiRole role = atspi_accessible_get_role(current, &error); if (error != NULL) { g_clear_error(&error); g_object_unref(current); return; } if (role == ATSPI_ROLE_DIALOG || role == ATSPI_ROLE_FRAME || role == ATSPI_ROLE_WINDOW) { snapshot->referenceGeometryKnown = cthulhu_capture_sound_rect( current, &snapshot->referenceX, &snapshot->referenceY, &snapshot->referenceWidth, &snapshot->referenceHeight); g_object_unref(current); return; } AtspiAccessible *parent = atspi_accessible_get_parent(current, &error); g_object_unref(current); if (error != NULL) { g_clear_error(&error); if (parent != NULL) g_object_unref(parent); return; } current = parent; } if (current != NULL) g_object_unref(current); } static void cthulhu_capture_value(AtspiAccessible *source, cthulhu_snapshot *snapshot) { GError *error = NULL; AtspiRole role = atspi_accessible_get_role(source, &error); if (error != NULL) { g_error_free(error); return; } switch (role) { case ATSPI_ROLE_COMBO_BOX: { // Many applications expose the selected item only through Selection. // Prefer an already supplied description so well-behaved controls do // not pay another synchronous AT-SPI round trip on every focus event. if (snapshot->description != NULL && *snapshot->description != '\0') { return; } AtspiSelection *selection = atspi_accessible_get_selection_iface(source); if (selection == NULL) return; AtspiAccessible *selected = atspi_selection_get_selected_child(selection, 0, &error); g_object_unref(selection); if (error != NULL) { g_clear_error(&error); if (selected != NULL) g_object_unref(selected); return; } if (selected == NULL) return; snapshot->valueText = atspi_accessible_get_name(selected, &error); g_object_unref(selected); if (error != NULL) { g_clear_error(&error); snapshot->valueText = NULL; } return; } case ATSPI_ROLE_DIAL: case ATSPI_ROLE_PROGRESS_BAR: case ATSPI_ROLE_SCROLL_BAR: case ATSPI_ROLE_SLIDER: break; case ATSPI_ROLE_SPIN_BUTTON: if (snapshot->text != NULL && *snapshot->text != '\0') { return; } break; case ATSPI_ROLE_SPLIT_PANE: break; default: return; } AtspiValue *value = atspi_accessible_get_value_iface(source); if (value == NULL) { return; } error = NULL; snapshot->valueText = atspi_value_get_text(value, &error); if (error != NULL) { g_error_free(error); error = NULL; } snapshot->currentValue = atspi_value_get_current_value(value, &error); if (error != NULL) { g_error_free(error); error = NULL; g_object_unref(value); return; } snapshot->minimumValue = atspi_value_get_minimum_value(value, &error); if (error != NULL) { g_error_free(error); error = NULL; g_object_unref(value); return; } snapshot->maximumValue = atspi_value_get_maximum_value(value, &error); if (error == NULL) { snapshot->valueKnown = TRUE; } else { g_clear_error(&error); } snapshot->minimumIncrement = atspi_value_get_minimum_increment(value, &error); if (error != NULL) { g_clear_error(&error); snapshot->minimumIncrement = (snapshot->maximumValue - snapshot->minimumValue) / 100.0; } g_object_unref(value); } // GTK tree rows can be unnamed while their renderer cells hold the visible // label (including file names in Thunar's details view). Bound the lookup so // a large or broken table cannot stall the event callback. static gchar *cthulhu_table_cell_label(AtspiAccessible *source, guint depth, guint *remaining) { if (source == NULL || *remaining == 0) return NULL; (*remaining)--; if (depth != 0) { GError *error = NULL; gchar *name = atspi_accessible_get_name(source, &error); if (error != NULL) g_clear_error(&error); if (name != NULL && *g_strstrip(name) != '\0') return name; g_free(name); } if (depth >= 2) return NULL; GError *error = NULL; gint count = atspi_accessible_get_child_count(source, &error); if (error != NULL) { g_clear_error(&error); return NULL; } for (gint index = 0; index < count && index < 12 && *remaining != 0; index++) { AtspiAccessible *child = atspi_accessible_get_child_at_index(source, index, &error); if (error != NULL) { g_clear_error(&error); continue; } if (child == NULL) continue; gchar *name = cthulhu_table_cell_label(child, depth + 1, remaining); g_object_unref(child); if (name != NULL) return name; } return NULL; } // Document cells can contain several text/link children. Collect their labels // within a fixed budget, rather than the GUI row helper's first-label shortcut. static gchar *cthulhu_document_cell_label(AtspiAccessible *source, guint depth, guint *remaining) { if (!source || !*remaining || depth > 8) return NULL; (*remaining)--; gchar *name = atspi_accessible_get_name(source, NULL); if (name && *g_strstrip(name)) return name; g_free(name); GString *text = g_string_new(NULL); gint count = atspi_accessible_get_child_count(source, NULL); for (gint i = 0; i < count && *remaining; i++) { AtspiAccessible *child = atspi_accessible_get_child_at_index(source, i, NULL); if (!child) continue; gchar *label = cthulhu_document_cell_label(child, depth + 1, remaining); g_object_unref(child); if (label && *label) { if (text->len) g_string_append_c(text, ' '); g_string_append(text, label); } g_free(label); } return g_string_free(text, FALSE); } // Header arrays own their accessible references. Copy labels before release. static gchar *cthulhu_table_headers(GPtrArray *headers) { if (!headers) return NULL; GString *text = g_string_new(NULL); guint remaining = 32; for (guint i = 0; i < headers->len && remaining; i++) { gchar *label = cthulhu_document_cell_label(g_ptr_array_index(headers, i), 0, &remaining); if (label && *label) { if (text->len) g_string_append(text, ", "); g_string_append(text, label); } g_free(label); } g_ptr_array_unref(headers); return g_string_free(text, FALSE); } cthulhu_snapshot cthulhu_capture_snapshot(AtspiAccessible *source, gboolean captureText, gboolean captureCaretCharacter, gboolean captureDocumentMetadata, gboolean captureSoundGeometry, gboolean captureApplicationName) { cthulhu_snapshot snapshot = {0}; if (source == NULL) { return snapshot; } snapshot.path = ATSPI_OBJECT(source)->path; if (ATSPI_OBJECT(source)->app != NULL) { snapshot.busName = ATSPI_OBJECT(source)->app->bus_name; if (captureApplicationName) { GError *applicationError = NULL; AtspiAccessible *application = atspi_accessible_get_application(source, &applicationError); if (applicationError != NULL) { g_clear_error(&applicationError); } if (application != NULL) { snapshot.applicationName = atspi_accessible_get_name(application, &applicationError); if (applicationError != NULL) { g_clear_error(&applicationError); } g_object_unref(application); } } } AtspiStateSet *states = atspi_accessible_get_state_set(source); if (states != NULL) { snapshot.statesKnown = TRUE; snapshot.visible = atspi_state_set_contains(states, ATSPI_STATE_VISIBLE); snapshot.showing = atspi_state_set_contains(states, ATSPI_STATE_SHOWING); snapshot.active = atspi_state_set_contains(states, ATSPI_STATE_ACTIVE); snapshot.autocomplete = atspi_state_set_contains(states, ATSPI_STATE_SUPPORTS_AUTOCOMPLETION) && atspi_state_set_contains(states, ATSPI_STATE_SINGLE_LINE); snapshot.focusable = atspi_state_set_contains(states, ATSPI_STATE_FOCUSABLE); snapshot.focused = atspi_state_set_contains(states, ATSPI_STATE_FOCUSED); snapshot.hasPopup = atspi_state_set_contains(states, ATSPI_STATE_HAS_POPUP); snapshot.sensitiveKnown = TRUE; snapshot.checked = atspi_state_set_contains(states, ATSPI_STATE_CHECKED) || atspi_state_set_contains(states, ATSPI_STATE_PRESSED); snapshot.defunct = atspi_state_set_contains(states, ATSPI_STATE_DEFUNCT); snapshot.editable = atspi_state_set_contains(states, ATSPI_STATE_EDITABLE); snapshot.expandable = atspi_state_set_contains(states, ATSPI_STATE_EXPANDABLE) || atspi_state_set_contains(states, ATSPI_STATE_EXPANDED); snapshot.expanded = atspi_state_set_contains(states, ATSPI_STATE_EXPANDED); snapshot.indeterminate = atspi_state_set_contains(states, ATSPI_STATE_INDETERMINATE); snapshot.invalid = atspi_state_set_contains(states, ATSPI_STATE_INVALID_ENTRY) || atspi_state_set_contains(states, ATSPI_STATE_INVALID); snapshot.multiselectable = atspi_state_set_contains(states, ATSPI_STATE_MULTISELECTABLE); snapshot.readOnly = atspi_state_set_contains(states, ATSPI_STATE_READ_ONLY); snapshot.required = atspi_state_set_contains(states, ATSPI_STATE_REQUIRED); snapshot.selected = atspi_state_set_contains(states, ATSPI_STATE_SELECTED); snapshot.sensitive = atspi_state_set_contains(states, ATSPI_STATE_SENSITIVE); snapshot.visited = atspi_state_set_contains(states, ATSPI_STATE_VISITED); g_object_unref(states); } GError *error = NULL; snapshot.name = atspi_accessible_get_name(source, &error); if (error != NULL) { g_error_free(error); error = NULL; } snapshot.role = atspi_accessible_get_role_name(source, &error); if (error != NULL) { g_error_free(error); error = NULL; } if (!snapshot.defunct && (snapshot.name == NULL || *snapshot.name == '\0') && (g_strcmp0(snapshot.role, "text") == 0 || g_strcmp0(snapshot.role, "entry") == 0 || g_strcmp0(snapshot.role, "password text") == 0)) { g_free(snapshot.name); snapshot.name = cthulhu_edit_field_label(source); } if (!snapshot.defunct && (g_strcmp0(snapshot.role, "list") == 0 || g_strcmp0(snapshot.role, "list box") == 0)) { snapshot.childCount = atspi_accessible_get_child_count(source, &error); snapshot.childCountKnown = error == NULL && snapshot.childCount >= 0; g_clear_error(&error); } if (!snapshot.defunct && !captureDocumentMetadata && g_strcmp0(snapshot.role, "table cell") == 0 && (snapshot.name == NULL || *snapshot.name == '\0')) { guint remaining = 20; gchar *label = cthulhu_table_cell_label(source, 0, &remaining); if (label != NULL) { g_free(snapshot.name); snapshot.name = label; } } snapshot.description = atspi_accessible_get_description(source, &error); if (error != NULL) { g_error_free(error); } if (captureDocumentMetadata) { cthulhu_capture_document_metrics(source, &snapshot); } if (captureSoundGeometry) { cthulhu_capture_sound_geometry(source, &snapshot); } // Copy only semantic attribute fields used by navigation; arbitrary // browser attributes can include URLs and unrelated document data. error = NULL; GHashTable *attributes = NULL; gboolean captureSetPosition = !snapshot.defunct && cthulhu_has_set_position(snapshot.role); if (captureDocumentMetadata || g_strcmp0(snapshot.role, "heading") == 0 || captureSetPosition) { attributes = atspi_accessible_get_attributes(source, &error); } if (error != NULL) { g_clear_error(&error); } if (captureSetPosition) cthulhu_capture_set_position(source, attributes, &snapshot); if (attributes != NULL) { if (captureDocumentMetadata) { snapshot.explicitName = g_strcmp0(g_hash_table_lookup(attributes, "explicit-name"), "true") == 0; const gchar *xmlRoles = g_hash_table_lookup(attributes, "xml-roles"); if (xmlRoles != NULL) { snapshot.xmlRoles = g_strdup(xmlRoles); } const gchar *htmlTag = g_hash_table_lookup(attributes, "tag"); if (htmlTag != NULL) snapshot.htmlTag = g_strdup(htmlTag); const gchar *elementID = g_hash_table_lookup(attributes, "id"); if (elementID != NULL) { snapshot.elementID = g_strdup(elementID); } const gchar *containerLive = g_hash_table_lookup(attributes, "container-live"); if (containerLive != NULL && (g_strcmp0(containerLive, "off") == 0 || g_strcmp0(containerLive, "polite") == 0 || g_strcmp0(containerLive, "assertive") == 0)) { snapshot.liveRegion = TRUE; } const gchar *layoutGuess = g_hash_table_lookup(attributes, "layout-guess"); if (g_strcmp0(layoutGuess, "true") == 0) { snapshot.layoutTable = TRUE; } } if (g_strcmp0(snapshot.role, "heading") == 0) { const gchar *level = g_hash_table_lookup(attributes, "level"); if (level != NULL && *level >= '1' && *level <= '9') { gchar *end = NULL; gint64 number = g_ascii_strtoll(level, &end, 10); if (end != NULL && *end == '\0' && number <= G_MAXINT) { snapshot.headingLevel = (gint)number; } } } if (attributes != NULL) g_hash_table_unref(attributes); } if (captureDocumentMetadata && (g_strcmp0(snapshot.role, "table") == 0 || g_strcmp0(snapshot.role, "tree table") == 0)) { AtspiTable *table = atspi_accessible_get_table_iface(source); if (table != NULL) { error = NULL; snapshot.tableRowCount = atspi_table_get_n_rows(table, &error); if (error != NULL) { g_clear_error(&error); } snapshot.tableColumnCount = atspi_table_get_n_columns(table, NULL); g_object_unref(table); } } if (captureDocumentMetadata && (g_strcmp0(snapshot.role, "table cell") == 0 || g_strcmp0(snapshot.role, "column header") == 0 || g_strcmp0(snapshot.role, "row header") == 0)) { if (!snapshot.name || !*snapshot.name) { guint remaining = 64; gchar *label = cthulhu_document_cell_label(source, 0, &remaining); g_free(snapshot.name); snapshot.name = label; } AtspiTableCell *cell = atspi_accessible_get_table_cell(source); if (cell) { error = NULL; snapshot.tableRowIndex = snapshot.tableColumnIndex = -1; atspi_table_cell_get_row_column_span(cell, &snapshot.tableRowIndex, &snapshot.tableColumnIndex, &snapshot.tableRowSpan, &snapshot.tableColumnSpan, &error); snapshot.tablePositionKnown = !error && snapshot.tableRowIndex >= 0 && snapshot.tableColumnIndex >= 0 && snapshot.tableRowSpan > 0 && snapshot.tableColumnSpan > 0; g_clear_error(&error); snapshot.tableRowHeaders = cthulhu_table_headers(atspi_table_cell_get_row_header_cells(cell, NULL)); snapshot.tableColumnHeaders = cthulhu_table_headers(atspi_table_cell_get_column_header_cells(cell, NULL)); g_object_unref(cell); } } AtspiAction *action = captureDocumentMetadata ? atspi_accessible_get_action_iface(source) : NULL; if (action != NULL) { error = NULL; gint actionCount = atspi_action_get_n_actions(action, &error); if (error != NULL) { g_clear_error(&error); } for (gint index = 0; index < actionCount; index++) { error = NULL; gchar *actionName = atspi_action_get_name(action, index, &error); if (error != NULL) { g_clear_error(&error); } if (g_strcmp0(actionName, "click") == 0) { snapshot.supportsClick = TRUE; } g_free(actionName); } g_object_unref(action); } // Text and caret capture make synchronous AT-SPI calls. Focus events for // ordinary controls do not use that text in presentation, so avoid stalling // the GLib event loop for each Tab stop. Caret events still need line and // character text even when they originate in static web content. if (captureText && g_strcmp0(snapshot.role, "password text") != 0 && (snapshot.editable || g_strcmp0(snapshot.role, "terminal") == 0 || captureCaretCharacter)) { snapshot.text = cthulhu_capture_text(source, &snapshot.textBeforeCaret, &snapshot.textStartOffset); } if (captureCaretCharacter && g_strcmp0(snapshot.role, "password text") != 0) { snapshot.caretCharacter = cthulhu_capture_caret_unit(source, ATSPI_TEXT_GRANULARITY_CHAR); } cthulhu_capture_value(source, &snapshot); return snapshot; } static gchar *cthulhu_capture_text(AtspiAccessible *source, gchar **beforeCaret, gint *startOffset) { *beforeCaret = NULL; AtspiText *text = atspi_accessible_get_text(source); if (text == NULL) { return NULL; } GError *error = NULL; gint caretOffset = atspi_text_get_caret_offset(text, &error); if (error != NULL) { g_error_free(error); g_object_unref(text); return NULL; } AtspiTextRange *range = atspi_text_get_string_at_offset( text, caretOffset, ATSPI_TEXT_GRANULARITY_LINE, &error); // AXText.get_line_at_offset retries Chromium's invalid end-of-text // range. A valid empty range is a real blank line and must stay empty. if (error == NULL && range != NULL && caretOffset > 0) { gboolean retry = range->start_offset > caretOffset; if (range->start_offset == -1 && range->end_offset == -1) { gint count = atspi_text_get_character_count(text, &error); retry = error == NULL && caretOffset == count; } if (retry) { g_boxed_free(atspi_text_range_get_type(), range); range = atspi_text_get_string_at_offset( text, caretOffset - 1, ATSPI_TEXT_GRANULARITY_LINE, &error); } } g_object_unref(text); if (error != NULL) { g_error_free(error); if (range != NULL) g_boxed_free(atspi_text_range_get_type(), range); return NULL; } if (range == NULL) { return NULL; } gchar *content = g_strdup(range->content != NULL ? range->content : ""); *startOffset = range->start_offset; gint relativeCaret = caretOffset - range->start_offset; glong characterCount = g_utf8_strlen(content, -1); if (relativeCaret < 0) { relativeCaret = 0; } else if (relativeCaret > characterCount) { relativeCaret = (gint)characterCount; } *beforeCaret = g_utf8_substring(content, 0, relativeCaret); g_free(range->content); g_free(range); return content; } static gchar *cthulhu_label_text(AtspiAccessible *source) { AtspiText *text = atspi_accessible_get_text(source); if (text != NULL) { GError *error = NULL; gint characterCount = atspi_text_get_character_count(text, &error); if (error != NULL) { g_clear_error(&error); g_object_unref(text); return NULL; } gchar *content = atspi_text_get_text(text, 0, characterCount, &error); g_object_unref(text); if (error != NULL) { g_clear_error(&error); return NULL; } return content != NULL ? content : g_strdup(""); } GError *error = NULL; gchar *name = atspi_accessible_get_name(source, &error); if (error != NULL) g_error_free(error); return name != NULL ? name : g_strdup(""); } static void cthulhu_append_label_texts(AtspiAccessible *source, GString *contents, int *visited) { if (source == NULL || *visited >= 100 || contents->len >= 4096) return; (*visited)++; GError *error = NULL; AtspiRole role = atspi_accessible_get_role(source, &error); if (error != NULL) g_clear_error(&error); if (role == ATSPI_ROLE_LABEL) { gchar *text = cthulhu_label_text(source); if (text != NULL && *text != '\0') { if (contents->len != 0) g_string_append_c(contents, ' '); g_string_append(contents, text); } g_free(text); } error = NULL; gint childCount = atspi_accessible_get_child_count(source, &error); if (error != NULL) { g_clear_error(&error); return; } for (gint index = 0; index < childCount && *visited < 100 && contents->len < 4096; index++) { error = NULL; AtspiAccessible *child = atspi_accessible_get_child_at_index(source, index, &error); if (error != NULL) { g_clear_error(&error); continue; } cthulhu_append_label_texts(child, contents, visited); if (child != NULL) g_object_unref(child); } } gchar *cthulhu_collect_notification_text(AtspiAccessible *source) { if (source == NULL) return g_strdup(""); GString *contents = g_string_new(""); int visited = 0; cthulhu_append_label_texts(source, contents, &visited); return g_string_free(contents, FALSE); } // YAD exposes prompts as labels beside buttons or a choice list. Keep the // capture within that small shape; ordinary frames contain unrelated labels. static gboolean cthulhu_append_prompt_labels(AtspiAccessible *source, GString *contents, gint *visited, gint *buttons, gint *labels, gint *choices) { if (source == NULL || *visited >= 100 || contents->len >= 4096) return FALSE; (*visited)++; GError *error = NULL; AtspiRole role = atspi_accessible_get_role(source, &error); if (error != NULL) { g_clear_error(&error); return FALSE; } if (role == ATSPI_ROLE_BUTTON) { (*buttons)++; return *buttons <= 4; } if (role == ATSPI_ROLE_LABEL || role == ATSPI_ROLE_STATIC) { AtspiStateSet *states = atspi_accessible_get_state_set(source); gboolean showing = states != NULL && atspi_state_set_contains(states, ATSPI_STATE_SHOWING); if (states != NULL) g_object_unref(states); if (!showing) return TRUE; gchar *label = cthulhu_label_text(source); if (label != NULL && *label != '\0') { if (contents->len != 0) g_string_append_c(contents, ' '); g_string_append(contents, label); (*labels)++; } g_free(label); return TRUE; } if (role == ATSPI_ROLE_ICON || role == ATSPI_ROLE_SCROLL_BAR) return TRUE; if (role == ATSPI_ROLE_TREE_TABLE || role == ATSPI_ROLE_TREE || role == ATSPI_ROLE_TABLE || role == ATSPI_ROLE_LIST) { (*choices)++; return *choices <= 1; } if (role != ATSPI_ROLE_FRAME && role != ATSPI_ROLE_DIALOG && role != ATSPI_ROLE_FILLER && role != ATSPI_ROLE_PANEL && role != ATSPI_ROLE_SCROLL_PANE) return FALSE; gint childCount = atspi_accessible_get_child_count(source, &error); if (error != NULL) { g_clear_error(&error); return FALSE; } for (gint index = 0; index < childCount; index++) { AtspiAccessible *child = atspi_accessible_get_child_at_index(source, index, &error); if (error != NULL || child == NULL) { g_clear_error(&error); if (child != NULL) g_object_unref(child); return FALSE; } gboolean valid = cthulhu_append_prompt_labels(child, contents, visited, buttons, labels, choices); g_object_unref(child); if (!valid) return FALSE; } return TRUE; } gchar *cthulhu_collect_simple_prompt_text(AtspiAccessible *source) { if (source == NULL) return NULL; GString *contents = g_string_new(""); gint visited = 0, buttons = 0, labels = 0, choices = 0; gboolean valid = cthulhu_append_prompt_labels(source, contents, &visited, &buttons, &labels, &choices); if (!valid || labels == 0 || (buttons == 0 && choices == 0)) { g_string_free(contents, TRUE); return NULL; } return g_string_free(contents, FALSE); } // Follow the same label/ancestor conventions used by GTK radio groups. A // named panel or filler is the group's question, not the option's own name. gchar *cthulhu_radio_group_name(AtspiAccessible *source) { GArray *relations = atspi_accessible_get_relation_set(source, NULL); if (relations != NULL) { for (guint i = 0; i < relations->len; i++) { AtspiRelation *relation = g_array_index(relations, AtspiRelation *, i); if (atspi_relation_get_relation_type(relation) == ATSPI_RELATION_LABELLED_BY && atspi_relation_get_n_targets(relation) > 0) { AtspiAccessible *label = atspi_relation_get_target(relation, 0); gchar *name = label != NULL ? cthulhu_label_text(label) : NULL; gchar *option = atspi_accessible_get_name(source, NULL); gboolean ownLabel = g_strcmp0(name, option) == 0; g_free(option); if (label != NULL) g_object_unref(label); for (guint j = 0; j < relations->len; j++) g_object_unref(g_array_index(relations, AtspiRelation *, j)); g_array_unref(relations); if (name != NULL && *name != '\0' && !ownLabel) return name; g_free(name); relations = NULL; break; } } if (relations != NULL) { for (guint i = 0; i < relations->len; i++) g_object_unref(g_array_index(relations, AtspiRelation *, i)); g_array_unref(relations); } } AtspiAccessible *current = atspi_accessible_get_parent(source, NULL); for (int depth = 0; current != NULL && depth < 16; depth++) { AtspiRole role = atspi_accessible_get_role(current, NULL); if (role == ATSPI_ROLE_FRAME || role == ATSPI_ROLE_DIALOG || role == ATSPI_ROLE_APPLICATION) { g_object_unref(current); return NULL; } if (role == ATSPI_ROLE_PANEL || role == ATSPI_ROLE_FILLER) { gchar *name = atspi_accessible_get_name(current, NULL); if (name != NULL && *name != '\0') { g_object_unref(current); return name; } g_free(name); } AtspiAccessible *parent = atspi_accessible_get_parent(current, NULL); g_object_unref(current); current = parent; } if (current != NULL) g_object_unref(current); return NULL; } static void cthulhu_append_live_text(AtspiAccessible *source, GString *contents, int *visited) { if (source == NULL || *visited >= 100 || contents->len >= 4096) return; (*visited)++; GError *error = NULL; gint childCount = atspi_accessible_get_child_count(source, &error); if (error != NULL) { g_clear_error(&error); childCount = 0; } guint before = contents->len; for (gint index = 0; index < childCount && *visited < 100 && contents->len < 4096; index++) { AtspiAccessible *child = atspi_accessible_get_child_at_index(source, index, &error); if (error != NULL) g_clear_error(&error); if (child != NULL) { cthulhu_append_live_text(child, contents, visited); g_object_unref(child); } } if (contents->len == before) { gchar *text = cthulhu_label_text(source); if (text != NULL && *text != '\0') { if (contents->len != 0) g_string_append_c(contents, ' '); g_string_append(contents, text); } g_free(text); } } gchar *cthulhu_live_content(AtspiAccessible *source) { GString *contents = g_string_new(""); int visited = 0; cthulhu_append_live_text(source, contents, &visited); return g_string_free(contents, FALSE); } gchar *cthulhu_capture_caret_unit(AtspiAccessible *source, AtspiTextGranularity unit) { AtspiText *text = atspi_accessible_get_text(source); if (text == NULL) { return NULL; } GError *error = NULL; gint caretOffset = atspi_text_get_caret_offset(text, &error); if (error != NULL) { g_error_free(error); g_object_unref(text); return NULL; } if (caretOffset < 0) { g_object_unref(text); return NULL; } AtspiTextRange *range = atspi_text_get_string_at_offset( text, caretOffset, unit, &error); g_object_unref(text); if (error != NULL) { g_error_free(error); if (range != NULL) g_boxed_free(atspi_text_range_get_type(), range); return NULL; } if (range == NULL) { return NULL; } gchar *content = g_strdup(range->content != NULL ? range->content : ""); g_free(range->content); g_free(range); return content; } static void cthulhu_capture_document_metrics(AtspiAccessible *source, cthulhu_snapshot *snapshot) { const gchar *role = snapshot->role; if (g_strcmp0(role, "heading") != 0 && g_strcmp0(role, "link") != 0 && g_strcmp0(role, "paragraph") != 0 && g_strcmp0(role, "list item") != 0 && g_strcmp0(role, "math") != 0 && g_strcmp0(role, "static") != 0 && g_strcmp0(role, "column header") != 0 && g_strcmp0(role, "row header") != 0 && g_strcmp0(role, "table cell") != 0 && g_strcmp0(role, "table row") != 0 && g_strcmp0(role, "text") != 0 && g_strcmp0(role, "section") != 0 && g_strcmp0(role, "article") != 0 && g_strcmp0(role, "description term") != 0 && g_strcmp0(role, "description value") != 0 && g_strcmp0(role, "document email") != 0 && g_strcmp0(role, "document frame") != 0 && g_strcmp0(role, "document presentation") != 0 && g_strcmp0(role, "document spreadsheet") != 0 && g_strcmp0(role, "document text") != 0 && g_strcmp0(role, "document web") != 0 && g_strcmp0(role, "block quote") != 0 && g_strcmp0(role, "description list") != 0 && g_strcmp0(role, "form") != 0 && g_strcmp0(role, "footer") != 0 && g_strcmp0(role, "header") != 0 && g_strcmp0(role, "landmark") != 0 && g_strcmp0(role, "log") != 0 && g_strcmp0(role, "list") != 0 && g_strcmp0(role, "marquee") != 0 && g_strcmp0(role, "panel") != 0 && g_strcmp0(role, "table") != 0 && g_strcmp0(role, "tree") != 0 && g_strcmp0(role, "tree table") != 0) { return; } AtspiText *text = atspi_accessible_get_text(source); if (text == NULL) { return; } GError *error = NULL; gint characterCount = atspi_text_get_character_count(text, &error); if (error != NULL) { g_clear_error(&error); g_object_unref(text); return; } snapshot->characterCount = characterCount; if (characterCount > 75) { error = NULL; gchar *contents = atspi_text_get_text(text, 0, characterCount, &error); if (error != NULL) { g_clear_error(&error); } else if (contents != NULL) { for (const gchar *cursor = contents; *cursor != '\0'; cursor = g_utf8_next_char(cursor)) { if (g_utf8_get_char(cursor) == 0xFFFC) { snapshot->embeddedObjectCount++; } } g_free(contents); } } g_object_unref(text); }