// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package atspi /* #include "adapter_native.h" static char *tentacle_attribute(GHashTable *attributes, const char *key) { return attributes ? g_hash_table_lookup(attributes, key) : NULL; } */ import "C" import ( "context" "errors" "strings" "unsafe" ) type TransferItem struct{ Text, Path string } // ReadTransferItem reads one current line or one explicit path from the // foreground object. It never walks descendants or reads password contents. func (adapter *Adapter) ReadTransferItem(ctx context.Context, object ObjectID, offset int) (TransferItem, error) { result, err := adapter.documentOperation(ctx, treeCaptureRequest{transferItem: true, reviewFocus: object, positionOffset: offset}) return result.item, err } func (adapter *Adapter) nativeTransferItem(ctx context.Context, object ObjectID, offset int) (TransferItem, error) { source, _, err := adapter.native_action_source(object) if err != nil || ctx.Err() != nil { return TransferItem{}, ErrStaleObject } if C.atspi_accessible_get_role(source, nil) == C.ATSPI_ROLE_PASSWORD_TEXT { return TransferItem{}, errors.New("protected item") } attributes := C.atspi_accessible_get_attributes(source, nil) if attributes != nil { defer C.g_hash_table_unref(attributes) for _, key := range []string{"file-uri", "uri", "file-path"} { rawKey := C.CString(key) raw := C.tentacle_attribute(attributes, rawKey) C.free(unsafe.Pointer(rawKey)) if raw != nil { path := C.GoString(raw) if strings.HasPrefix(path, "file:") || strings.HasPrefix(path, "/") { return TransferItem{Path: path}, nil } } } } snapshot := snapshotFromNativeWithCaret(source, false, false, false, false) // File icons and rows without explicit paths must not become text messages. if snapshot.Role == "icon" || snapshot.Role == "list item" || snapshot.Role == "table cell" && !snapshot.Spreadsheet { return TransferItem{}, errors.New("file item does not expose a reliable path") } value := snapshot.Name text := C.atspi_accessible_get_text_iface(source) if text != nil { defer C.g_object_unref(C.gpointer(text)) if offset < 0 { offset = int(C.atspi_text_get_caret_offset(text, nil)) if offset < 0 { offset = 0 } } line := C.atspi_text_get_string_at_offset(text, C.gint(offset), C.ATSPI_TEXT_GRANULARITY_LINE, nil) if line == nil { return TransferItem{}, errors.New("focused line unavailable") } value = C.GoString(line.content) C.g_free(C.gpointer(line.content)) C.g_free(C.gpointer(line)) // Native line delimiters are not part of the message; retain actual spacing. value = strings.TrimRight(value, "\r\n") } if len(value) >= 4096 { return TransferItem{}, errors.New("focused item exceeds transfer limit") } if value == "" { return TransferItem{}, errors.New("focused item has no text or path") } return TransferItem{Text: strings.NewReplacer("\r\n", " ", "\r", " ", "\n", " ").Replace(value)}, nil }