aboutsummaryrefslogtreecommitdiffstats
path: root/surfaces
diff options
context:
space:
mode:
Diffstat (limited to 'surfaces')
-rw-r--r--surfaces/access.c806
-rw-r--r--surfaces/dbus.c407
-rw-r--r--surfaces/dbus.h85
-rw-r--r--surfaces/main.c62
-rw-r--r--surfaces/vega.h22
5 files changed, 1377 insertions, 5 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);
+}
diff --git a/surfaces/dbus.c b/surfaces/dbus.c
new file mode 100644
index 0000000..e93d8f1
--- /dev/null
+++ b/surfaces/dbus.c
@@ -0,0 +1,407 @@
+/* D-Bus, the part Vega speaks (surfaces/dbus.h), from the specification:
+ * authentication is the text protocol's EXTERNAL mechanism; a message is a
+ * 12-byte fixed header (endianness 'l', type, flags, version 1, body
+ * length, serial), an array of header fields (code, variant), padding to 8,
+ * then the body; every value is aligned to its size from the message's
+ * start (strings to 4, structs to 8). */
+#define _GNU_SOURCE
+#include "dbus.h"
+#include <errno.h>
+#include <poll.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+/* ---- Bodies */
+
+static void reserve(VegaDbusBody *b, size_t n) {
+ if (b->failed || b->used + n <= b->size) return;
+ size_t size = b->size ? b->size : 256;
+ while (size < b->used + n) size *= 2;
+ unsigned char *grown = realloc(b->bytes, size);
+ if (!grown) {
+ b->failed = true;
+ return;
+ }
+ b->bytes = grown;
+ b->size = size;
+}
+
+static void append(VegaDbusBody *b, const void *bytes, size_t n) {
+ reserve(b, n);
+ if (b->failed) return;
+ memcpy(b->bytes + b->used, bytes, n);
+ b->used += n;
+}
+
+static void pad(VegaDbusBody *b, unsigned alignment) {
+ static const unsigned char zero[8] = {0};
+ size_t n = (alignment - b->used % alignment) % alignment;
+ append(b, zero, n);
+}
+
+void vega_dbus_put_byte(VegaDbusBody *b, uint8_t v) { append(b, &v, 1); }
+
+void vega_dbus_put_u32(VegaDbusBody *b, uint32_t v) {
+ pad(b, 4);
+ unsigned char bytes[4] = {(unsigned char)v, (unsigned char)(v >> 8), (unsigned char)(v >> 16), (unsigned char)(v >> 24)};
+ append(b, bytes, 4);
+}
+
+void vega_dbus_put_i32(VegaDbusBody *b, int32_t v) { vega_dbus_put_u32(b, (uint32_t)v); }
+void vega_dbus_put_bool(VegaDbusBody *b, bool v) { vega_dbus_put_u32(b, v ? 1 : 0); }
+
+void vega_dbus_put_string(VegaDbusBody *b, const char *s) {
+ size_t n = strlen(s);
+ vega_dbus_put_u32(b, (uint32_t)n);
+ append(b, s, n + 1);
+}
+
+void vega_dbus_put_signature(VegaDbusBody *b, const char *s) {
+ size_t n = strlen(s);
+ vega_dbus_put_byte(b, (uint8_t)n);
+ append(b, s, n + 1);
+}
+
+void vega_dbus_put_struct(VegaDbusBody *b) { pad(b, 8); }
+
+size_t vega_dbus_put_array(VegaDbusBody *b, unsigned alignment) {
+ vega_dbus_put_u32(b, 0);
+ size_t at = b->used - 4;
+ pad(b, alignment);
+ return at;
+}
+
+void vega_dbus_end_array(VegaDbusBody *b, size_t at, unsigned alignment) {
+ if (b->failed) return;
+ size_t start = at + 4;
+ start += (alignment - start % alignment) % alignment;
+ uint32_t length = (uint32_t)(b->used - start);
+ for (int i = 0; i < 4; ++i) b->bytes[at + i] = (unsigned char)(length >> (8 * i));
+}
+
+void vega_dbus_body_free(VegaDbusBody *b) {
+ free(b->bytes);
+ *b = (VegaDbusBody){0};
+}
+
+/* ---- Reading */
+
+void vega_dbus_reader(VegaDbusReader *r, const VegaDbusMessage *m) { *r = (VegaDbusReader){m->body, m->body_size, 0, false}; }
+
+static bool skip_to(VegaDbusReader *r, unsigned alignment, size_t need) {
+ size_t at = r->at + (alignment - r->at % alignment) % alignment;
+ if (r->failed || at > r->size || need > r->size - at) {
+ r->failed = true;
+ return false;
+ }
+ r->at = at;
+ return true;
+}
+
+uint32_t vega_dbus_get_u32(VegaDbusReader *r) {
+ if (!skip_to(r, 4, 4)) return 0;
+ const unsigned char *p = r->bytes + r->at;
+ r->at += 4;
+ return p[0] | p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24;
+}
+
+int32_t vega_dbus_get_i32(VegaDbusReader *r) { return (int32_t)vega_dbus_get_u32(r); }
+
+const char *vega_dbus_get_string(VegaDbusReader *r) {
+ uint32_t n = vega_dbus_get_u32(r);
+ if (r->failed || n >= r->size - r->at || r->bytes[r->at + n]) {
+ r->failed = true;
+ return "";
+ }
+ const char *s = (const char *)r->bytes + r->at;
+ r->at += n + 1;
+ return s;
+}
+
+const char *vega_dbus_get_signature(VegaDbusReader *r) {
+ if (!skip_to(r, 1, 1)) return "";
+ size_t n = r->bytes[r->at++];
+ if (n >= r->size - r->at || r->bytes[r->at + n]) {
+ r->failed = true;
+ return "";
+ }
+ const char *s = (const char *)r->bytes + r->at;
+ r->at += n + 1;
+ return s;
+}
+
+void vega_dbus_get_struct(VegaDbusReader *r) { skip_to(r, 8, 0); }
+
+/* ---- The connection */
+
+static bool write_all(int fd, const void *bytes, size_t n) {
+ const char *p = bytes;
+ while (n) {
+ ssize_t w = write(fd, p, n);
+ if (w < 0 && errno == EINTR) continue;
+ if (w < 0 && errno == EAGAIN) {
+ struct pollfd wait = {fd, POLLOUT, 0};
+ if (poll(&wait, 1, 5000) <= 0) return false;
+ continue;
+ }
+ if (w <= 0) return false;
+ p += w;
+ n -= (size_t)w;
+ }
+ return true;
+}
+
+/* One line of the authentication protocol, into line (without \r\n). */
+static bool read_line(int fd, char *line, size_t size) {
+ size_t n = 0;
+ while (n + 1 < size) {
+ char c;
+ ssize_t r = read(fd, &c, 1);
+ if (r < 0 && errno == EINTR) continue;
+ if (r <= 0) return false;
+ if (c == '\n') {
+ if (n && line[n - 1] == '\r') --n;
+ line[n] = 0;
+ return true;
+ }
+ line[n++] = c;
+ }
+ return false;
+}
+
+#define REFUSE(...) \
+ do { \
+ snprintf(error, error_size, __VA_ARGS__); \
+ vega_bus_close(bus); \
+ return false; \
+ } while (0)
+
+bool vega_bus_open(VegaBus *bus, const char *address, char *error, size_t error_size) {
+ *bus = (VegaBus){-1, 0, "", NULL, 0, 0, 0};
+ /* The first unix address of the list: path=, or abstract= (a name
+ * outside the file system). */
+ struct sockaddr_un where = {.sun_family = AF_UNIX};
+ socklen_t length = 0;
+ for (const char *p = address; p && *p;) {
+ const char *end = strchr(p, ';');
+ size_t n = end ? (size_t)(end - p) : strlen(p);
+ if (!strncmp(p, "unix:", 5)) {
+ for (const char *key = p + 5; key < p + n;) {
+ const char *comma = memchr(key, ',', (size_t)(p + n - key));
+ size_t k = comma ? (size_t)(comma - key) : (size_t)(p + n - key);
+ bool path = k > 5 && !strncmp(key, "path=", 5), abstract = k > 9 && !strncmp(key, "abstract=", 9);
+ if (path || abstract) {
+ size_t skip = path ? 5 : 9, name = k - skip;
+ if (name + 1 + abstract > sizeof(where.sun_path)) REFUSE("bus address too long");
+ memcpy(where.sun_path + abstract, key + skip, name);
+ length = (socklen_t)(offsetof(struct sockaddr_un, sun_path) + name + abstract + (path ? 1 : 0));
+ }
+ key += k + (comma ? 1 : 0);
+ }
+ if (length) break;
+ }
+ p = end ? end + 1 : NULL;
+ }
+ if (!length) REFUSE("no unix address in %s", address);
+ bus->fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (bus->fd < 0 || connect(bus->fd, (struct sockaddr *)&where, length) < 0) REFUSE("cannot connect to %s", address);
+ /* EXTERNAL: who we are is what the socket says; we name our uid (in
+ * decimal, hex-encoded). */
+ char uid[32], hex[80], line[256];
+ snprintf(uid, sizeof(uid), "%u", (unsigned)getuid());
+ for (size_t i = 0; uid[i]; ++i) snprintf(hex + 2 * i, 3, "%02x", (unsigned char)uid[i]);
+ char auth[128];
+ int n = snprintf(auth, sizeof(auth), "AUTH EXTERNAL %s\r\n", hex);
+ if (!write_all(bus->fd, "", 1) || !write_all(bus->fd, auth, (size_t)n) || !read_line(bus->fd, line, sizeof(line)) ||
+ strncmp(line, "OK ", 3) || !write_all(bus->fd, "BEGIN\r\n", 7))
+ REFUSE("the bus did not take us");
+ VegaDbusMessage reply;
+ if (!vega_bus_call(bus, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "Hello", NULL, NULL,
+ &reply, NULL, NULL) ||
+ reply.type != VEGA_DBUS_RETURN)
+ REFUSE("no Hello from the bus");
+ VegaDbusReader r;
+ vega_dbus_reader(&r, &reply);
+ const char *name = vega_dbus_get_string(&r);
+ if (r.failed || strlen(name) >= sizeof(bus->name)) REFUSE("a strange name from the bus");
+ strcpy(bus->name, name);
+ return true;
+}
+
+void vega_bus_close(VegaBus *bus) {
+ if (bus->fd >= 0) close(bus->fd);
+ free(bus->in);
+ *bus = (VegaBus){-1, 0, "", NULL, 0, 0, 0};
+}
+
+static void field(VegaDbusBody *h, uint8_t code, const char *type, const char *value, uint32_t number) {
+ vega_dbus_put_struct(h);
+ vega_dbus_put_byte(h, code);
+ vega_dbus_put_signature(h, type);
+ if (type[0] == 'u') vega_dbus_put_u32(h, number);
+ else if (type[0] == 'g') vega_dbus_put_signature(h, value);
+ else vega_dbus_put_string(h, value);
+}
+
+bool vega_bus_send(VegaBus *bus, uint8_t type, uint8_t flags, uint32_t reply_serial, const char *destination,
+ const char *path, const char *interface, const char *member, const char *error,
+ const char *signature, const VegaDbusBody *body, uint32_t *serial) {
+ VegaDbusBody h = {0};
+ uint32_t number = ++bus->serial ? bus->serial : ++bus->serial;
+ unsigned char fixed[4] = {'l', type, flags, 1};
+ append(&h, fixed, 4);
+ vega_dbus_put_u32(&h, body ? (uint32_t)body->used : 0);
+ vega_dbus_put_u32(&h, number);
+ size_t fields = vega_dbus_put_array(&h, 8);
+ if (path) field(&h, 1, "o", path, 0);
+ if (interface) field(&h, 2, "s", interface, 0);
+ if (member) field(&h, 3, "s", member, 0);
+ if (error) field(&h, 4, "s", error, 0);
+ if (reply_serial) field(&h, 5, "u", NULL, reply_serial);
+ if (destination) field(&h, 6, "s", destination, 0);
+ if (signature && *signature) field(&h, 8, "g", signature, 0);
+ vega_dbus_end_array(&h, fields, 8);
+ pad(&h, 8);
+ if (body) append(&h, body->bytes, body->used);
+ bool sent = !h.failed && (!body || !body->failed) && write_all(bus->fd, h.bytes, h.used);
+ vega_dbus_body_free(&h);
+ if (serial) *serial = number;
+ return sent;
+}
+
+/* A whole message at the start of bus->in, parsed into m; its length, or 0
+ * when it is not all there yet. A message that is not little-endian or
+ * whose header does not parse is passed over (type 0). */
+static size_t parse(const unsigned char *in, size_t used, VegaDbusMessage *m) {
+ if (used < 16) return 0;
+ uint32_t body = in[4] | in[5] << 8 | (uint32_t)in[6] << 16 | (uint32_t)in[7] << 24;
+ uint32_t fields = in[12] | in[13] << 8 | (uint32_t)in[14] << 16 | (uint32_t)in[15] << 24;
+ if (fields > (1u << 26) || body > (1u << 27)) return used; /* nonsense: drop what we have */
+ size_t header = 16 + fields, total = header + (8 - header % 8) % 8 + body;
+ if (used < total) return 0;
+ *m = (VegaDbusMessage){0};
+ if (in[0] != 'l') return total;
+ m->type = in[1];
+ m->flags = in[2];
+ m->serial = in[8] | in[9] << 8 | (uint32_t)in[10] << 16 | (uint32_t)in[11] << 24;
+ VegaDbusReader r = {in, header, 16, false};
+ while (!r.failed && r.at < header) {
+ vega_dbus_get_struct(&r);
+ if (r.at >= header) break;
+ uint8_t code = in[r.at++];
+ const char *type = vega_dbus_get_signature(&r);
+ if (type[0] == 'u' && !type[1]) {
+ uint32_t v = vega_dbus_get_u32(&r);
+ if (code == 5) m->reply_serial = v;
+ } else if (type[0] == 'g' && !type[1]) {
+ const char *v = vega_dbus_get_signature(&r);
+ if (code == 8) m->signature = v;
+ } else if ((type[0] == 's' || type[0] == 'o') && !type[1]) {
+ const char *v = vega_dbus_get_string(&r);
+ if (code == 1) m->path = v;
+ else if (code == 2) m->interface = v;
+ else if (code == 3) m->member = v;
+ else if (code == 4) m->error = v;
+ else if (code == 6) m->destination = v;
+ else if (code == 7) m->sender = v;
+ } else
+ r.failed = true;
+ }
+ if (r.failed) m->type = 0;
+ m->body = in + header + (8 - header % 8) % 8;
+ m->body_size = body;
+ if (!m->signature) m->signature = "";
+ return total;
+}
+
+/* Bytes from the socket into bus->in, waiting up to timeout ms for them
+ * (0: they are there); false when the bus is gone. */
+static bool fill(VegaBus *bus, int timeout) {
+ if (bus->in_used == bus->in_size) {
+ size_t size = bus->in_size ? 2 * bus->in_size : 65536;
+ unsigned char *grown = realloc(bus->in, size);
+ if (!grown) return false;
+ bus->in = grown;
+ bus->in_size = size;
+ }
+ struct pollfd wait = {bus->fd, POLLIN, 0};
+ if (timeout) {
+ int ready = poll(&wait, 1, timeout);
+ if (ready < 0 && errno != EINTR) return false;
+ if (ready <= 0) return true; /* nothing yet */
+ }
+ ssize_t n = read(bus->fd, bus->in + bus->in_used, bus->in_size - bus->in_used);
+ if (n < 0 && (errno == EAGAIN || errno == EINTR)) return true;
+ if (n <= 0) return false;
+ bus->in_used += (size_t)n;
+ return true;
+}
+
+/* Hand over every whole message in bus->in; stop at the reply to serial
+ * (when not 0), left at the start of bus->in and returned in *reply. */
+static bool drain(VegaBus *bus, uint32_t serial, VegaDbusMessage *reply,
+ void (*handle)(void *, const VegaDbusMessage *), void *context) {
+ size_t at = 0;
+ bool found = false;
+ for (;;) {
+ VegaDbusMessage m;
+ size_t n = parse(bus->in + at, bus->in_used - at, &m);
+ if (!n) break;
+ if (serial && m.reply_serial == serial && (m.type == VEGA_DBUS_RETURN || m.type == VEGA_DBUS_ERROR)) {
+ /* keep it where it is, at the front, until the next read */
+ memmove(bus->in, bus->in + at, bus->in_used - at);
+ bus->in_used -= at;
+ parse(bus->in, bus->in_used, reply);
+ found = true;
+ at = 0;
+ break;
+ }
+ if (m.type && handle) handle(context, &m);
+ at += n;
+ }
+ if (at) {
+ memmove(bus->in, bus->in + at, bus->in_used - at);
+ bus->in_used -= at;
+ }
+ return found;
+}
+
+/* A reply kept at the front of bus->in from the last call is done with. */
+static void forget_reply(VegaBus *bus) {
+ VegaDbusMessage m;
+ size_t n = parse(bus->in, bus->in_used, &m);
+ if (n && m.reply_serial && (m.type == VEGA_DBUS_RETURN || m.type == VEGA_DBUS_ERROR) && bus->kept == m.reply_serial) {
+ memmove(bus->in, bus->in + n, bus->in_used - n);
+ bus->in_used -= n;
+ }
+ bus->kept = 0;
+}
+
+bool vega_bus_call(VegaBus *bus, const char *destination, const char *path, const char *interface, const char *member,
+ const char *signature, const VegaDbusBody *body, VegaDbusMessage *reply,
+ void (*handle)(void *, const VegaDbusMessage *), void *context) {
+ forget_reply(bus);
+ uint32_t serial;
+ if (!vega_bus_send(bus, VEGA_DBUS_CALL, 0, 0, destination, path, interface, member, NULL, signature, body, &serial))
+ return false;
+ /* Five seconds for the reply: a bus that takes longer is not there. */
+ for (int waited = 0; waited < 50; ++waited) {
+ if (drain(bus, serial, reply, handle, context)) {
+ bus->kept = serial;
+ return true;
+ }
+ if (!fill(bus, 100)) return false;
+ }
+ return false;
+}
+
+bool vega_bus_read(VegaBus *bus, void (*handle)(void *, const VegaDbusMessage *), void *context) {
+ forget_reply(bus);
+ if (!fill(bus, 0)) return false;
+ drain(bus, 0, NULL, handle, context);
+ return true;
+}
diff --git a/surfaces/dbus.h b/surfaces/dbus.h
new file mode 100644
index 0000000..01be9a3
--- /dev/null
+++ b/surfaces/dbus.h
@@ -0,0 +1,85 @@
+#ifndef VEGA_DBUS_H
+#define VEGA_DBUS_H
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+/* The part of D-Bus Vega speaks (the D-Bus specification's wire format, no
+ * libdbus): one connection to a bus over a Unix socket, authenticated as
+ * the user (EXTERNAL), messages built and read little-endian. Accessibility
+ * (access.c) is what uses it. */
+
+enum { VEGA_DBUS_CALL = 1, VEGA_DBUS_RETURN = 2, VEGA_DBUS_ERROR = 3, VEGA_DBUS_SIGNAL = 4 };
+enum { VEGA_DBUS_NO_REPLY = 1 };
+
+/* What a message carries, built with the vega_dbus_put functions: each
+ * value aligned as the wire format wants it, relative to the body's start
+ * (a message's body starts 8-aligned). */
+typedef struct {
+ unsigned char *bytes;
+ size_t used, size;
+ bool failed;
+} VegaDbusBody;
+void vega_dbus_put_byte(VegaDbusBody *, uint8_t);
+void vega_dbus_put_bool(VegaDbusBody *, bool);
+void vega_dbus_put_i32(VegaDbusBody *, int32_t);
+void vega_dbus_put_u32(VegaDbusBody *, uint32_t);
+void vega_dbus_put_string(VegaDbusBody *, const char *); /* also an object path */
+void vega_dbus_put_signature(VegaDbusBody *, const char *);
+void vega_dbus_put_struct(VegaDbusBody *); /* aligns to 8, as a struct or dict entry starts */
+/* An array: put_array gives where its length goes; end_array fills it
+ * in. alignment is its elements' (8 for structs and dict entries). */
+size_t vega_dbus_put_array(VegaDbusBody *, unsigned alignment);
+void vega_dbus_end_array(VegaDbusBody *, size_t at, unsigned alignment);
+void vega_dbus_body_free(VegaDbusBody *);
+
+/* A message read: its header's fields (NULL when absent) and its body. */
+typedef struct {
+ uint8_t type, flags;
+ uint32_t serial, reply_serial;
+ const char *path, *interface, *member, *error, *destination, *sender, *signature;
+ const unsigned char *body;
+ size_t body_size;
+} VegaDbusMessage;
+
+/* Reading a body in order; a read past its end (or of the wrong shape)
+ * fails the reader, and every read after gives zero. */
+typedef struct {
+ const unsigned char *bytes;
+ size_t size, at;
+ bool failed;
+} VegaDbusReader;
+void vega_dbus_reader(VegaDbusReader *, const VegaDbusMessage *);
+uint32_t vega_dbus_get_u32(VegaDbusReader *);
+int32_t vega_dbus_get_i32(VegaDbusReader *);
+const char *vega_dbus_get_string(VegaDbusReader *); /* also an object path */
+const char *vega_dbus_get_signature(VegaDbusReader *);
+void vega_dbus_get_struct(VegaDbusReader *);
+
+typedef struct {
+ int fd;
+ uint32_t serial;
+ char name[128]; /* the unique name the bus gave us */
+ unsigned char *in;
+ size_t in_used, in_size;
+ uint32_t kept; /* the call whose reply stays at the start of in */
+} VegaBus;
+
+/* Connect to address (unix:path=... or unix:abstract=...), authenticate,
+ * say Hello; false with why. */
+bool vega_bus_open(VegaBus *, const char *address, char *error, size_t error_size);
+void vega_bus_close(VegaBus *);
+/* Send a message; its serial goes to *serial when given. */
+bool vega_bus_send(VegaBus *, uint8_t type, uint8_t flags, uint32_t reply_serial, const char *destination,
+ const char *path, const char *interface, const char *member, const char *error,
+ const char *signature, const VegaDbusBody *body, uint32_t *serial);
+/* Call a method and wait for its reply (a return or an error) into reply,
+ * which stays valid until the next read; messages that come first go to
+ * handle. */
+bool vega_bus_call(VegaBus *, const char *destination, const char *path, const char *interface, const char *member,
+ const char *signature, const VegaDbusBody *body, VegaDbusMessage *reply,
+ void (*handle)(void *, const VegaDbusMessage *), void *context);
+/* Read what the socket has and hand each whole message to handle; false
+ * when the bus is gone. */
+bool vega_bus_read(VegaBus *, void (*handle)(void *, const VegaDbusMessage *), void *context);
+#endif
diff --git a/surfaces/main.c b/surfaces/main.c
index b0c32b5..d58625a 100644
--- a/surfaces/main.c
+++ b/surfaces/main.c
@@ -595,6 +595,26 @@ static bool show_frame(const VegaGrid *grid, char path[512], unsigned long *diff
static volatile sig_atomic_t stopping;
static void on_stop(int signal) { (void)signal; stopping = 1; }
+/* Accessibility (--access): what the surface tells it, and does for it. */
+static bool accessible;
+static bool access_typing(void *context) { (void)context; return typing; }
+static const char *access_typed(void *context, size_t *length) {
+ (void)context;
+ *length = entry_length;
+ return entry;
+}
+static bool access_press(void *context, const char *key) {
+ (void)context;
+ int input;
+ return press(key, &input);
+}
+static bool access_acted; /* assistive technology changed the app: draw again */
+static void access_changed(void *context) {
+ (void)context;
+ access_acted = true;
+}
+static const VegaAccessHost access_host = {NULL, access_typing, access_typed, access_press, access_changed};
+
static void show_pointer(uint32_t *out, size_t stride, int x, int y, unsigned width, unsigned height) {
/* The cell under a mouse pointer, outlined. */
int left = x / face.cell_width * face.cell_width, top = y / face.cell_height * face.cell_height;
@@ -631,14 +651,28 @@ static int live(VegaInput *input, unsigned columns, unsigned rows, bool recorded
unsigned long differing = 0;
bool open_input = true, printed = false;
int status = 0;
+ VegaAccess *access = NULL;
+ if (accessible) {
+ char error[256];
+ if (!(access = vega_access_open(&app, &screen, &access_host, error, sizeof(error))))
+ fprintf(stderr, "vega: no accessibility: %s\n", error);
+ }
+ /* A frame is drawn when something may have changed it: first, then
+ * after input, ticks, a node's save, or what assistive technology did
+ * (not for what it only asked). */
+ bool dirty = true;
while (!stopping && !(node && node->stopping(node->host))) {
bool ticked = false;
long due = node ? -1 : vega_timers_run(&timers, &app, vega_now(), &ticked);
PureSnapshot *snapshot = node ? node->read(node->host, &app) : NULL;
+ dirty = dirty || ticked || snapshot || access_acted;
+ access_acted = false;
char line[VEGA_TEXT + 4];
- if (!node || snapshot) vega_draw(&grid, &app, &screen, line, shown_entry(line), typing);
+ if (dirty && (!node || snapshot)) vega_draw(&grid, &app, &screen, line, shown_entry(line), typing);
if (snapshot) node->release(node->host, snapshot);
- if (!display) { /* a node's pixels: each frame into a file */
+ if (!dirty) {
+ /* nothing to draw */
+ } else if (!display) { /* a node's pixels: each frame into a file */
if (!write_frame(&grid, path)) { fprintf(stderr, "vega: cannot write %s\n", path); status = 1; break; }
} else {
unsigned width, height;
@@ -652,20 +686,23 @@ static int live(VegaInput *input, unsigned columns, unsigned rows, bool recorded
if (pointer) show_pointer(out, stride, x, y, width, height);
if (!show_frame(&grid, path, &differing, false)) { fputs("vega: the display did not take the frame\n", stderr); status = 1; break; }
}
+ dirty = false;
+ if (access) vega_access_changed(access);
if (!open_input && !printed && recorded) {
print_last(&grid, path, differing);
printed = true;
}
if (!open_input && !node) break;
- struct pollfd waits[33];
+ struct pollfd waits[34];
unsigned count = open_input ? vega_input_fds(input, waits, 32) : 0;
if (node) waits[count++] = (struct pollfd){node->changes, POLLIN, 0}; /* the node saved: draw again */
+ if (access) waits[count++] = (struct pollfd){vega_access_fd(access), POLLIN, 0}; /* a screen reader asks */
int timeout = screen.ticks && !node ? 16 : -1;
if (due >= 0 && (timeout < 0 || due < timeout)) timeout = (int)due;
if (poll(waits, count, timeout) <= 0) {
if (node) continue;
app.now = vega_now();
- if (screen.ticks) pure_app_frame(&app);
+ if (screen.ticks) pure_app_frame(&app), dirty = true;
continue;
}
if (!node) app.now = vega_now();
@@ -676,9 +713,18 @@ static int live(VegaInput *input, unsigned columns, unsigned rows, bool recorded
if (read(node->changes, &saves, sizeof(saves)) < 0) { /* cleared already */ }
continue;
}
+ if (access && waits[i].fd == vega_access_fd(access)) {
+ if (!vega_access_read(access)) {
+ fputs("vega: the accessibility bus went away\n", stderr);
+ vega_access_close(access);
+ access = NULL;
+ }
+ continue;
+ }
VegaEvent events[64];
bool ended;
unsigned n = vega_input_read(input, waits[i].fd, events, 64, &ended);
+ if (n || ended) dirty = true;
for (unsigned k = 0; k < n; ++k) {
const VegaEvent *e = &events[k];
unsigned line = e->y >= 0 ? (unsigned)(e->y / face.cell_height) : 0;
@@ -691,6 +737,7 @@ static int live(VegaInput *input, unsigned columns, unsigned rows, bool recorded
}
}
if (recorded && !printed) print_last(&grid, path, differing);
+ vega_access_close(access);
drawn = NULL;
vega_grid_free(&grid);
return status;
@@ -937,6 +984,11 @@ int main(int argc, char **argv) {
--i;
continue;
}
+ if (!strcmp(argv[i], "--access") && !strcmp(argv[1], "display")) {
+ accessible = true;
+ --i;
+ continue;
+ }
if (i + 1 >= argc) usage = true;
else if (!strcmp(argv[i], "--store")) store = argv[i + 1];
else if (!strcmp(argv[i], "--listen") && !strcmp(argv[1], "web")) listen_on = argv[i + 1];
@@ -964,7 +1016,7 @@ int main(int argc, char **argv) {
"vega web MODULE.pmod [--store PATH] [--listen ADDRESS] [--port N] [--watch]\n"
"vega pixels MODULE.pmod [--store PATH] --symbols FONT.vsym [--size PX] --grid WIDTHxHEIGHT --out PREFIX < script\n"
"vega display MODULE.pmod [--store PATH] --symbols FONT.vsym [--size PX] [--card PATH]\n"
- " [--writeback WIDTHxHEIGHT --out PREFIX] [--live | --input EVENTS] [< script]\n"
+ " [--writeback WIDTHxHEIGHT --out PREFIX] [--live | --input EVENTS] [--access] [< script]\n"
"vega check MODULE.pmod\n", stderr);
return 2;
}
diff --git a/surfaces/vega.h b/surfaces/vega.h
index cd6b1ee..9f30ee4 100644
--- a/surfaces/vega.h
+++ b/surfaces/vega.h
@@ -139,6 +139,28 @@ void vega_pixels(const VegaGrid *, VegaFace *, uint32_t *out, size_t stride);
/* width x height of out in the background's colour. */
void vega_pixels_clear(uint32_t *out, size_t stride, unsigned width, unsigned height);
+/* Accessibility on Linux desktops (access.c): the screen as AT-SPI2 objects
+ * on the accessibility bus, for screen readers. The surface that hosts it
+ * says whether the user is typing and what, presses keys for assistive
+ * technology, and hears when that changed the app. */
+typedef struct {
+ void *context;
+ bool (*typing)(void *);
+ const char *(*typed)(void *, size_t *length);
+ bool (*press)(void *, const char *key);
+ void (*changed)(void *);
+} VegaAccessHost;
+typedef struct VegaAccess VegaAccess;
+/* Join the accessibility bus; NULL (with why) when there is none. */
+VegaAccess *vega_access_open(PureApp *, const VegaScreen *, const VegaAccessHost *, char *error, size_t error_size);
+int vega_access_fd(const VegaAccess *);
+/* Answer what assistive technology asked (the fd is readable); false when
+ * the bus is gone. */
+bool vega_access_read(VegaAccess *);
+/* After the app changed: the focus that moved, as events. */
+void vega_access_changed(VegaAccess *);
+void vega_access_close(VegaAccess *);
+
/* A display through KMS: the first connected connector (or, with writeback,
* a writeback connector: the frames come back to memory, so no screen is
* needed to check them), its preferred mode or width x height. */