Files
cthulhu/internal/atspi/document_lines_native.go

119 lines
3.6 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package atspi
/*
#include <atspi/atspi.h>
*/
import "C"
import (
"context"
"strings"
"time"
"unicode/utf8"
)
func (adapter *Adapter) readNativeDocumentLine(ctx context.Context, id ObjectID, snapshot Snapshot, text *C.AtspiText, offset, count int) (DocumentCaretText, error) {
source := nativeReviewText{text: text}
line, err := source.line(ctx, offset)
if err != nil {
return DocumentCaretText{}, err
}
result := DocumentCaretText{Start: line.Start, End: line.End, Text: line.Text, CharacterCount: count}
if snapshot.Editable || snapshot.Role == "password text" || snapshot.HTMLTag == "pre" || snapshot.HTMLTag == "code" || count > maxReviewLineCharacters ||
len(strings.Fields(line.Text)) > 1 || line.Start == 0 && line.End == count {
return result, nil
}
if cached, ok := adapter.documentLineCache[id]; ok && cached.CharacterCount == count {
if cached.End > 0 {
return cached, nil
}
return result, nil
}
adapter.treeMutex.Lock()
tree := adapter.documentTree
adapter.treeMutex.Unlock()
if tree != nil {
if ref, err := tree.Reference(id); err == nil {
if owner, err := tree.DocumentContext(ref); err == nil {
for _, ancestor := range owner.Ancestors {
if ancestor.HTMLTag == "pre" || ancestor.HTMLTag == "code" || strings.Contains(" "+ancestor.XMLRoles+" ", " code ") {
return result, nil
}
}
}
}
}
var nativeErr *C.GError
value := C.atspi_text_get_text(text, 0, C.gint(count), &nativeErr)
if value != nil {
defer C.g_free(C.gpointer(value))
}
if nativeErr != nil {
C.g_error_free(nativeErr)
return result, nil
}
content := strings.ToValidUTF8(C.GoString(value), "�")
if utf8.RuneCountInString(content) != count {
return result, nil
}
probe, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
repair, probeErr := documentLinesAreTokens(probe, source, content)
cancel()
if probeErr != nil {
return result, ctx.Err()
}
if adapter.documentLineCache == nil || len(adapter.documentLineCache) >= 64 {
adapter.documentLineCache = make(map[ObjectID]DocumentCaretText)
}
cached := DocumentCaretText{CharacterCount: count}
if repair {
cached.End, cached.Text = count, content
result = cached
}
adapter.documentLineCache[id] = cached
return result, nil
}
// Repair only owned web text, on the GLib owner. Native selections use the
// same logical line as virtual browsing, including braille cursor placement.
func (adapter *Adapter) repairNativeCaretLine(source *C.AtspiAccessible, snapshot *Snapshot, offset int) {
if !snapshot.TextCaretKnown || snapshot.Editable || snapshot.Role == "password text" || offset < 0 {
return
}
adapter.treeMutex.Lock()
tree := adapter.documentTree
adapter.treeMutex.Unlock()
if tree == nil {
return
}
ref, err := tree.Reference(snapshot.ID())
if err != nil {
return
}
if _, err = tree.DocumentContext(ref); err != nil {
return
}
text := C.atspi_accessible_get_text_iface(source)
if text == nil {
return
}
defer C.g_object_unref(C.gpointer(text))
count := int(C.atspi_text_get_character_count(text, nil))
if count <= 0 || offset > count {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
line, err := adapter.readNativeDocumentLine(ctx, snapshot.ID(), *snapshot, text, min(offset, count-1), count)
if err != nil || line.Start < 0 || line.End <= line.Start {
return
}
snapshot.Text = sanitizeSnapshotField(line.Text)
snapshot.BrailleLine, snapshot.BrailleLineStart, snapshot.BrailleCaret = line.Text, line.Start, offset
snapshot.CharacterCount = count
}