aboutsummaryrefslogtreecommitdiffstats
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
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.
-rw-r--r--Makefile6
-rw-r--r--docs/design/004-text.md32
-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
-rw-r--r--tests/check.py140
8 files changed, 1552 insertions, 8 deletions
diff --git a/Makefile b/Makefile
index af7b522..bee12a1 100644
--- a/Makefile
+++ b/Makefile
@@ -13,7 +13,7 @@ LIBPURE = $(PURE)/build/libpure.a
# format, drawn by CPU or GPU), text/ (Unicode's algorithms over tables
# text/unicode.py generates from the UCD: python3 text/unicode.py > text/unicode-data.c).
SURFACES = surfaces/screen.c surfaces/draw.c surfaces/width.c surfaces/web.c surfaces/pixels.c surfaces/display.c \
- surfaces/input.c surfaces/hangul.c surfaces/main.c
+ surfaces/input.c surfaces/hangul.c surfaces/dbus.c surfaces/access.c surfaces/main.c
.PHONY: all check FORCE
all: $(BUILD)/vega $(BUILD)/vega-symbols $(BUILD)/vega-surface.so
@@ -24,7 +24,7 @@ $(BUILD):
$(LIBPURE): FORCE
$(MAKE) -C $(PURE) build/libpure.a
-$(BUILD)/vega: $(SURFACES) surfaces/vega.h symbols/symbols.c symbols/symbols.h $(LIBPURE) | $(BUILD)
+$(BUILD)/vega: $(SURFACES) surfaces/vega.h surfaces/dbus.h symbols/symbols.c symbols/symbols.h $(LIBPURE) | $(BUILD)
$(CC) $(CPPFLAGS) $(CFLAGS) -I$(PURE)/core -I$(PURE)/serve -Isymbols $(SURFACES) symbols/symbols.c $(LIBPURE) $(LDFLAGS) -lm $(LDLIBS) -o $@
# The same host as a surface a Pure node maps (pure-serve --surface, Pure's
@@ -34,7 +34,7 @@ $(BUILD)/vega: $(SURFACES) surfaces/vega.h symbols/symbols.c symbols/symbols.h $
# when Pure's headers change: a node refuses a surface built for another
# layout of its app and module.
PURE_HEADERS = $(wildcard $(PURE)/core/*.h) $(PURE)/serve/surface.h
-$(BUILD)/vega-surface.so: $(SURFACES) surfaces/vega.h symbols/symbols.c symbols/symbols.h $(PURE_HEADERS) | $(BUILD)
+$(BUILD)/vega-surface.so: $(SURFACES) surfaces/vega.h surfaces/dbus.h symbols/symbols.c symbols/symbols.h $(PURE_HEADERS) | $(BUILD)
$(CC) $(CPPFLAGS) $(CFLAGS) -Wno-unused-function -fPIC -shared -fvisibility=hidden -ffunction-sections \
-fdata-sections -DVEGA_SURFACE -I$(PURE)/core -I$(PURE)/serve -Isymbols $(SURFACES) symbols/symbols.c \
$(LDFLAGS) -Wl,--gc-sections -lm $(LDLIBS) -o $@
diff --git a/docs/design/004-text.md b/docs/design/004-text.md
index e86f590..0a54f3e 100644
--- a/docs/design/004-text.md
+++ b/docs/design/004-text.md
@@ -175,3 +175,35 @@ advance, 오프셋 가운데 하나라도 다른 실행이 있으면 파일을 �
집합 안의 자리는 DevTools 트리에 없어서, Chromium이 플랫폼 접근성 API로 넘기는 속성으로 본다.
+## 접근성: 리눅스 데스크톱(AT-SPI2)
+
+디스플레이 표면(`vega display --access`)은 접근성 버스에 AT-SPI2 객체로 나타난다. Orca 같은 화면 낭독기가 읽는 표준이다.
+GTK나 ATK, libdbus 없이 D-Bus를 명세의 전송 형식 그대로 말한다([surfaces/dbus.c](../../surfaces/dbus.c)). 유닉스 소켓, EXTERNAL
+인증, 리틀 엔디언의 고정 머리와 머리 필드, 정렬된 본문, 메서드 호출과 응답과 신호다.
+
+[surfaces/access.c](../../surfaces/access.c)는 세션 버스에 접근성 버스의 주소를 묻고, 그 버스에서 레지스트리에 자신을 끼운다
+(`Socket.Embed`). 객체는 말하기 표면이 말하는 순서를 따른다. application 아래 frame(제목)이 있고, 그 아래에 다음이 온다.
+
+- heading
+- 필드마다 label("이름: 값")
+- 목록마다 list box. 행마다 list item이 있다. 포커스된 행은 selected이고, 키가 움직이는 목록이면 focused다. 행은 전체에서의
+ 자리(`posinset`, `setsize`)를 속성으로 가진다.
+- 표마다 table. 칸들을 가진 행으로 이루어진다.
+- entry
+- 키들의 tool bar와 push button
+
+대답은 물을 때마다 앱에서 구하므로 낡을 것이 없다. focused는 한 객체만 가진다. 글을 치는 동안은 entry, 아니면 키가 움직이는 목록의
+포커스된 행이다. 보조 기술은 앱을 움직일 수도 있다. 키의 단추를 누르면(Action) 그 키가 눌리고, 행을 고르면(Selection) 그 행이
+포커스된다. 앱이 바뀌면 옮겨 간 포커스를 사건(`Object:StateChanged` focused·selected, `SelectionChanged`)으로 알린다.
+화면 낭독기가 묻기만 할 때는 다시 그리지 않는다. 전에는 깨어날 때마다 그렸고, 질문이 몰리면 되돌려 쓰기(writeback) 프레임을
+놓쳤다.
+
+확인은 `tests/check.py`의 `accessibility`가 한다. 개인 세션 버스를 띄우고 그 안에서 `at-spi-bus-launcher`를 돌린다. 그리고
+화면 낭독기처럼 gdbus로 다음을 본다.
+
+- 레지스트리가 앱을 나열한다.
+- 트리의 역할과 이름이 맞다(제목 heading, 필드 label, 목록 list box와 a·b·c 항목과 자리, entry, 키 단추).
+- 처음에는 entry만 focused다.
+- Tab 뒤에는 c, 위 화살표 뒤에는 b가 focused다. Selection으로 고르면 a, Action으로 토글 키를 누르면 "a, done"이다.
+- 모니터한 사건에서 포커스가 c, b, a 순서로 옮겨 간다.
+
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. */
diff --git a/tests/check.py b/tests/check.py
index de26a3f..116121d 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -1160,6 +1160,145 @@ def live_display(vega, module, work):
return {'events': len(events), 'lines': len(got)}
+def accessibility(vega, module, work):
+ """The display on an accessibility bus of its own (a private session bus
+ with at-spi-bus-launcher in it), walked as a screen reader walks it, with
+ gdbus: the registry lists the app; its tree has the title as a heading,
+ the field as a label, the list as a list box of items that know their
+ place, the entry and the keys as buttons; one object at a time is
+ focused (the entry while typing, then the list's focused row); keys,
+ a row selected through Selection and a key's button pressed through
+ Action move the focus, and the display says so in events."""
+ import re, shutil, signal, struct, time
+ launcher = Path('/usr/libexec/at-spi-bus-launcher')
+ card, face = Path('/dev/dri/card1'), work / 'ui.vsym'
+ if not (shutil.which('dbus-daemon') and shutil.which('gdbus') and launcher.exists() and card.exists() and face.exists()):
+ return None
+ out = subprocess.run(['dbus-daemon', '--session', '--fork', '--print-address=1', '--print-pid=1', '--nopidfile'],
+ capture_output=True, text=True, check=True).stdout.split('\n')
+ session, daemon = out[0], int(out[1])
+ env = dict(os.environ, DBUS_SESSION_BUS_ADDRESS=session)
+ helper = subprocess.Popen([launcher, '--launch-immediately'], env=env, start_new_session=True,
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ shown, monitor, keys = None, None, None
+ try:
+ for _ in range(100):
+ found = subprocess.run(['gdbus', 'call', '--session', '--dest', 'org.a11y.Bus', '--object-path', '/org/a11y/bus',
+ '--method', 'org.a11y.Bus.GetAddress'], capture_output=True, text=True, env=env)
+ if found.returncode == 0:
+ break
+ time.sleep(0.05)
+ a11y = re.search(r"'([^']*)'", found.stdout).group(1)
+ store = work / 'access-store'
+ assert run(vega, 'cli', module, '--store', store, stdin='text 61\ntext 62\ntext 63\n').returncode == 0
+ fifo = work / 'access-keys'
+ os.mkfifo(fifo)
+ shown = subprocess.Popen([str(vega), 'display', str(module), '--store', str(store), '--symbols', str(face), '--size', '18',
+ '--card', str(card), '--writeback', '640x480', '--out', str(work / 'access'),
+ '--input', str(fifo), '--access'],
+ env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
+ keys = open(fifo, 'wb')
+
+ def call(dest, path, method, *args):
+ result = subprocess.run(['gdbus', 'call', '--address', a11y, '--dest', dest, '--object-path', path, '--method', method,
+ *map(str, args)], capture_output=True, text=True, env=env)
+ if result.returncode:
+ shown.poll()
+ why = shown.stderr.read() if shown.returncode is not None else 'still running'
+ raise AssertionError((path, method, result.stderr, shown.returncode, why))
+ return result.stdout.strip()
+
+ name = None
+ for _ in range(100):
+ apps = re.findall(r"'(:[0-9.]+)', (?:objectpath )?'([^']*)'",
+ call('org.a11y.atspi.Registry', '/org/a11y/atspi/accessible/root',
+ 'org.a11y.atspi.Accessible.GetChildren'))
+ if apps:
+ name, root = apps[-1]
+ break
+ time.sleep(0.05)
+ assert name, 'the registry never listed the display'
+
+ def item(path):
+ role = re.search(r"'([^']*)'", call(name, path, 'org.a11y.atspi.Accessible.GetRoleName')).group(1)
+ label = re.search(r"<'(.*)'>", call(name, path, 'org.freedesktop.DBus.Properties.Get', 'org.a11y.atspi.Accessible',
+ 'Name')).group(1)
+ bits = [int(b) for b in re.findall(r'\d+', call(name, path, 'org.a11y.atspi.Accessible.GetState')
+ .replace('uint32', ''))[:2]]
+ states = {s for s, bit in (('focused', 12), ('selected', 23), ('editable', 7)) if bits[bit // 32] >> (bit % 32) & 1}
+ kids = re.findall(r"'(/[^']*)'", call(name, path, 'org.a11y.atspi.Accessible.GetChildren'))
+ return role, label, states, kids
+
+ def tree(path, into):
+ role, label, states, kids = item(path)
+ into.append((path, role, label, states))
+ for kid in kids:
+ tree(kid, into)
+ return into
+
+ def focused(nodes):
+ return [(role, label) for path, role, label, states in nodes if 'focused' in states]
+
+ nodes = tree(root, [])
+ roles = [(role, label) for path, role, label, states in nodes]
+ assert roles[:3] == [('application', 'To do'), ('frame', 'To do'), ('heading', 'To do')], roles
+ assert ('label', 'left: 3 of 3') in roles and ('list box', 'tasks_list') in roles and ('entry', 'add') in roles, roles
+ items = [(label, states) for path, role, label, states in nodes if role == 'list item']
+ assert [label for label, _ in items] == ['a', 'b', 'c'] and 'selected' in items[2][1], items
+ buttons = {label: path for path, role, label, states in nodes if role == 'push button'}
+ assert 'up (ArrowUp)' in buttons and 'toggle (Space)' in buttons, buttons
+ assert focused(nodes) == [('entry', 'add')], focused(nodes) # typing, as the surface starts
+ rows = [path for path, role, label, states in nodes if role == 'list item']
+ places = call(name, rows[1], 'org.a11y.atspi.Accessible.GetAttributes')
+ assert "'posinset': '2'" in places and "'setsize': '3'" in places, places
+
+ monitor = subprocess.Popen(['gdbus', 'monitor', '--address', a11y, '--dest', name], env=env,
+ stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
+ time.sleep(0.3)
+
+ def press(code):
+ keys.write(struct.pack('llHHi', 0, 0, 1, code, 1) + struct.pack('llHHi', 0, 0, 0, 0, 0) +
+ struct.pack('llHHi', 0, 0, 1, code, 0) + struct.pack('llHHi', 0, 0, 0, 0, 0))
+ keys.flush()
+
+ def until(what, test):
+ for _ in range(100):
+ nodes = tree(root, [])
+ if test(nodes):
+ return nodes
+ time.sleep(0.05)
+ raise AssertionError(f'{what}: {focused(nodes)}')
+
+ press(15) # Tab: keys go to the list
+ until('Tab gives the list the focus', lambda n: focused(n) == [('list item', 'c')])
+ press(103) # ArrowUp
+ until('ArrowUp moves it', lambda n: focused(n) == [('list item', 'b')])
+ assert call(name, [p for p, r, l, s in nodes if r == 'list box'][0], 'org.a11y.atspi.Selection.SelectChild', 0) == '(true,)'
+ until('a row selected through Selection is focused', lambda n: focused(n) == [('list item', 'a')])
+ assert call(name, buttons['toggle (Space)'], 'org.a11y.atspi.Action.DoAction', 0) == '(true,)'
+ nodes = until('the toggle key pressed through Action marks it done',
+ lambda n: focused(n) == [('list item', 'a, done')])
+ keys.close()
+ keys = None
+ out, err = shown.communicate(timeout=20)
+ assert shown.returncode == 0, err
+ monitor.terminate()
+ said = monitor.communicate(timeout=10)[0]
+ events = re.findall(r"(/org/vega/[^:]*): org\.a11y\.atspi\.Event\.Object\.(\w+) \('(\w*)', (-?\d+)", said)
+ moves = [(path.rsplit('/', 1)[-1], detail, int(value)) for path, member, detail, value in events
+ if member == 'StateChanged' and detail == 'focused' and int(value)]
+ assert [row for row, _, _ in moves] == ['2', '1', '0'], events
+ return {'objects': len(nodes), 'events': len(events)}
+ finally:
+ if keys:
+ keys.close()
+ for process in (shown, monitor):
+ if process and process.poll() is None:
+ process.kill()
+ os.killpg(helper.pid, signal.SIGTERM)
+ os.kill(daemon, signal.SIGTERM)
+
+
def hangul(vega, module):
"""Hangul typed on the keyboard (두벌식, surfaces/hangul.c): letter keys
compose syllables in the entry line after the 한/영 key, and Enter commits
@@ -1463,6 +1602,7 @@ def main():
report['symbols'] = symbols(args.symbols.resolve(), work)
report['pixels'] = pixels(vega, args.symbols.resolve(), modules['todo'], work)
report['display'] = display(vega, modules['todo'], work)
+ report['accessibility'] = accessibility(vega, modules['todo'], work)
report['live_display'] = live_display(vega, modules['todo'], work)
report['node_display'] = node_display(vega, args.serve.resolve(), modules['todo'], work)
report['messenger'] = messenger(vega, args.serve.resolve(), modules['messenger'], work)