//go:build brlapi && cgo package braille /* #cgo LDFLAGS: -lbrlapi #include #include #include #include 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 }