Add opt-in Tentacle protocol 1.1, explicit remote grants, lifecycle IPC and expiring core leases in both SDKs. Revoke access before update preparation, including failed updates. Add transient secret entries, shared Braille cell export and isolated Xorg recovery experiments with focused tests and documented limitations. Production consent, desktop services and live interoperability remain pending.
215 lines
6.5 KiB
Go
215 lines
6.5 KiB
Go
//go:build brlapi && cgo
|
|
|
|
package braille
|
|
|
|
/*
|
|
#cgo LDFLAGS: -lbrlapi
|
|
#include <brlapi.h>
|
|
#include <errno.h>
|
|
#include <stddef.h>
|
|
#include <stdlib.h>
|
|
|
|
typedef struct cth_brl cth_brl;
|
|
struct cth_brl {
|
|
brlapi_handle_t *handle;
|
|
unsigned int columns, rows;
|
|
int opened, entered, failed;
|
|
};
|
|
|
|
// The default BrlAPI handler terminates the process and can dump a packet.
|
|
// Record a failure instead; never print display contents or packet payloads.
|
|
static void cth_exception(brlapi_handle_t *handle, int error,
|
|
brlapi_packetType_t type, const void *packet, size_t size) {
|
|
cth_brl *owner = brlapi__getClientData(handle);
|
|
if (owner) owner->failed = 1;
|
|
}
|
|
|
|
static cth_brl *cth_alloc(void) {
|
|
cth_brl *s = calloc(1, sizeof(*s));
|
|
if (!s) return NULL;
|
|
s->handle = calloc(1, brlapi_getHandleSize());
|
|
if (!s->handle) { free(s); return NULL; }
|
|
return s;
|
|
}
|
|
|
|
static int cth_open(cth_brl *s, int routing) {
|
|
// NULL settings use BrlAPI's normal local connection/authentication.
|
|
if (brlapi__openConnection(s->handle, NULL, NULL) < 0) return -1;
|
|
s->opened = 1;
|
|
brlapi__setClientData(s->handle, s);
|
|
brlapi__setExceptionHandler(s->handle, cth_exception);
|
|
if (brlapi__getDisplaySize(s->handle, &s->columns, &s->rows) < 0 || s->failed) return -1;
|
|
if (!s->columns || !s->rows || s->columns > 4096 || s->rows > 4096/s->columns) return -2;
|
|
// Screen readers take the root of their terminal path. BrlAPI prepends
|
|
// WINDOWPATH or XDG_VTNR, so this follows the active graphical session.
|
|
if (brlapi__enterTtyModeWithPath(s->handle, NULL, 0, NULL) < 0 || s->failed) return -3;
|
|
s->entered = 1;
|
|
if (brlapi__ignoreAllKeys(s->handle) < 0 || s->failed) return -4;
|
|
brlapi_keyCode_t keys[] = {
|
|
BRLAPI_KEY_TYPE_CMD | BRLAPI_KEY_CMD_FWINLT,
|
|
BRLAPI_KEY_TYPE_CMD | BRLAPI_KEY_CMD_FWINRT
|
|
};
|
|
if (brlapi__acceptKeys(s->handle, brlapi_rangeType_key, keys, 2) < 0 || s->failed) return -4;
|
|
if (routing) {
|
|
brlapi_keyCode_t route = BRLAPI_KEY_TYPE_CMD | BRLAPI_KEY_CMD_ROUTE;
|
|
if (brlapi__acceptKeys(s->handle, brlapi_rangeType_command, &route, 1) < 0 || s->failed) return -4;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int cth_write(cth_brl *s, const char *text, int size, int cursor,
|
|
const unsigned char *andMask, const unsigned char *orMask) {
|
|
if (s->failed) return -1;
|
|
brlapi_writeArguments_t args = BRLAPI_WRITEARGUMENTS_INITIALIZER;
|
|
args.regionBegin = 1;
|
|
args.regionSize = s->columns * s->rows;
|
|
args.text = text;
|
|
args.textSize = size;
|
|
args.charset = "UTF-8";
|
|
args.cursor = cursor;
|
|
args.andMask = andMask;
|
|
args.orMask = orMask;
|
|
int result = brlapi__write(s->handle, &args);
|
|
return s->failed ? -1 : result;
|
|
}
|
|
|
|
static int cth_read(cth_brl *s, char *error, size_t errorSize) {
|
|
if (s->failed) return -1;
|
|
brlapi_keyCode_t code;
|
|
int result = brlapi__readKey(s->handle, 0, &code);
|
|
if (s->failed) return -1;
|
|
if (result < 0 && brlapi_errno == BRLAPI_ERROR_LIBCERR && brlapi_libcerrno == EINTR) return 0;
|
|
if (result < 0) brlapi_strerror_r(&brlapi_error, error, errorSize);
|
|
if (result <= 0) return result;
|
|
if ((code & BRLAPI_KEY_TYPE_MASK) != BRLAPI_KEY_TYPE_CMD) return 0;
|
|
if ((code & BRLAPI_KEY_CMD_BLK_MASK) == BRLAPI_KEY_CMD_ROUTE)
|
|
return 0x10000 + (code & BRLAPI_KEY_CMD_ARG_MASK);
|
|
switch (code & BRLAPI_KEY_CODE_MASK) {
|
|
case BRLAPI_KEY_CMD_FWINLT: return 1;
|
|
case BRLAPI_KEY_CMD_FWINRT: return 2;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
static int cth_close(cth_brl *s) {
|
|
int result = 0;
|
|
if (s->entered && !s->failed) result = brlapi__leaveTtyMode(s->handle);
|
|
if (s->failed) result = -1;
|
|
if (s->opened) brlapi__closeConnection(s->handle);
|
|
free(s->handle);
|
|
free(s);
|
|
return result;
|
|
}
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"errors"
|
|
"sync"
|
|
"unicode/utf8"
|
|
"unsafe"
|
|
)
|
|
|
|
type native struct {
|
|
mu sync.Mutex
|
|
session *C.cth_brl
|
|
cells int
|
|
routing bool
|
|
}
|
|
|
|
func New() Backend { return &native{} }
|
|
|
|
// NewRouting claims routing keys as well as pan commands for the live helper.
|
|
func NewRouting() Backend { return &native{routing: true} }
|
|
|
|
func (n *native) Open() (Geometry, error) {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
if n.session != nil {
|
|
return Geometry{}, errors.New("BrlAPI connection already open")
|
|
}
|
|
s := C.cth_alloc()
|
|
if s == nil {
|
|
return Geometry{}, errors.New("allocate BrlAPI connection")
|
|
}
|
|
routing := C.int(0)
|
|
if n.routing {
|
|
routing = 1
|
|
}
|
|
if code := C.cth_open(s, routing); code != 0 {
|
|
C.cth_close(s)
|
|
switch code {
|
|
case -2:
|
|
return Geometry{}, errors.New("unsupported braille display geometry")
|
|
case -3:
|
|
return Geometry{}, errors.New("acquire BrlAPI terminal path; check WINDOWPATH, XDG_VTNR, and terminal ownership")
|
|
case -4:
|
|
return Geometry{}, errors.New("configure BrlAPI commands")
|
|
default:
|
|
return Geometry{}, errors.New("BrlAPI connection or display query failed; check BRLTTY and authorization")
|
|
}
|
|
}
|
|
n.session = s
|
|
g := Geometry{Columns: int(s.columns), Rows: int(s.rows)}
|
|
n.cells, _ = g.Cells()
|
|
return g, nil
|
|
}
|
|
|
|
func (n *native) Write(text string, cursor int) error {
|
|
return n.WriteWithDots(text, cursor, nil)
|
|
}
|
|
|
|
func (n *native) WriteWithDots(text string, cursor int, dots []byte) error {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
if n.session == nil {
|
|
return errors.New("BrlAPI connection is closed")
|
|
}
|
|
if !utf8.ValidString(text) || utf8.RuneCountInString(text) != n.cells || cursor < 0 || cursor > n.cells {
|
|
return errors.New("invalid braille frame")
|
|
}
|
|
andMask, orMask, err := displayMasks(text, dots)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value := C.CString(text)
|
|
defer C.free(unsafe.Pointer(value))
|
|
if C.cth_write(n.session, value, C.int(len(text)), C.int(cursor),
|
|
(*C.uchar)(unsafe.Pointer(&andMask[0])), (*C.uchar)(unsafe.Pointer(&orMask[0]))) < 0 {
|
|
return errors.New("BrlAPI display write failed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (n *native) ReadKey() (Key, error) {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
if n.session == nil {
|
|
return NoKey, errors.New("BrlAPI connection is closed")
|
|
}
|
|
var detail [256]C.char
|
|
key := C.cth_read(n.session, &detail[0], C.size_t(len(detail)))
|
|
if key < 0 {
|
|
if detail[0] != 0 {
|
|
return NoKey, errors.New("BrlAPI key read failed: " + C.GoString(&detail[0]))
|
|
}
|
|
return NoKey, errors.New("BrlAPI key read failed after protocol exception")
|
|
}
|
|
return Key(key), nil
|
|
}
|
|
|
|
func (n *native) Close() error {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
if n.session == nil {
|
|
return nil
|
|
}
|
|
result := C.cth_close(n.session)
|
|
n.session = nil
|
|
n.cells = 0
|
|
if result < 0 {
|
|
return errors.New("release BrlAPI terminal failed")
|
|
}
|
|
return nil
|
|
}
|