aboutsummaryrefslogtreecommitdiffstats
path: root/surfaces/access.c
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-28 13:25:21 +0100
committereunhhu <[email protected]>2026-09-28 13:25:21 +0100
commit6052b048467f392f4eb9fb5b268cae15e94691d5 (patch)
tree18c832ff701deb3bfdab4c85dc33a7dc7c81b833 /surfaces/access.c
parent0e9a48699dac240665ecea96b9a244dbde5dcac8 (diff)
downloadvega-6052b048467f392f4eb9fb5b268cae15e94691d5.tar.gz
vega-6052b048467f392f4eb9fb5b268cae15e94691d5.zip
Speak AT-SPI2 on the accessibility bus from the display, over Vega's own D-Bus
vega display --access joins the accessibility bus as AT-SPI2 objects for screen readers such as Orca, with no toolkit, ATK or libdbus: dbus.c speaks the D-Bus wire format (EXTERNAL authentication, headers, aligned bodies, calls, replies, signals). The tree follows what the say surface says: a heading, a label per field, a list box per list whose items know their place and which is selected and focused, tables, the entry, the keys as buttons; answers come from the app when asked, one object has the focus, and focus moves are said as events. Assistive technology can act too: Selection focuses a row, Action presses a key. The live loop now draws only when something changed, so a screen reader's questions no longer cost frames. The check walks the tree with gdbus on a private session bus with at-spi-bus-launcher, and follows the focus through keys, Selection, Action and the events.
Diffstat (limited to 'surfaces/access.c')
-rw-r--r--surfaces/access.c806
1 files changed, 806 insertions, 0 deletions
diff --git a/surfaces/access.c b/surfaces/access.c
new file mode 100644
index 0000000..9b5452b
--- /dev/null
+++ b/surfaces/access.c
@@ -0,0 +1,806 @@
+/* Accessibility on Linux desktops: the screen as AT-SPI2 objects on the
+ * accessibility bus (surfaces/vega.h), for screen readers such as Orca,
+ * spoken over Vega's own D-Bus (dbus.c), no toolkit or library.
+ *
+ * The objects follow the screen as the say surface does:
+ * application (the root) > frame (the title) > a heading, a label per
+ * field ("name: value"), a list box per list (a list item per row, the
+ * focused one selected, and focused while the list is the one keys move),
+ * a table per table (rows of cells), the text entry, and a tool bar of
+ * the keys as push buttons.
+ * Each answer is worked out from the app when it is asked for, so nothing
+ * is kept that could go stale. Assistive technology can also act: a key's
+ * button presses it (Action), a row selected is focused (Selection). When
+ * the app changes, the focus that moved is said as events. */
+#define _POSIX_C_SOURCE 200809L
+#include "dbus.h"
+#include "vega.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define ROOT "/org/a11y/atspi/accessible/root"
+#define NULL_PATH "/org/a11y/atspi/null"
+#define ACCESSIBLE "org.a11y.atspi.Accessible"
+
+/* AT-SPI's roles (atspi-constants.h) and states (bit positions). */
+enum { ROLE_FRAME = 23, ROLE_LABEL = 29, ROLE_LIST_ITEM = 32, ROLE_PUSH_BUTTON = 43, ROLE_TABLE = 55,
+ ROLE_TABLE_CELL = 56, ROLE_TOOL_BAR = 63, ROLE_APPLICATION = 75, ROLE_ENTRY = 79, ROLE_HEADING = 83,
+ ROLE_TABLE_ROW = 90, ROLE_LIST_BOX = 98 };
+enum { STATE_ACTIVE = 1, STATE_EDITABLE = 7, STATE_ENABLED = 8, STATE_FOCUSABLE = 11, STATE_FOCUSED = 12,
+ STATE_SELECTABLE = 22, STATE_SELECTED = 23, STATE_SENSITIVE = 24, STATE_SHOWING = 25, STATE_SINGLE_LINE = 26,
+ STATE_VISIBLE = 30 };
+
+struct VegaAccess {
+ VegaBus bus;
+ PureApp *app;
+ const VegaScreen *screen;
+ const VegaAccessHost *host;
+ char parent_name[128], parent_path[128]; /* the registry's desktop, which embeds us */
+ int32_t id; /* the registry's number for us */
+ char focused[128]; /* the object last said to have the focus */
+};
+
+/* ---- What an object is: its path taken apart */
+
+typedef enum { ROOT_OBJECT, FRAME, TITLE, FIELD, PART, ROW, CELL, ENTRY, KEYS, KEY, NONE } Kind;
+typedef struct {
+ Kind kind;
+ unsigned part, column, key; /* part: an index in screen->parts; key: an input */
+ size_t row;
+} Object;
+
+static Object parse_path(const VegaAccess *a, const char *path) {
+ Object o = {NONE, 0, 0, 0, 0};
+ unsigned part, column, key;
+ size_t row;
+ int used = 0;
+ if (!strcmp(path, ROOT)) o.kind = ROOT_OBJECT;
+ else if (!strcmp(path, "/org/vega/frame")) o.kind = FRAME;
+ else if (!strcmp(path, "/org/vega/title")) o.kind = TITLE;
+ else if (!strcmp(path, "/org/vega/entry")) o.kind = ENTRY;
+ else if (!strcmp(path, "/org/vega/keys")) o.kind = KEYS;
+ else if (sscanf(path, "/org/vega/key/%u%n", &key, &used) == 1 && !path[used]) o = (Object){KEY, 0, 0, key, 0};
+ else if (sscanf(path, "/org/vega/field/%u%n", &part, &used) == 1 && !path[used]) o = (Object){FIELD, part, 0, 0, 0};
+ else if (sscanf(path, "/org/vega/part/%u/%zu/%u%n", &part, &row, &column, &used) == 3 && !path[used])
+ o = (Object){CELL, part, column, 0, row};
+ else if (sscanf(path, "/org/vega/part/%u/%zu%n", &part, &row, &used) == 2 && !path[used]) o = (Object){ROW, part, 0, 0, row};
+ else if (sscanf(path, "/org/vega/part/%u%n", &part, &used) == 1 && !path[used]) o = (Object){PART, part, 0, 0, 0};
+ /* what no longer exists is nothing */
+ const VegaScreen *s = a->screen;
+ if ((o.kind == FIELD || o.kind == PART || o.kind == ROW || o.kind == CELL) &&
+ (o.part >= s->count || (o.kind == FIELD) != (s->parts[o.part].kind == VEGA_FIELD)))
+ o.kind = NONE;
+ if ((o.kind == ROW || o.kind == CELL) && o.row >= vega_rows(a->app, &s->parts[o.part])) o.kind = NONE;
+ if (o.kind == CELL && (s->parts[o.part].kind != VEGA_TABLE || o.column >= s->parts[o.part].column_count)) o.kind = NONE;
+ if (o.kind == ROW && s->parts[o.part].kind == VEGA_FIELD) o.kind = NONE;
+ if (o.kind == TITLE && s->title < 0) o.kind = NONE;
+ if (o.kind == ENTRY && s->prompt < 0) o.kind = NONE;
+ if (o.kind == KEY && (o.key >= a->app->module->inputs || a->app->module->input[o.key].kind != PURE_KIND_KEY)) o.kind = NONE;
+ return o;
+}
+
+static void path_of(const Object *o, char *out, size_t size) {
+ switch (o->kind) {
+ case ROOT_OBJECT: snprintf(out, size, ROOT); break;
+ case FRAME: snprintf(out, size, "/org/vega/frame"); break;
+ case TITLE: snprintf(out, size, "/org/vega/title"); break;
+ case FIELD: snprintf(out, size, "/org/vega/field/%u", o->part); break;
+ case PART: snprintf(out, size, "/org/vega/part/%u", o->part); break;
+ case ROW: snprintf(out, size, "/org/vega/part/%u/%zu", o->part, o->row); break;
+ case CELL: snprintf(out, size, "/org/vega/part/%u/%zu/%u", o->part, o->row, o->column); break;
+ case ENTRY: snprintf(out, size, "/org/vega/entry"); break;
+ case KEYS: snprintf(out, size, "/org/vega/keys"); break;
+ case KEY: snprintf(out, size, "/org/vega/key/%u", o->key); break;
+ default: snprintf(out, size, NULL_PATH); break;
+ }
+}
+
+/* The keys the screen answers: key inputs a handler takes. */
+static bool bound_key(const PureModule *m, uint32_t input) {
+ if (m->input[input].kind != PURE_KIND_KEY) return false;
+ for (uint32_t h = 0; h < m->handlers; ++h)
+ if (m->handler[h].event == 1 + 2 * input) return true;
+ return false;
+}
+
+/* The list or table keys move: the first with rows. */
+static int active_part(const VegaAccess *a) {
+ for (unsigned i = 0; i < a->screen->count; ++i)
+ if (a->screen->parts[i].kind != VEGA_FIELD && vega_rows(a->app, &a->screen->parts[i])) return (int)i;
+ return -1;
+}
+
+/* An object's children, in order, into out (up to max); how many there are. */
+static size_t children(const VegaAccess *a, const Object *o, Object *out, size_t max) {
+ const VegaScreen *s = a->screen;
+ const PureModule *m = a->app->module;
+ size_t n = 0;
+#define CHILD(...) \
+ do { \
+ if (n < max) out[n] = (Object){__VA_ARGS__}; \
+ ++n; \
+ } while (0)
+ switch (o->kind) {
+ case ROOT_OBJECT: CHILD(FRAME, 0, 0, 0, 0); break;
+ case FRAME:
+ if (s->title >= 0) CHILD(TITLE, 0, 0, 0, 0);
+ for (unsigned i = 0; i < s->count; ++i) CHILD(s->parts[i].kind == VEGA_FIELD ? FIELD : PART, i, 0, 0, 0);
+ if (s->prompt >= 0) CHILD(ENTRY, 0, 0, 0, 0);
+ for (uint32_t i = 0; i < m->inputs; ++i)
+ if (bound_key(m, i)) {
+ CHILD(KEYS, 0, 0, 0, 0);
+ break;
+ }
+ break;
+ case PART:
+ for (size_t row = 0, rows = vega_rows(a->app, &s->parts[o->part]); row < rows; ++row) CHILD(ROW, o->part, 0, 0, row);
+ break;
+ case ROW:
+ if (s->parts[o->part].kind == VEGA_TABLE)
+ for (unsigned c = 0; c < s->parts[o->part].column_count; ++c) CHILD(CELL, o->part, c, 0, o->row);
+ break;
+ case KEYS:
+ for (uint32_t i = 0; i < m->inputs; ++i)
+ if (bound_key(m, i)) CHILD(KEY, 0, 0, i, 0);
+ break;
+ default: break;
+ }
+#undef CHILD
+ return n;
+}
+
+static Object parent_of(const Object *o) {
+ switch (o->kind) {
+ case FRAME: return (Object){ROOT_OBJECT, 0, 0, 0, 0};
+ case TITLE: case FIELD: case PART: case ENTRY: case KEYS: return (Object){FRAME, 0, 0, 0, 0};
+ case ROW: return (Object){PART, o->part, 0, 0, 0};
+ case CELL: return (Object){ROW, o->part, 0, 0, o->row};
+ case KEY: return (Object){KEYS, 0, 0, 0, 0};
+ default: return (Object){NONE, 0, 0, 0, 0};
+ }
+}
+
+static size_t index_in_parent(const VegaAccess *a, const Object *o) {
+ Object parent = parent_of(o), kids[VEGA_PARTS + PURE_MODULE_INPUTS + 4];
+ if (o->kind == ROW) return o->row;
+ if (o->kind == CELL) return o->column;
+ size_t n = children(a, &parent, kids, sizeof(kids) / sizeof(*kids));
+ for (size_t i = 0; i < n && i < sizeof(kids) / sizeof(*kids); ++i)
+ if (kids[i].kind == o->kind && kids[i].part == o->part && kids[i].key == o->key) return i;
+ return 0;
+}
+
+static int role_of(const VegaAccess *a, const Object *o) {
+ switch (o->kind) {
+ case ROOT_OBJECT: return ROLE_APPLICATION;
+ case FRAME: return ROLE_FRAME;
+ case TITLE: return ROLE_HEADING;
+ case FIELD: return ROLE_LABEL;
+ case PART: return a->screen->parts[o->part].kind == VEGA_LIST ? ROLE_LIST_BOX : ROLE_TABLE;
+ case ROW: return a->screen->parts[o->part].kind == VEGA_LIST ? ROLE_LIST_ITEM : ROLE_TABLE_ROW;
+ case CELL: return ROLE_TABLE_CELL;
+ case ENTRY: return ROLE_ENTRY;
+ case KEYS: return ROLE_TOOL_BAR;
+ case KEY: return ROLE_PUSH_BUTTON;
+ default: return 0;
+ }
+}
+
+static const char *role_name(int role) {
+ switch (role) {
+ case ROLE_APPLICATION: return "application";
+ case ROLE_FRAME: return "frame";
+ case ROLE_HEADING: return "heading";
+ case ROLE_LABEL: return "label";
+ case ROLE_LIST_BOX: return "list box";
+ case ROLE_LIST_ITEM: return "list item";
+ case ROLE_TABLE: return "table";
+ case ROLE_TABLE_ROW: return "table row";
+ case ROLE_TABLE_CELL: return "table cell";
+ case ROLE_ENTRY: return "entry";
+ case ROLE_TOOL_BAR: return "tool bar";
+ case ROLE_PUSH_BUTTON: return "push button";
+ default: return "unknown";
+ }
+}
+
+/* ---- Names: what the say surface says */
+
+typedef struct { char text[4 * VEGA_TEXT]; size_t length; } Name;
+
+static void add(Name *n, const char *text, size_t length) {
+ for (size_t i = 0; i < length && n->length + 1 < sizeof(n->text); ++i)
+ n->text[n->length++] = (unsigned char)text[i] < 0x20 ? ' ' : text[i]; /* control characters say nothing */
+ n->text[n->length] = 0;
+}
+static void add_string(Name *n, const char *text) { add(n, text, strlen(text)); }
+static void add_view(Name *n, PureApp *app, uint32_t view, size_t row) {
+ char text[VEGA_TEXT];
+ size_t length;
+ if (vega_text(app, view, row, text, &length)) {
+ while (length && (unsigned char)text[length - 1] <= 0x20) --length;
+ add(n, text, length);
+ }
+}
+
+static const char *const tone_words[VEGA_TONES] = {NULL, NULL, "accent", "good", "warning", "danger"};
+
+static void name_of(const VegaAccess *a, const Object *o, Name *n) {
+ const VegaScreen *s = a->screen;
+ const PureModule *m = a->app->module;
+ n->length = 0;
+ n->text[0] = 0;
+ switch (o->kind) {
+ case ROOT_OBJECT:
+ case FRAME:
+ case TITLE:
+ if (s->title >= 0) add_view(n, a->app, (uint32_t)s->title, 0);
+ if (!n->length && o->kind != TITLE) add_string(n, "Vega");
+ break;
+ case FIELD:
+ add_string(n, m->view_names[s->parts[o->part].view]);
+ add_string(n, ": ");
+ add_view(n, a->app, s->parts[o->part].view, 0);
+ break;
+ case PART: add_string(n, m->view_names[s->parts[o->part].view]); break;
+ case ROW: {
+ const VegaPart *part = &s->parts[o->part];
+ if (part->kind == VEGA_LIST) {
+ if (part->label >= 0) add_view(n, a->app, (uint32_t)part->label, o->row);
+ int more[] = {part->badge, part->detail};
+ for (unsigned k = 0; k < 2; ++k) {
+ if (more[k] < 0) continue;
+ Name extra = {{0}, 0};
+ add_view(&extra, a->app, (uint32_t)more[k], o->row);
+ if (extra.length) {
+ add_string(n, ", ");
+ add(n, extra.text, extra.length);
+ }
+ }
+ } else
+ for (unsigned c = 0; c < part->column_count; ++c) {
+ if (c) add_string(n, ", ");
+ add_string(n, m->view_names[part->columns[c]]);
+ add_string(n, " ");
+ add_view(n, a->app, part->columns[c], o->row);
+ }
+ const char *tone = tone_words[vega_tone(a->app, part, o->row)];
+ if (tone) {
+ add_string(n, ", ");
+ add_string(n, tone);
+ }
+ break;
+ }
+ case CELL: add_view(n, a->app, s->parts[o->part].columns[o->column], o->row); break;
+ case ENTRY: add_string(n, m->input_names[s->prompt]); break;
+ case KEYS: add_string(n, "Keys"); break;
+ case KEY:
+ add_string(n, m->input_names[o->key]);
+ add_string(n, " (");
+ add_string(n, m->input_keys[o->key]);
+ add_string(n, ")");
+ break;
+ default: break;
+ }
+}
+
+/* ---- States */
+
+static bool focused_row(const VegaAccess *a, const Object *o) {
+ const VegaPart *part = &a->screen->parts[o->part];
+ return vega_focus(a->app, part) == o->row;
+}
+
+static Object focus_of(const VegaAccess *a);
+
+static bool same_object(const Object *x, const Object *y) {
+ return x->kind == y->kind && x->part == y->part && x->row == y->row && x->column == y->column && x->key == y->key;
+}
+
+static void state_of(const VegaAccess *a, const Object *o, uint32_t bits[2]) {
+ bits[0] = bits[1] = 0;
+#define SET(state) (bits[(state) / 32] |= 1u << ((state) % 32))
+ if (o->kind == NONE) return;
+ SET(STATE_ENABLED);
+ SET(STATE_SENSITIVE);
+ SET(STATE_SHOWING);
+ SET(STATE_VISIBLE);
+ /* One object has the focus (focus_of); it alone is focused. */
+ Object focus = focus_of(a);
+ if (same_object(o, &focus)) SET(STATE_FOCUSED);
+ switch (o->kind) {
+ case FRAME: SET(STATE_ACTIVE); break;
+ case PART: SET(STATE_FOCUSABLE); break;
+ case ROW:
+ SET(STATE_SELECTABLE);
+ SET(STATE_FOCUSABLE);
+ if (focused_row(a, o)) SET(STATE_SELECTED);
+ break;
+ case ENTRY:
+ SET(STATE_EDITABLE);
+ SET(STATE_FOCUSABLE);
+ SET(STATE_SINGLE_LINE);
+ break;
+ case KEY: SET(STATE_FOCUSABLE); break;
+ default: break;
+ }
+#undef SET
+}
+
+/* The object that has the focus: the entry while typing, else the focused
+ * row of the list keys move, else that list, else the frame. */
+static Object focus_of(const VegaAccess *a) {
+ if (a->host->typing && a->host->typing(a->host->context) && a->screen->prompt >= 0) return (Object){ENTRY, 0, 0, 0, 0};
+ int active = active_part(a);
+ if (active < 0) return (Object){FRAME, 0, 0, 0, 0};
+ const VegaPart *part = &a->screen->parts[active];
+ size_t focus = vega_focus(a->app, part);
+ if (focus < vega_rows(a->app, part)) return (Object){ROW, (unsigned)active, 0, 0, focus};
+ return (Object){PART, (unsigned)active, 0, 0, 0};
+}
+
+static void interfaces_of(const VegaAccess *a, const Object *o, const char *out[6], size_t *n) {
+ *n = 0;
+ out[(*n)++] = ACCESSIBLE;
+ if (o->kind == ROOT_OBJECT) out[(*n)++] = "org.a11y.atspi.Application";
+ if (o->kind == PART) out[(*n)++] = "org.a11y.atspi.Selection";
+ if (o->kind == KEY || (o->kind == ROW && a->screen->parts[o->part].pointer >= 0)) out[(*n)++] = "org.a11y.atspi.Action";
+ if (o->kind == ENTRY) out[(*n)++] = "org.a11y.atspi.Text";
+}
+
+/* ---- Answers */
+
+static void put_reference(VegaDbusBody *b, const char *name, const char *path) {
+ vega_dbus_put_struct(b);
+ vega_dbus_put_string(b, name);
+ vega_dbus_put_string(b, path);
+}
+
+static void put_object(VegaAccess *a, VegaDbusBody *b, const Object *o) {
+ char path[128];
+ path_of(o, path, sizeof(path));
+ put_reference(b, o->kind == NONE ? "" : a->bus.name, path);
+}
+
+static void put_parent(VegaAccess *a, VegaDbusBody *b, const Object *o) {
+ if (o->kind == ROOT_OBJECT) put_reference(b, a->parent_name, a->parent_path[0] ? a->parent_path : NULL_PATH);
+ else {
+ Object parent = parent_of(o);
+ put_object(a, b, &parent);
+ }
+}
+
+static void reply(VegaAccess *a, const VegaDbusMessage *m, const char *signature, VegaDbusBody *b) {
+ if (!(m->flags & VEGA_DBUS_NO_REPLY))
+ vega_bus_send(&a->bus, VEGA_DBUS_RETURN, VEGA_DBUS_NO_REPLY, m->serial, m->sender, NULL, NULL, NULL, NULL,
+ signature, b, NULL);
+ vega_dbus_body_free(b);
+}
+
+static void refuse(VegaAccess *a, const VegaDbusMessage *m, const char *error, const char *why) {
+ VegaDbusBody b = {0};
+ vega_dbus_put_string(&b, why);
+ if (!(m->flags & VEGA_DBUS_NO_REPLY))
+ vega_bus_send(&a->bus, VEGA_DBUS_ERROR, VEGA_DBUS_NO_REPLY, m->serial, m->sender, NULL, NULL, NULL, error, "s", &b,
+ NULL);
+ vega_dbus_body_free(&b);
+}
+
+/* A property as a variant (its signature, then its value); false for one
+ * the object does not have. */
+static bool put_property(VegaAccess *a, VegaDbusBody *b, const Object *o, const char *interface, const char *name) {
+ Name text;
+ if (!strcmp(interface, ACCESSIBLE)) {
+ if (!strcmp(name, "Name")) {
+ name_of(a, o, &text);
+ vega_dbus_put_signature(b, "s");
+ vega_dbus_put_string(b, text.text);
+ } else if (!strcmp(name, "Description") || !strcmp(name, "HelpText") || !strcmp(name, "AccessibleId")) {
+ vega_dbus_put_signature(b, "s");
+ vega_dbus_put_string(b, "");
+ } else if (!strcmp(name, "Locale")) {
+ vega_dbus_put_signature(b, "s");
+ vega_dbus_put_string(b, "");
+ } else if (!strcmp(name, "Parent")) {
+ vega_dbus_put_signature(b, "(so)");
+ put_parent(a, b, o);
+ } else if (!strcmp(name, "ChildCount")) {
+ vega_dbus_put_signature(b, "i");
+ vega_dbus_put_i32(b, (int32_t)children(a, o, NULL, 0));
+ } else
+ return false;
+ return true;
+ }
+ if (!strcmp(interface, "org.a11y.atspi.Application") && o->kind == ROOT_OBJECT) {
+ const char *value = !strcmp(name, "ToolkitName") ? "Vega" : !strcmp(name, "Version") ? "1"
+ : !strcmp(name, "AtspiVersion") ? "2.1" : NULL;
+ if (value) {
+ vega_dbus_put_signature(b, "s");
+ vega_dbus_put_string(b, value);
+ return true;
+ }
+ if (!strcmp(name, "Id")) {
+ vega_dbus_put_signature(b, "i");
+ vega_dbus_put_i32(b, a->id);
+ return true;
+ }
+ return false;
+ }
+ if (!strcmp(interface, "org.a11y.atspi.Selection") && o->kind == PART && !strcmp(name, "NSelectedChildren")) {
+ const VegaPart *part = &a->screen->parts[o->part];
+ vega_dbus_put_signature(b, "i");
+ vega_dbus_put_i32(b, vega_focus(a->app, part) < vega_rows(a->app, part));
+ return true;
+ }
+ if (!strcmp(interface, "org.a11y.atspi.Action") && !strcmp(name, "NActions")) {
+ vega_dbus_put_signature(b, "i");
+ vega_dbus_put_i32(b, 1);
+ return true;
+ }
+ if (!strcmp(interface, "org.a11y.atspi.Text") && o->kind == ENTRY) {
+ size_t length = 0;
+ const char *typed = a->host->typed ? a->host->typed(a->host->context, &length) : "";
+ int32_t characters = 0;
+ for (size_t i = 0; i < length; ++i) characters += ((unsigned char)typed[i] & 0xC0) != 0x80;
+ if (!strcmp(name, "CharacterCount") || !strcmp(name, "CaretOffset")) {
+ vega_dbus_put_signature(b, "i");
+ vega_dbus_put_i32(b, characters);
+ return true;
+ }
+ }
+ return false;
+}
+
+/* What was said: the characters of typed from start to end (-1: the end). */
+static void put_text_range(VegaDbusBody *b, const char *typed, size_t length, int32_t start, int32_t end) {
+ size_t from = length, to = length;
+ int32_t character = 0;
+ for (size_t i = 0; i <= length; ++i) {
+ if (i == length || ((unsigned char)typed[i] & 0xC0) != 0x80) {
+ if (character == start) from = i;
+ if (character == end) to = i;
+ ++character;
+ }
+ }
+ if (start <= 0) from = 0;
+ if (end < 0 || to < from) to = end < 0 ? length : from;
+ char out[4 * VEGA_TEXT];
+ size_t n = to - from < sizeof(out) - 1 ? to - from : sizeof(out) - 1;
+ memcpy(out, typed + from, n);
+ out[n] = 0;
+ vega_dbus_put_string(b, out);
+}
+
+static void answer(VegaAccess *a, const VegaDbusMessage *m) {
+ const char *interface = m->interface ? m->interface : "", *member = m->member ? m->member : "";
+ VegaDbusReader r;
+ vega_dbus_reader(&r, m);
+ if (!strcmp(interface, "org.freedesktop.DBus.Peer") && !strcmp(member, "Ping")) {
+ VegaDbusBody b = {0};
+ reply(a, m, NULL, &b);
+ return;
+ }
+ Object o = parse_path(a, m->path ? m->path : "");
+ if (!strcmp(interface, "org.freedesktop.DBus.Introspectable") && !strcmp(member, "Introspect")) {
+ VegaDbusBody b = {0};
+ vega_dbus_put_string(&b, "<node/>");
+ reply(a, m, "s", &b);
+ return;
+ }
+ if (o.kind == NONE) {
+ refuse(a, m, "org.freedesktop.DBus.Error.UnknownObject", "no such object");
+ return;
+ }
+ VegaDbusBody b = {0};
+ if (!strcmp(interface, "org.freedesktop.DBus.Properties")) {
+ if (!strcmp(member, "Get")) {
+ const char *on = vega_dbus_get_string(&r), *name = vega_dbus_get_string(&r);
+ if (r.failed || !put_property(a, &b, &o, on, name)) {
+ vega_dbus_body_free(&b);
+ refuse(a, m, "org.freedesktop.DBus.Error.UnknownProperty", "no such property");
+ return;
+ }
+ reply(a, m, "v", &b);
+ return;
+ }
+ if (!strcmp(member, "GetAll")) {
+ const char *on = vega_dbus_get_string(&r);
+ static const char *const names[] = {"Name", "Description", "Parent", "ChildCount", "Locale", "AccessibleId",
+ "HelpText", "ToolkitName", "Version", "AtspiVersion", "Id",
+ "NSelectedChildren", "NActions", "CharacterCount", "CaretOffset"};
+ size_t array = vega_dbus_put_array(&b, 8);
+ for (size_t i = 0; !r.failed && i < sizeof(names) / sizeof(*names); ++i) {
+ size_t before = b.used;
+ vega_dbus_put_struct(&b);
+ vega_dbus_put_string(&b, names[i]);
+ if (!put_property(a, &b, &o, on, names[i])) b.used = before;
+ }
+ vega_dbus_end_array(&b, array, 8);
+ reply(a, m, "a{sv}", &b);
+ return;
+ }
+ if (!strcmp(member, "Set")) {
+ /* the registry numbers us: Application.Id, an int in a variant */
+ const char *on = vega_dbus_get_string(&r), *name = vega_dbus_get_string(&r), *type = vega_dbus_get_signature(&r);
+ if (!r.failed && !strcmp(on, "org.a11y.atspi.Application") && !strcmp(name, "Id") && !strcmp(type, "i"))
+ a->id = vega_dbus_get_i32(&r);
+ reply(a, m, NULL, &b);
+ return;
+ }
+ }
+ if (!strcmp(interface, ACCESSIBLE)) {
+ if (!strcmp(member, "GetRole")) {
+ vega_dbus_put_u32(&b, (uint32_t)role_of(a, &o));
+ reply(a, m, "u", &b);
+ } else if (!strcmp(member, "GetRoleName") || !strcmp(member, "GetLocalizedRoleName")) {
+ vega_dbus_put_string(&b, role_name(role_of(a, &o)));
+ reply(a, m, "s", &b);
+ } else if (!strcmp(member, "GetChildren")) {
+ size_t count = children(a, &o, NULL, 0);
+ Object *kids = malloc((count + 1) * sizeof(*kids));
+ size_t array = vega_dbus_put_array(&b, 8);
+ if (kids) {
+ children(a, &o, kids, count);
+ for (size_t i = 0; i < count; ++i) put_object(a, &b, &kids[i]);
+ }
+ vega_dbus_end_array(&b, array, 8);
+ free(kids);
+ reply(a, m, "a(so)", &b);
+ } else if (!strcmp(member, "GetChildAtIndex")) {
+ int32_t index = vega_dbus_get_i32(&r);
+ size_t count = children(a, &o, NULL, 0);
+ Object kid = {NONE, 0, 0, 0, 0};
+ if (index >= 0 && (size_t)index < count) {
+ Object *kids = malloc(count * sizeof(*kids));
+ if (kids) {
+ children(a, &o, kids, count);
+ kid = kids[index];
+ }
+ free(kids);
+ }
+ put_object(a, &b, &kid);
+ reply(a, m, "(so)", &b);
+ } else if (!strcmp(member, "GetIndexInParent")) {
+ vega_dbus_put_i32(&b, o.kind == ROOT_OBJECT ? -1 : (int32_t)index_in_parent(a, &o));
+ reply(a, m, "i", &b);
+ } else if (!strcmp(member, "GetState")) {
+ uint32_t bits[2];
+ state_of(a, &o, bits);
+ size_t array = vega_dbus_put_array(&b, 4);
+ vega_dbus_put_u32(&b, bits[0]);
+ vega_dbus_put_u32(&b, bits[1]);
+ vega_dbus_end_array(&b, array, 4);
+ reply(a, m, "au", &b);
+ } else if (!strcmp(member, "GetAttributes")) {
+ size_t array = vega_dbus_put_array(&b, 8);
+ vega_dbus_put_struct(&b);
+ vega_dbus_put_string(&b, "toolkit");
+ vega_dbus_put_string(&b, "Vega");
+ if (o.kind == TITLE) {
+ vega_dbus_put_struct(&b);
+ vega_dbus_put_string(&b, "level");
+ vega_dbus_put_string(&b, "1");
+ }
+ if (o.kind == ROW) {
+ char text[32];
+ snprintf(text, sizeof(text), "%zu", o.row + 1);
+ vega_dbus_put_struct(&b);
+ vega_dbus_put_string(&b, "posinset");
+ vega_dbus_put_string(&b, text);
+ snprintf(text, sizeof(text), "%zu", vega_rows(a->app, &a->screen->parts[o.part]));
+ vega_dbus_put_struct(&b);
+ vega_dbus_put_string(&b, "setsize");
+ vega_dbus_put_string(&b, text);
+ }
+ vega_dbus_end_array(&b, array, 8);
+ reply(a, m, "a{ss}", &b);
+ } else if (!strcmp(member, "GetRelationSet")) {
+ size_t array = vega_dbus_put_array(&b, 8);
+ vega_dbus_end_array(&b, array, 8);
+ reply(a, m, "a(ua(so))", &b);
+ } else if (!strcmp(member, "GetApplication")) {
+ Object root = {ROOT_OBJECT, 0, 0, 0, 0};
+ put_object(a, &b, &root);
+ reply(a, m, "(so)", &b);
+ } else if (!strcmp(member, "GetInterfaces")) {
+ const char *names[6];
+ size_t n;
+ interfaces_of(a, &o, names, &n);
+ size_t array = vega_dbus_put_array(&b, 4);
+ for (size_t i = 0; i < n; ++i) vega_dbus_put_string(&b, names[i]);
+ vega_dbus_end_array(&b, array, 4);
+ reply(a, m, "as", &b);
+ } else {
+ vega_dbus_body_free(&b);
+ refuse(a, m, "org.freedesktop.DBus.Error.UnknownMethod", "not a method Vega answers");
+ }
+ return;
+ }
+ if (!strcmp(interface, "org.a11y.atspi.Application")) {
+ if (!strcmp(member, "GetLocale")) vega_dbus_put_string(&b, "");
+ reply(a, m, !strcmp(member, "GetLocale") ? "s" : NULL, &b);
+ return;
+ }
+ if (!strcmp(interface, "org.a11y.atspi.Selection") && o.kind == PART) {
+ const VegaPart *part = &a->screen->parts[o.part];
+ size_t rows = vega_rows(a->app, part), focus = vega_focus(a->app, part);
+ int32_t index = !strcmp(member, "SelectAll") || !strcmp(member, "ClearSelection") ? 0 : vega_dbus_get_i32(&r);
+ if (!strcmp(member, "GetSelectedChild")) {
+ Object kid = index == 0 && focus < rows ? (Object){ROW, o.part, 0, 0, focus} : (Object){NONE, 0, 0, 0, 0};
+ put_object(a, &b, &kid);
+ reply(a, m, "(so)", &b);
+ } else if (!strcmp(member, "SelectChild")) {
+ /* selecting a row focuses it, as a click on it does */
+ bool done = index >= 0 && (size_t)index < rows && vega_point(a->app, part, (size_t)index);
+ if (done && a->host->changed) a->host->changed(a->host->context);
+ vega_dbus_put_bool(&b, done);
+ reply(a, m, "b", &b);
+ } else if (!strcmp(member, "IsChildSelected")) {
+ vega_dbus_put_bool(&b, index >= 0 && (size_t)index == focus);
+ reply(a, m, "b", &b);
+ } else {
+ vega_dbus_put_bool(&b, false); /* one row is always the selection: it cannot be cleared or grown */
+ reply(a, m, "b", &b);
+ }
+ return;
+ }
+ if (!strcmp(interface, "org.a11y.atspi.Action") && (o.kind == KEY || o.kind == ROW)) {
+ const char *what = o.kind == KEY ? "press" : "focus";
+ if (!strcmp(member, "DoAction")) {
+ bool done = false;
+ if (o.kind == KEY) done = a->host->press && a->host->press(a->host->context, a->app->module->input_keys[o.key]);
+ else done = vega_point(a->app, &a->screen->parts[o.part], o.row);
+ if (done && a->host->changed) a->host->changed(a->host->context);
+ vega_dbus_put_bool(&b, done);
+ reply(a, m, "b", &b);
+ } else if (!strcmp(member, "GetActions")) {
+ size_t array = vega_dbus_put_array(&b, 8);
+ vega_dbus_put_struct(&b);
+ vega_dbus_put_string(&b, what);
+ vega_dbus_put_string(&b, "");
+ vega_dbus_put_string(&b, o.kind == KEY ? a->app->module->input_keys[o.key] : "");
+ vega_dbus_end_array(&b, array, 8);
+ reply(a, m, "a(sss)", &b);
+ } else if (!strcmp(member, "GetName") || !strcmp(member, "GetLocalizedName")) {
+ vega_dbus_put_string(&b, what);
+ reply(a, m, "s", &b);
+ } else if (!strcmp(member, "GetDescription")) {
+ vega_dbus_put_string(&b, "");
+ reply(a, m, "s", &b);
+ } else if (!strcmp(member, "GetKeyBinding")) {
+ vega_dbus_put_string(&b, o.kind == KEY ? a->app->module->input_keys[o.key] : "");
+ reply(a, m, "s", &b);
+ } else {
+ vega_dbus_body_free(&b);
+ refuse(a, m, "org.freedesktop.DBus.Error.UnknownMethod", "not a method Vega answers");
+ }
+ return;
+ }
+ if (!strcmp(interface, "org.a11y.atspi.Text") && o.kind == ENTRY && !strcmp(member, "GetText")) {
+ int32_t start = vega_dbus_get_i32(&r), end = vega_dbus_get_i32(&r);
+ size_t length = 0;
+ const char *typed = a->host->typed ? a->host->typed(a->host->context, &length) : "";
+ put_text_range(&b, typed, length, start, end);
+ reply(a, m, "s", &b);
+ return;
+ }
+ vega_dbus_body_free(&b);
+ refuse(a, m, "org.freedesktop.DBus.Error.UnknownMethod", "not a method Vega answers");
+}
+
+static void handle(void *context, const VegaDbusMessage *m) {
+ VegaAccess *a = context;
+ if (m->type == VEGA_DBUS_CALL) answer(a, m);
+}
+
+/* ---- Events */
+
+static void event(VegaAccess *a, const Object *o, const char *member, const char *detail, int32_t detail1) {
+ char path[128];
+ path_of(o, path, sizeof(path));
+ VegaDbusBody b = {0};
+ vega_dbus_put_string(&b, detail);
+ vega_dbus_put_i32(&b, detail1);
+ vega_dbus_put_i32(&b, 0);
+ vega_dbus_put_signature(&b, "i");
+ vega_dbus_put_i32(&b, 0);
+ size_t array = vega_dbus_put_array(&b, 8);
+ vega_dbus_end_array(&b, array, 8);
+ vega_bus_send(&a->bus, VEGA_DBUS_SIGNAL, VEGA_DBUS_NO_REPLY, 0, NULL, path, "org.a11y.atspi.Event.Object", member,
+ NULL, "siiva{sv}", &b, NULL);
+ vega_dbus_body_free(&b);
+}
+
+void vega_access_changed(VegaAccess *a) {
+ Object now = focus_of(a);
+ char path[128];
+ path_of(&now, path, sizeof(path));
+ if (!strcmp(path, a->focused)) return;
+ Object before = parse_path(a, a->focused);
+ if (before.kind != NONE) {
+ event(a, &before, "StateChanged", "focused", 0);
+ if (before.kind == ROW) event(a, &before, "StateChanged", "selected", 0);
+ }
+ if (now.kind == ROW) {
+ event(a, &now, "StateChanged", "selected", 1);
+ Object list = parent_of(&now);
+ event(a, &list, "SelectionChanged", "", 0);
+ }
+ event(a, &now, "StateChanged", "focused", 1);
+ snprintf(a->focused, sizeof(a->focused), "%s", path);
+}
+
+/* ---- The connection */
+
+VegaAccess *vega_access_open(PureApp *app, const VegaScreen *screen, const VegaAccessHost *host, char *error,
+ size_t error_size) {
+ const char *session = getenv("DBUS_SESSION_BUS_ADDRESS");
+ if (!session || !*session) {
+ snprintf(error, error_size, "no session bus");
+ return NULL;
+ }
+ VegaAccess *a = calloc(1, sizeof(*a));
+ if (!a) {
+ snprintf(error, error_size, "out of memory");
+ return NULL;
+ }
+ a->app = app;
+ a->screen = screen;
+ a->host = host;
+ a->bus.fd = -1;
+ /* The accessibility bus is its own: the session bus says where. */
+ VegaBus bus;
+ VegaDbusMessage m;
+ if (!vega_bus_open(&bus, session, error, error_size)) {
+ free(a);
+ return NULL;
+ }
+ char address[512] = "";
+ if (vega_bus_call(&bus, "org.a11y.Bus", "/org/a11y/bus", "org.a11y.Bus", "GetAddress", NULL, NULL, &m, NULL, NULL) &&
+ m.type == VEGA_DBUS_RETURN) {
+ VegaDbusReader r;
+ vega_dbus_reader(&r, &m);
+ snprintf(address, sizeof(address), "%s", vega_dbus_get_string(&r));
+ }
+ vega_bus_close(&bus);
+ if (!address[0]) {
+ snprintf(error, error_size, "no accessibility bus");
+ free(a);
+ return NULL;
+ }
+ if (!vega_bus_open(&a->bus, address, error, error_size)) {
+ free(a);
+ return NULL;
+ }
+ /* Be embedded in the desktop: the registry answers with its root, our
+ * parent. It may ask us things first; they are answered meanwhile. */
+ VegaDbusBody b = {0};
+ put_reference(&b, a->bus.name, ROOT);
+ bool embedded = vega_bus_call(&a->bus, "org.a11y.atspi.Registry", ROOT, "org.a11y.atspi.Socket", "Embed", "(so)", &b, &m,
+ handle, a) &&
+ m.type == VEGA_DBUS_RETURN;
+ vega_dbus_body_free(&b);
+ if (!embedded) {
+ snprintf(error, error_size, "the accessibility registry did not embed us");
+ vega_access_close(a);
+ return NULL;
+ }
+ VegaDbusReader r;
+ vega_dbus_reader(&r, &m);
+ vega_dbus_get_struct(&r);
+ snprintf(a->parent_name, sizeof(a->parent_name), "%s", vega_dbus_get_string(&r));
+ snprintf(a->parent_path, sizeof(a->parent_path), "%s", vega_dbus_get_string(&r));
+ vega_access_changed(a);
+ return a;
+}
+
+int vega_access_fd(const VegaAccess *a) { return a->bus.fd; }
+
+bool vega_access_read(VegaAccess *a) { return vega_bus_read(&a->bus, handle, a); }
+
+void vega_access_close(VegaAccess *a) {
+ if (!a) return;
+ vega_bus_close(&a->bus);
+ free(a);
+}