Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package atspi
|
||||
|
||||
/*
|
||||
#cgo pkg-config: atspi-2 glib-2.0 gobject-2.0
|
||||
#include <atspi/atspi.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
func (adapter *Adapter) ReadFocusedText(ctx context.Context, focus ObjectID) (DocumentContent, error) {
|
||||
result, err := adapter.documentOperation(ctx, treeCaptureRequest{reviewFocus: focus, readFocusedText: true})
|
||||
return result.content, err
|
||||
}
|
||||
|
||||
func (adapter *Adapter) SetFocusedTextCaret(ctx context.Context, focus ObjectID, offset int) error {
|
||||
_, err := adapter.documentOperation(ctx, treeCaptureRequest{reviewFocus: focus, setFocusedCaret: true, positionOffset: offset})
|
||||
return err
|
||||
}
|
||||
|
||||
func (adapter *Adapter) setNativeFocusedTextCaret(ctx context.Context, focus ObjectID, offset int) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !adapter.nativeFocusIs(focus) || offset < 0 {
|
||||
return ErrStaleObject
|
||||
}
|
||||
source := adapter.nativeFocus[len(adapter.nativeFocus)-1].source
|
||||
if C.atspi_accessible_get_role(source, nil) == C.ATSPI_ROLE_PASSWORD_TEXT {
|
||||
return ErrNoDocumentCaret
|
||||
}
|
||||
text := C.atspi_accessible_get_text_iface(source)
|
||||
if text == nil {
|
||||
return ErrNoDocumentCaret
|
||||
}
|
||||
defer C.g_object_unref(C.gpointer(text))
|
||||
adapter.navigationEcho = map[ObjectID]uint8{focus: 7}
|
||||
var nativeErr *C.GError
|
||||
placed := C.atspi_text_set_caret_offset(text, C.gint(offset), &nativeErr)
|
||||
if nativeErr != nil {
|
||||
err := nativeError("place read all caret", nativeErr)
|
||||
C.g_error_free(nativeErr)
|
||||
return err
|
||||
}
|
||||
if placed == 0 {
|
||||
delete(adapter.navigationEcho, focus)
|
||||
return ErrNoDocumentCaret
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (adapter *Adapter) readNativeFocusedText(ctx context.Context, focus ObjectID) (DocumentContent, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return DocumentContent{}, err
|
||||
}
|
||||
if !adapter.nativeFocusIs(focus) {
|
||||
return DocumentContent{}, ErrStaleObject
|
||||
}
|
||||
source := adapter.nativeFocus[len(adapter.nativeFocus)-1].source
|
||||
snapshot := snapshotFromNativeWithCaret(source, false, false, false, false)
|
||||
content := DocumentContent{Snapshot: snapshot}
|
||||
if snapshot.Role == "password text" {
|
||||
return content, nil
|
||||
}
|
||||
text := C.atspi_accessible_get_text_iface(source)
|
||||
if text == nil {
|
||||
return content, nil
|
||||
}
|
||||
defer C.g_object_unref(C.gpointer(text))
|
||||
var nativeErr *C.GError
|
||||
count := int(C.atspi_text_get_character_count(text, &nativeErr))
|
||||
if nativeErr != nil {
|
||||
err := nativeError("read all character count", nativeErr)
|
||||
C.g_error_free(nativeErr)
|
||||
return DocumentContent{}, err
|
||||
}
|
||||
offset := int(C.atspi_text_get_caret_offset(text, &nativeErr))
|
||||
if nativeErr != nil {
|
||||
err := nativeError("read all caret offset", nativeErr)
|
||||
C.g_error_free(nativeErr)
|
||||
return DocumentContent{}, err
|
||||
}
|
||||
offset = max(0, offset)
|
||||
if snapshot.Role == "terminal" && count > 0 {
|
||||
terminal := nativeReviewText{text: text, terminal: true}
|
||||
terminalCount, err := terminal.count(ctx)
|
||||
if err != nil {
|
||||
return DocumentContent{}, err
|
||||
}
|
||||
count = terminalCount
|
||||
if count > 0 {
|
||||
// A terminal caret commonly sits after the prompt's last character.
|
||||
// Python Say All reads its containing line, including that prompt,
|
||||
// before advancing. Keep editable controls' exact caret behavior.
|
||||
line, err := terminal.line(ctx, min(offset, count-1))
|
||||
if err != nil {
|
||||
return DocumentContent{}, err
|
||||
}
|
||||
if line.Start < 0 || line.Start > offset || line.End <= line.Start || line.End > count {
|
||||
return DocumentContent{}, errors.New("read all: invalid terminal line offsets")
|
||||
}
|
||||
offset = line.Start
|
||||
}
|
||||
}
|
||||
if count-offset > 1<<20 {
|
||||
return DocumentContent{}, errors.New("read all: text exceeds speech capture limit")
|
||||
}
|
||||
if offset >= count {
|
||||
content.Snapshot.Name, content.Snapshot.Text = "", ""
|
||||
return content, nil
|
||||
}
|
||||
value := C.atspi_text_get_text(text, C.gint(offset), C.gint(count), &nativeErr)
|
||||
if value != nil {
|
||||
defer C.g_free(C.gpointer(value))
|
||||
}
|
||||
if nativeErr != nil {
|
||||
err := nativeError("read all text", nativeErr)
|
||||
C.g_error_free(nativeErr)
|
||||
return DocumentContent{}, err
|
||||
}
|
||||
content.Start, content.End, content.Text = offset, count, C.GoString(value)
|
||||
return content, ctx.Err()
|
||||
}
|
||||
Reference in new issue
Block a user