// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package atspi /* #include "adapter_native.h" static gboolean pidgin_typing_attributes(AtspiText *text, gint offset, gchar **scale, gchar **weight) { gint start = 0, end = 0; GError *error = NULL; GHashTable *attributes = atspi_text_get_attribute_run(text, offset, FALSE, &start, &end, &error); gboolean known = attributes != NULL && error == NULL; if (known) { *scale = g_strdup(g_hash_table_lookup(attributes, "scale")); *weight = g_strdup(g_hash_table_lookup(attributes, "weight")); } if (attributes != NULL) g_hash_table_unref(attributes); if (error != NULL) g_clear_error(&error); return known; } */ import "C" import ( "strconv" "time" "git.stormux.org/storm/cthulhu/internal/messaging" ) // pidginMessage follows legacy chat.py's transcript contract, never a window // title or application-supplied name. Only verified Pidgin transcript insertions // bypass background filtering. Entry typing and buddy-list changes do not. func (adapter *Adapter) pidginMessage(event *C.AtspiEvent) *messaging.Message { if canonicalObjectChangeEventType(C.GoString(event._type)) != TextInsertedEvent || adapter.applicationProcess(event.source) != "pidgin" || C.atspi_accessible_get_role(event.source, nil) != C.ATSPI_ROLE_TEXT { return nil } states := C.atspi_accessible_get_state_set(event.source) if states == nil { return nil } transcript := C.atspi_state_set_contains(states, C.ATSPI_STATE_MULTI_LINE) != 0 && C.atspi_state_set_contains(states, C.ATSPI_STATE_EDITABLE) == 0 && C.atspi_state_set_contains(states, C.ATSPI_STATE_DEFUNCT) == 0 C.g_object_unref(C.gpointer(states)) if !transcript { return nil } current := C.atspi_accessible_get_parent(event.source, nil) if current == nil { return nil } if C.atspi_accessible_get_role(current, nil) != C.ATSPI_ROLE_SCROLL_PANE { C.g_object_unref(C.gpointer(current)) return nil } id := nativeObjectID(event.source) message := &messaging.Message{Provider: id.BusName, Conversation: id.Path} deadline := time.Now().Add(50 * time.Millisecond) for depth := 0; current != nil && depth < 32 && time.Now().Before(deadline); depth++ { role := C.atspi_accessible_get_role(current, nil) if role == C.ATSPI_ROLE_PAGE_TAB && message.RoomName == "" || role == C.ATSPI_ROLE_FRAME { if message.RoomName == "" { name := C.atspi_accessible_get_name(current, nil) message.RoomName = sanitizeSnapshotField(C.GoString(name)) C.g_free(C.gpointer(name)) } if role == C.ATSPI_ROLE_FRAME { window := nativeObjectID(current) if window.BusName == id.BusName { message.Window = window.Path } C.g_object_unref(C.gpointer(current)) current = nil break } } parent := C.atspi_accessible_get_parent(current, nil) C.g_object_unref(C.gpointer(current)) current = parent } if current != nil { C.g_object_unref(C.gpointer(current)) } if !id.Valid() || message.Window == "" { return nil } payload := C.cthulhu_event_payload(event) message.Text = sanitizeTextContext(C.GoString(payload)) C.g_free(C.gpointer(payload)) // Pidgin inserts typing status into the transcript with smaller/lighter // attributes. Legacy defaults suppress it and exclude it from history. if text := C.atspi_accessible_get_text(event.source); text != nil { // Scale is not part of the reader's ordinary spoken-attribute set. // Query both Pidgin-specific markers without expanding generic capture. var scaleValue, weightValue *C.gchar known := C.pidgin_typing_attributes(text, event.detail1, &scaleValue, &weightValue) != 0 scale, scaleErr := strconv.ParseFloat(C.GoString(scaleValue), 64) weight, weightErr := strconv.Atoi(C.GoString(weightValue)) C.g_free(C.gpointer(scaleValue)) C.g_free(C.gpointer(weightValue)) message.Typing = scaleErr == nil && scale < 1 || weightErr == nil && weight < 400 C.g_object_unref(C.gpointer(text)) if !known { return nil } // A destroyed range cannot establish a message. } else { return nil } return message }