226 lines
6.8 KiB
Go
226 lines
6.8 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")
|
|
}
|
|
if len(dots) != 0 && len(dots) != n.cells {
|
|
return errors.New("invalid braille indicator length")
|
|
}
|
|
value := C.CString(text)
|
|
defer C.free(unsafe.Pointer(value))
|
|
andMask := make([]byte, n.cells)
|
|
orMask := make([]byte, n.cells)
|
|
for index, cell := range []rune(text) {
|
|
andMask[index] = 0xff
|
|
if len(dots) != 0 {
|
|
orMask[index] = dots[index]
|
|
}
|
|
if cell >= 0x2800 && cell <= 0x28ff {
|
|
andMask[index] = 0
|
|
orMask[index] |= byte(cell - 0x2800)
|
|
}
|
|
}
|
|
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
|
|
}
|