diff options
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | docs/design/003-pixel-surface.md | 17 | ||||
| -rw-r--r-- | surfaces/input.c | 190 | ||||
| -rw-r--r-- | surfaces/main.c | 125 | ||||
| -rw-r--r-- | surfaces/vega.h | 20 | ||||
| -rw-r--r-- | tests/check.py | 53 |
7 files changed, 394 insertions, 16 deletions
@@ -12,7 +12,7 @@ LIBPURE = $(PURE)/build/libpure.a # host: TUI, CLI, NUI sentences, web), symbols/ (glyphs and icons as one # format, drawn by CPU or GPU). SURFACES = surfaces/screen.c surfaces/draw.c surfaces/width.c surfaces/web.c surfaces/pixels.c surfaces/display.c \ - surfaces/main.c + surfaces/input.c surfaces/main.c .PHONY: all check FORCE all: $(BUILD)/vega $(BUILD)/vega-symbols @@ -16,7 +16,8 @@ printf 'text 6d696c6b\nkey Space\nshow\n' | build/vega say todo.pmod build/vega web todo.pmod --watch # 브라우저(GUI). --watch: 모듈이 다시 빌드되면 상태를 이어 바꾼다 # (옆에서 AUP_PATH=lib aup watch examples/todo.aup -o todo.pmod) build/vega pixels todo.pmod --symbols ui.vsym --grid 48x12 --out frame < script # 픽셀: frame-N.ppm (설계 003) -build/vega display todo.pmod --symbols ui.vsym < script # 디스플레이(KMS). --writeback WxH --out: 화면 없이 되돌려 받기 +build/vega display todo.pmod --symbols ui.vsym --live # 디스플레이(KMS)와 키보드·마우스·터치(evdev) + # (< script, --input 기록한 사건, --writeback WxH --out: 화면 없이 되돌려 받기) make check # 참조 모델 대조, 프레임 검사, 대화형 pty 검사, 견고성 ``` diff --git a/docs/design/003-pixel-surface.md b/docs/design/003-pixel-surface.md index 9d9f2b1..725f3b1 100644 --- a/docs/design/003-pixel-surface.md +++ b/docs/design/003-pixel-surface.md @@ -41,9 +41,22 @@ PPM 쓰기)가 7 ms다. 192×45칸(1920×1350)은 프레임마다 12 ms다(그� 프레임 하나가 20.6 ms다(합성, 복사, 커밋과 수직 동기 한 번(60 Hz, 16.7 ms), 캡처를 읽어 대 보고 6 MB PPM을 쓰기까지). 돌아온 그림은 늘 그린 것과 같았다(다른 픽셀 0). +## 입력(evdev) + +`--live`는 스크립트 대신 사람의 입력을 받는다(`surfaces/input.c`, 라이브러리 없이 커널의 입력 사건을 읽는다). 키보드는 Pure의 키 +이름(`KeyboardEvent.code`의 철자)과 US 배열에서 그 키가 치는 글자가 되고, TUI가 쓰는 것과 같은 처리(`handle_key`, +`handle_text`: 입력 줄이면 글이, 아니면 키가 된다)로 간다. 마우스는 움직임과 왼쪽 단추와 휠이, 터치 화면은 장치의 범위를 화면에 +맞춘 위치와 누름이 된다. 누른 픽셀의 줄이 곧 행이다(`vega_hit`). 마우스 포인터는 그 아래 칸의 테두리로 보인다. 신호(SIGINT, +SIGTERM)에서 멈춘다. + +`--input FILE`은 파일에 기록된 사건(`struct input_event`, 위치는 화면 픽셀)을 끝까지 읽고 마지막 프레임을 글로 낸다. 화면도 +키보드도 없는 Pi에서 이 경로로 확인한다(`/dev/uinput`은 root만 쓴다). + ## 확인 -`tests/check.py` 11번과 12번. 12번은 writeback 커넥터가 있으면 같은 스크립트를 디스플레이로 돌린다. 프레임마다 돌아온 그림이 +`tests/check.py` 11번과 12번. 12번의 둘째 부분은 기록된 사건(Shift로 대문자를 치고 Enter, Tab, Space, 첫 항목 행을 터치)으로 +디스플레이를 돌린다. 마지막 프레임이 같은 키와 클릭을 받은 TUI의 프레임과 줄마다 같고, writeback으로 돌아온 그림이 그린 것과 +같다(사건 62개). 12번은 writeback 커넥터가 있으면 같은 스크립트를 디스플레이로 돌린다. 프레임마다 돌아온 그림이 그린 것과 같고, 같은 격자의 픽셀 표면 그림과 픽셀 하나까지 같다(640×480에서 프레임 둘, 589,512 픽셀). `tests/check.py` 11번. 같은 스크립트를 `tui --dump`와 `pixels`로 돌려 글이 같고, 그림에서 빈 칸은 그 행의 배경(포커스 행은 제 @@ -52,5 +65,5 @@ PPM 쓰기)가 7 ms다. 192×45칸(1920×1350)은 프레임마다 12 ms다(그� ## 다음 - 바뀐 칸만 다시 그리고 복사한다(TUI가 이미 칸마다 보인 것을 기억한다). 커밋을 기다리지 않고 페이지 넘김 이벤트로 맞춘다. -- 입력: evdev(키보드, 마우스, 터치). 칸의 격자가 있으니 클릭이 곧 행이다(`vega_hit`). +- 입력: 한글 입력기(지금은 US 배열의 글자만 친다), 제스처(끌기로 스크롤). - 비례 글꼴의 줄: 칸이 아닌 글자 폭으로 놓는 표와 문단. 합성은 GPU로(아틀라스, [002](002-symbol-format.md)). diff --git a/surfaces/input.c b/surfaces/input.c new file mode 100644 index 0000000..b358d47 --- /dev/null +++ b/surfaces/input.c @@ -0,0 +1,190 @@ +#define _DEFAULT_SOURCE +#include "vega.h" +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <linux/input.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <unistd.h> + +/* Input from evdev (the kernel's input events): keyboards, mice and touch + * screens, read without a library. Keys come out as Pure's key names (as + * KeyboardEvent.code spells them) and the text they type on a US layout; + * pointers as presses at a pixel. */ + +enum { DEVICES = 32 }; + +typedef struct { + int fd; + bool touch; /* absolute positions, scaled to the screen */ + int min[2], max[2]; /* the device's range on x and y */ +} Device; + +struct VegaInput { + Device devices[DEVICES]; + unsigned count; + unsigned width, height; /* the screen, for pointers */ + int x, y; /* the pointer */ + bool shift, touched, moved; +}; + +/* Key names by code, with what a key types on a US layout (unshifted, shifted). */ +static const struct { unsigned short code; const char *name; char plain, shifted; } keys[] = { + {KEY_ESC, "Escape", 0, 0}, {KEY_1, "Digit1", '1', '!'}, {KEY_2, "Digit2", '2', '@'}, {KEY_3, "Digit3", '3', '#'}, + {KEY_4, "Digit4", '4', '$'}, {KEY_5, "Digit5", '5', '%'}, {KEY_6, "Digit6", '6', '^'}, {KEY_7, "Digit7", '7', '&'}, + {KEY_8, "Digit8", '8', '*'}, {KEY_9, "Digit9", '9', '('}, {KEY_0, "Digit0", '0', ')'}, {KEY_MINUS, "Minus", '-', '_'}, + {KEY_EQUAL, "Equal", '=', '+'}, {KEY_BACKSPACE, "Backspace", 0, 0}, {KEY_TAB, "Tab", 0, 0}, + {KEY_Q, "KeyQ", 'q', 'Q'}, {KEY_W, "KeyW", 'w', 'W'}, {KEY_E, "KeyE", 'e', 'E'}, {KEY_R, "KeyR", 'r', 'R'}, + {KEY_T, "KeyT", 't', 'T'}, {KEY_Y, "KeyY", 'y', 'Y'}, {KEY_U, "KeyU", 'u', 'U'}, {KEY_I, "KeyI", 'i', 'I'}, + {KEY_O, "KeyO", 'o', 'O'}, {KEY_P, "KeyP", 'p', 'P'}, {KEY_LEFTBRACE, "BracketLeft", '[', '{'}, + {KEY_RIGHTBRACE, "BracketRight", ']', '}'}, {KEY_ENTER, "Enter", 0, 0}, {KEY_A, "KeyA", 'a', 'A'}, + {KEY_S, "KeyS", 's', 'S'}, {KEY_D, "KeyD", 'd', 'D'}, {KEY_F, "KeyF", 'f', 'F'}, {KEY_G, "KeyG", 'g', 'G'}, + {KEY_H, "KeyH", 'h', 'H'}, {KEY_J, "KeyJ", 'j', 'J'}, {KEY_K, "KeyK", 'k', 'K'}, {KEY_L, "KeyL", 'l', 'L'}, + {KEY_SEMICOLON, "Semicolon", ';', ':'}, {KEY_APOSTROPHE, "Quote", '\'', '"'}, {KEY_GRAVE, "Backquote", '`', '~'}, + {KEY_BACKSLASH, "Backslash", '\\', '|'}, {KEY_Z, "KeyZ", 'z', 'Z'}, {KEY_X, "KeyX", 'x', 'X'}, + {KEY_C, "KeyC", 'c', 'C'}, {KEY_V, "KeyV", 'v', 'V'}, {KEY_B, "KeyB", 'b', 'B'}, {KEY_N, "KeyN", 'n', 'N'}, + {KEY_M, "KeyM", 'm', 'M'}, {KEY_COMMA, "Comma", ',', '<'}, {KEY_DOT, "Period", '.', '>'}, + {KEY_SLASH, "Slash", '/', '?'}, {KEY_SPACE, "Space", ' ', ' '}, {KEY_F1, "F1", 0, 0}, {KEY_F2, "F2", 0, 0}, + {KEY_F3, "F3", 0, 0}, {KEY_F4, "F4", 0, 0}, {KEY_F5, "F5", 0, 0}, {KEY_F6, "F6", 0, 0}, {KEY_F7, "F7", 0, 0}, + {KEY_F8, "F8", 0, 0}, {KEY_F9, "F9", 0, 0}, {KEY_F10, "F10", 0, 0}, {KEY_F11, "F11", 0, 0}, {KEY_F12, "F12", 0, 0}, + {KEY_HOME, "Home", 0, 0}, {KEY_UP, "ArrowUp", 0, 0}, {KEY_PAGEUP, "PageUp", 0, 0}, {KEY_LEFT, "ArrowLeft", 0, 0}, + {KEY_RIGHT, "ArrowRight", 0, 0}, {KEY_END, "End", 0, 0}, {KEY_DOWN, "ArrowDown", 0, 0}, + {KEY_PAGEDOWN, "PageDown", 0, 0}, {KEY_INSERT, "Insert", 0, 0}, {KEY_DELETE, "Delete", 0, 0}, +}; + +static bool has(int fd, unsigned type, unsigned code) { + unsigned long bits[KEY_MAX / (8 * sizeof(long)) + 1] = {0}; + if (ioctl(fd, EVIOCGBIT(type, sizeof(bits)), bits) < 0) return false; + return bits[code / (8 * sizeof(long))] >> (code % (8 * sizeof(long))) & 1; +} + +/* A device worth reading: one with keys, a relative pointer, or touch. */ +static bool add(VegaInput *input, int fd, bool probe) { + Device device = {.fd = fd}; + if (probe) { + bool keys_ = has(fd, EV_KEY, KEY_A) || has(fd, EV_KEY, KEY_ENTER), mouse = has(fd, EV_REL, REL_X); + device.touch = has(fd, EV_ABS, ABS_MT_POSITION_X) || has(fd, EV_ABS, ABS_X); + if (!keys_ && !mouse && !device.touch) return false; + for (int axis = 0; device.touch && axis < 2; ++axis) { + struct input_absinfo range; + unsigned code = has(fd, EV_ABS, ABS_MT_POSITION_X) ? (axis ? ABS_MT_POSITION_Y : ABS_MT_POSITION_X) + : (axis ? ABS_Y : ABS_X); + if (ioctl(fd, EVIOCGABS(code), &range) < 0 || range.maximum <= range.minimum) device.touch = false; + else device.min[axis] = range.minimum, device.max[axis] = range.maximum; + } + } else device.touch = true, device.min[0] = device.min[1] = 0, device.max[0] = device.max[1] = 0; /* raw: screen pixels */ + if (input->count == DEVICES) return false; + input->devices[input->count++] = device; + return true; +} + +VegaInput *vega_input_open(const char *const *paths, unsigned count, unsigned width, unsigned height) { + VegaInput *input = calloc(1, sizeof(*input)); + if (!input) return NULL; + input->width = width, input->height = height, input->x = (int)width / 2, input->y = (int)height / 2; + if (count) { /* files of recorded events, positions already in screen pixels */ + for (unsigned i = 0; i < count; ++i) { + int fd = open(paths[i], O_RDONLY | O_CLOEXEC); + if (fd < 0 || !add(input, fd, false)) { if (fd >= 0) close(fd); vega_input_close(input); return NULL; } + } + return input; + } + DIR *directory = opendir("/dev/input"); + struct dirent *entry; + while (directory && (entry = readdir(directory))) { + if (strncmp(entry->d_name, "event", 5)) continue; + char path[300]; + snprintf(path, sizeof(path), "/dev/input/%s", entry->d_name); + int fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); + if (fd >= 0 && !add(input, fd, true)) close(fd); + } + if (directory) closedir(directory); + return input; +} + +unsigned vega_input_fds(const VegaInput *input, struct pollfd *out, unsigned max) { + unsigned n = 0; + for (unsigned i = 0; i < input->count && n < max; ++i) out[n++] = (struct pollfd){input->devices[i].fd, POLLIN, 0}; + return n; +} + +static int scaled(const Device *device, int axis, int value, unsigned size) { + if (device->max[axis] <= device->min[axis]) return value; /* already in screen pixels */ + long long span = device->max[axis] - device->min[axis]; + return (int)((long long)(value - device->min[axis]) * (long long)(size - 1) / span); +} + +static unsigned emit(VegaEvent *out, unsigned n, unsigned max, VegaEvent event) { + if (n < max) out[n++] = event; + return n; +} + +/* One event of a device into what it means; returns the new count. */ +static unsigned translate(VegaInput *input, const Device *device, const struct input_event *e, VegaEvent *out, unsigned n, + unsigned max) { + if (e->type == EV_KEY && (e->code == KEY_LEFTSHIFT || e->code == KEY_RIGHTSHIFT)) { + input->shift = e->value != 0; + return n; + } + if (e->type == EV_KEY && (e->code == BTN_LEFT || e->code == BTN_TOUCH) && e->value == 1) { + if (e->code == BTN_TOUCH) input->touched = true; + return emit(out, n, max, (VegaEvent){VEGA_EVENT_PRESS, "", "", input->x, input->y, 0}); + } + if (e->type == EV_KEY && e->value != 0) { /* a press or its repeat */ + for (size_t i = 0; i < sizeof(keys) / sizeof(*keys); ++i) + if (keys[i].code == e->code) { + VegaEvent event = {VEGA_EVENT_KEY, "", "", input->x, input->y, 0}; + snprintf(event.name, sizeof(event.name), "%s", keys[i].name); + char c = input->shift ? keys[i].shifted : keys[i].plain; + if (c) event.kind = VEGA_EVENT_TEXT, event.text[0] = c; + return emit(out, n, max, event); + } + return n; + } + if (e->type == EV_REL && (e->code == REL_X || e->code == REL_Y)) { + int *at = e->code == REL_X ? &input->x : &input->y, limit = (int)(e->code == REL_X ? input->width : input->height); + *at += e->value; + *at = *at < 0 ? 0 : *at >= limit ? limit - 1 : *at; + input->moved = true; + return n; + } + if (e->type == EV_REL && e->code == REL_WHEEL && e->value) + return emit(out, n, max, (VegaEvent){VEGA_EVENT_WHEEL, "", "", input->x, input->y, e->value > 0 ? -1 : 1}); + if (e->type == EV_ABS && (e->code == ABS_MT_POSITION_X || e->code == ABS_X)) { + input->x = scaled(device, 0, e->value, input->width); + input->moved = true; + } else if (e->type == EV_ABS && (e->code == ABS_MT_POSITION_Y || e->code == ABS_Y)) { + input->y = scaled(device, 1, e->value, input->height); + input->moved = true; + } + return n; +} + +unsigned vega_input_read(VegaInput *input, int fd, VegaEvent *out, unsigned max, bool *ended) { + Device *device = NULL; + for (unsigned i = 0; i < input->count; ++i) if (input->devices[i].fd == fd) device = &input->devices[i]; + *ended = false; + if (!device) return 0; + struct input_event events[64]; + ssize_t got = read(fd, events, sizeof(events)); + if (got <= 0) { + *ended = got == 0 || (errno != EAGAIN && errno != EINTR); + return 0; + } + unsigned n = 0; + for (size_t i = 0; i < (size_t)got / sizeof(*events); ++i) n = translate(input, device, &events[i], out, n, max); + return n; +} + +void vega_input_pointer(const VegaInput *input, int *x, int *y, bool *shown) { + *x = input->x, *y = input->y; + *shown = input->moved && !input->touched; /* a mouse has a pointer to see; a finger does not */ +} + +void vega_input_close(VegaInput *input) { + if (!input) return; + for (unsigned i = 0; i < input->count; ++i) close(input->devices[i].fd); + free(input); +} diff --git a/surfaces/main.c b/surfaces/main.c index 0d02cce..26aa4eb 100644 --- a/surfaces/main.c +++ b/surfaces/main.c @@ -24,10 +24,13 @@ * the TUI's frames drawn with a font's glyphs, driven by a * script: each show writes PREFIX-N.ppm after the text * display --symbols FONT.vsym [--size PX] [--card PATH] [--writeback WxH --out PREFIX] + * [--live | --input EVENTS] * the same on a display through KMS, as many cells as fit, - * driven by a script; with writeback each shown frame comes - * back from the display controller, is set against what was - * drawn and written to PREFIX-N.ppm + * driven by a script, or live: keyboards, mice and touch + * screens (evdev), or events recorded in a file (read to its + * end, then the last frame shown as text); with writeback + * each shown frame comes back from the display controller, + * is set against what was drawn and written to PREFIX-N.ppm * With --store the app's states live in that tree store: the app continues * where it was, whichever surface runs it. With --watch (tui, web) a module * written again replaces the running one and states keep their values by @@ -499,12 +502,14 @@ static VegaDisplay *display; /* A frame on the display; with writeback, what came back is set against it * and written (differing: how many pixels are not what was drawn). */ -static bool show_frame(const VegaGrid *grid, char path[512], unsigned long *differing) { +static bool show_frame(const VegaGrid *grid, char path[512], unsigned long *differing, bool compose) { unsigned width, height; size_t stride, captured_stride; uint32_t *out = vega_display_buffer(display, &width, &height, &stride); - vega_pixels_clear(out, stride, width, height); - vega_pixels(grid, &face, out, stride); + if (compose) { + vega_pixels_clear(out, stride, width, height); + vega_pixels(grid, &face, out, stride); + } if (!vega_display_show(display)) return false; const uint32_t *back = vega_display_captured(display, &captured_stride); *differing = 0; @@ -532,6 +537,85 @@ static bool show_frame(const VegaGrid *grid, char path[512], unsigned long *diff } +/* ---- the display, live ---------------------------------------------- */ + +static volatile sig_atomic_t stopping; +static void on_stop(int signal) { (void)signal; stopping = 1; } + +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; + for (int i = 0; i < face.cell_width; ++i) + for (int j = 0; j < face.cell_height; ++j) + if ((!i || !j || i == face.cell_width - 1 || j == face.cell_height - 1) && left + i < (int)width && top + j < (int)height) + out[(size_t)(top + j) * stride + (size_t)(left + i)] = 0xf5e0dc; +} + +/* Draw, show, then wait for input or the next tick, until a signal (or the + * end of recorded events: the last frame is then printed as text). */ +static int live(VegaInput *input, unsigned columns, unsigned rows, bool recorded) { + VegaGrid grid = {0}; + if (!vega_grid(&grid, columns, rows)) return 2; + drawn = &grid; + signal(SIGINT, on_stop); + signal(SIGTERM, on_stop); + VegaTimers timers; + vega_timers_start(&timers, app.module, vega_now()); + char path[512] = ""; + unsigned long differing = 0; + bool open_input = true; + int status = 0; + while (!stopping) { + bool ticked = false; + long due = vega_timers_run(&timers, &app, vega_now(), &ticked); + vega_draw(&grid, &app, &screen, entry, entry_length, typing); + unsigned width, height; + size_t stride; + uint32_t *out = vega_display_buffer(display, &width, &height, &stride); + vega_pixels_clear(out, stride, width, height); + vega_pixels(&grid, &face, out, stride); + int x, y; + bool pointer; + vega_input_pointer(input, &x, &y, &pointer); + 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; } + if (!open_input) break; + struct pollfd waits[32]; + unsigned count = vega_input_fds(input, waits, 32); + int timeout = screen.ticks ? 16 : -1; + if (due >= 0 && (timeout < 0 || due < timeout)) timeout = (int)due; + if (poll(waits, count, timeout) <= 0) { + app.now = vega_now(); + if (screen.ticks) pure_app_frame(&app); + continue; + } + app.now = vega_now(); + for (unsigned i = 0; i < count; ++i) { + if (!(waits[i].revents & (POLLIN | POLLHUP | POLLERR))) continue; + VegaEvent events[64]; + bool ended; + unsigned n = vega_input_read(input, waits[i].fd, events, 64, &ended); + for (unsigned k = 0; k < n; ++k) { + const VegaEvent *e = &events[k]; + unsigned line = e->y >= 0 ? (unsigned)(e->y / face.cell_height) : 0; + if (e->kind == VEGA_EVENT_TEXT && typing) handle_text(e->text, strlen(e->text)); + else if (e->kind == VEGA_EVENT_TEXT || e->kind == VEGA_EVENT_KEY) handle_key(e->name); + else if (e->kind == VEGA_EVENT_PRESS) mouse(0, line); + else if (e->kind == VEGA_EVENT_WHEEL) mouse(e->steps < 0 ? 64 : 65, line); + } + if (ended && recorded) open_input = false; /* draw once more, then stop */ + } + } + if (recorded) { + dump(&grid); + if (path[0]) printf("frame %s differing %lu grid %ux%u cell %dx%d\n", path, differing, grid.width, grid.height, face.cell_width, + face.cell_height); + } + drawn = NULL; + vega_grid_free(&grid); + return status; +} + /* painting: show writes the terminal bytes of the frame, not the grid. */ static int script(Surface surface, unsigned width, unsigned height, bool painting) { static char line[1 << 16]; @@ -619,8 +703,9 @@ static int script(Surface surface, unsigned width, unsigned height, bool paintin if (!write_frame(&grid, path)) { fprintf(stderr, "vega: cannot write %s\n", path); return 1; } printf("frame %s %dx%d\n", path, (int)grid.width * face.cell_width, (int)grid.height * face.cell_height); } else if (surface == SURFACE_DISPLAY) { - if (!show_frame(&grid, path, &differing)) { fputs("vega: the display did not take the frame\n", stderr); return 1; } - if (path[0]) printf("frame %s differing %lu grid %ux%u\n", path, differing, grid.width, grid.height); + if (!show_frame(&grid, path, &differing, true)) { fputs("vega: the display did not take the frame\n", stderr); return 1; } + if (path[0]) printf("frame %s differing %lu grid %ux%u cell %dx%d\n", path, differing, grid.width, grid.height, face.cell_width, + face.cell_height); } } counted = vega_evaluations(); @@ -665,7 +750,8 @@ int main(int argc, char **argv) { unsigned width = 0, height = 0, port = 8080; const char *symbols_path = NULL, *card = "/dev/dri/card1"; float size = 16; - bool writeback = false; + bool writeback = false, live_input = false; + const char *recorded = NULL; bool dumped = false, painting = false, watch = false, usage = argc < 3; const char *store = NULL, *listen_on = "127.0.0.1"; for (int i = 3; !usage && i < argc; i += 2) { @@ -674,6 +760,11 @@ int main(int argc, char **argv) { --i; /* a flag without a value */ continue; } + if (!strcmp(argv[i], "--live") && !strcmp(argv[1], "display")) { + live_input = 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]; @@ -687,6 +778,7 @@ int main(int argc, char **argv) { sscanf(argv[i + 1], "%ux%u", &width, &height) == 2 && width && height) writeback = true; else if (!strcmp(argv[i], "--card") && !strcmp(argv[1], "display")) card = argv[i + 1]; + else if (!strcmp(argv[i], "--input") && !strcmp(argv[1], "display")) recorded = argv[i + 1]; else if (!strcmp(argv[i], "--symbols") && glyphed(argv[1])) symbols_path = argv[i + 1]; else if (!strcmp(argv[i], "--out") && glyphed(argv[1])) frames_prefix = argv[i + 1]; else if (!strcmp(argv[i], "--size") && glyphed(argv[1]) && sscanf(argv[i + 1], "%f", &size) == 1) {} @@ -700,7 +792,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] < script\n" + " [--writeback WIDTHxHEIGHT --out PREFIX] [--live | --input EVENTS] [< script]\n" "vega check MODULE.pmod\n", stderr); return 2; } @@ -730,7 +822,18 @@ int main(int argc, char **argv) { watched = argv[2]; watched_digest = module_digest; /* of what load read */ } - int status = surface == SURFACE_WEB ? vega_web(&app, &screen, listen_on, port, watched ? module_changed : NULL) : + int status; + if (on_display && (live_input || recorded)) { + unsigned pixels_across, pixels_down; + size_t stride; + vega_display_buffer(display, &pixels_across, &pixels_down, &stride); + const char *paths[1] = {recorded}; + VegaInput *input = vega_input_open(paths, recorded ? 1 : 0, pixels_across, pixels_down); + if (!input) { fprintf(stderr, "vega: cannot read %s\n", recorded ? recorded : "input devices"); return 2; } + status = live(input, width, height, recorded != NULL); + vega_input_close(input); + } else + status = surface == SURFACE_WEB ? vega_web(&app, &screen, listen_on, port, watched ? module_changed : NULL) : surface == SURFACE_TUI && !dumped ? interactive() : script(surface, width, height, painting); /* Frame changes are stored with events; keep the last ones too. */ if (pure_app_save(&app) != PURE_OK) { fputs("vega: cannot save\n", stderr); status = status ? status : 1; } diff --git a/surfaces/vega.h b/surfaces/vega.h index 69eba0b..ba7b61f 100644 --- a/surfaces/vega.h +++ b/surfaces/vega.h @@ -2,6 +2,7 @@ #define VEGA_H #include "module.h" #include "symbols.h" +#include <poll.h> #include <stdio.h> /* Vega reads a verified Pure module by convention (docs/design/001-first-component.md): @@ -151,4 +152,23 @@ bool vega_display_show(VegaDisplay *); /* Writeback: the last frame as the display controller put it out. */ const uint32_t *vega_display_captured(const VegaDisplay *, size_t *stride); void vega_display_close(VegaDisplay *); + +/* Input from evdev: keyboards (key names and the text they type on a US + * layout), mice and touch screens (presses at a pixel of a width x height + * screen). With paths, recorded events are read from those files instead, + * positions in screen pixels. */ +typedef enum { VEGA_EVENT_KEY, VEGA_EVENT_TEXT, VEGA_EVENT_PRESS, VEGA_EVENT_WHEEL } VegaEventKind; +typedef struct { + VegaEventKind kind; + char name[24], text[8]; /* a key's name; the text it types */ + int x, y, steps; /* where a press or the wheel is; steps down (negative up) */ +} VegaEvent; +typedef struct VegaInput VegaInput; +VegaInput *vega_input_open(const char *const *paths, unsigned count, unsigned width, unsigned height); +unsigned vega_input_fds(const VegaInput *, struct pollfd *out, unsigned max); +/* What the device on fd has for us; ended: it has no more (a file read out). */ +unsigned vega_input_read(VegaInput *, int fd, VegaEvent *out, unsigned max, bool *ended); +/* Where the pointer is, and whether a mouse moved it (a finger's is not shown). */ +void vega_input_pointer(const VegaInput *, int *x, int *y, bool *shown); +void vega_input_close(VegaInput *); #endif diff --git a/tests/check.py b/tests/check.py index 119328a..0e5c3bb 100644 --- a/tests/check.py +++ b/tests/check.py @@ -35,7 +35,9 @@ inked, so no glyph spills into its neighbour. 12. Display: the same frames through KMS on a writeback connector (when the device has one) come back from the display controller exactly as drawn, - and are the pixel surface's frames for the same grid. + and are the pixel surface's frames for the same grid. Live, fed recorded + evdev events (typing with Shift, Enter, Tab, Space, a touch on a row), it + ends on the TUI's frame after the same keys and click. 10. Symbols: SVG icons (arcs, compact flags, holes, no viewBox) become outlines of the right area, an icon asking for what symbols cannot hold is refused by name, damaged files are refused whole, and (when DejaVu Sans is @@ -1110,6 +1112,54 @@ def display(vega, module, work): return {'frames': len(frames), 'pixels': compared} +def live_display(vega, module, work): + """The display live, fed recorded evdev events (a keyboard typing two items, + Tab, Space, then a touch on the first item's row): its last frame is the + TUI's after the same keys and click, and it came back from the display + controller as drawn.""" + import struct + card, face = Path('/dev/dri/card1'), work / 'ui.vsym' + if not card.exists() or not face.exists(): + return None + display_args = ['--symbols', face, '--size', 18, '--card', card, '--writeback', '640x480'] + first = run(vega, 'display', module, *display_args, '--out', work / 'first', stdin='show\n') + if first.returncode == 2 and ('writeback' in first.stderr or 'cannot open' in first.stderr): + return None + lines = first.stdout.splitlines() + frame = next(line.split() for line in lines if line.startswith('frame ')) + grid, cell_height = frame[5], int(frame[7].split('x')[1]) + heading = next(i for i, line in enumerate(lines) if line[1:].startswith('tasks_list')) + codes = {'m': 50, 'i': 23, 'l': 38, 'k': 37, 'b': 48, 'r': 19, 'e': 18, 'a': 30, 'd': 32, + 'enter': 28, 'tab': 15, 'space': 57, 'shift': 42} + events = [] # (type, code, value): EV_KEY 1, EV_ABS 3, EV_SYN 0 + def key(name, value=None): + for v in ((1, 0) if value is None else (value,)): + events.extend([(1, codes[name], v), (0, 0, 0)]) + for letter in 'milk': + key(letter) + key('enter') + key('shift', 1), key('b'), key('shift', 0) + for letter in 'read': + key(letter) + key('enter'), key('tab'), key('space') + # a touch on the first item's row (the line after the list's heading), in screen pixels + events.extend([(3, 0, 20), (3, 1, (heading + 1) * cell_height + cell_height // 2), (1, 330, 1), (0, 0, 0), + (1, 330, 0), (0, 0, 0)]) + recording = work / 'events.bin' + recording.write_bytes(b''.join(struct.pack('llHHi', 0, 0, *event) for event in events)) + live = run(vega, 'display', module, *display_args, '--out', work / 'live', '--input', recording) + assert live.returncode == 0, live.stderr + last = [line.split() for line in live.stdout.splitlines() if line.startswith('frame ')][-1] + assert last[3] == '0', last # came back as drawn + script = f'type {"milk".encode().hex()}\nenter\ntype {"Bread".encode().hex()}\nenter\ntab\nkey Space\n' \ + f'click {heading + 2} 3\nshow\n' + tui = run(vega, 'tui', module, '--dump', grid, stdin=script) + expected = [line for line in tui.stdout.splitlines() if not line.startswith('cells ')] + got = [line for line in live.stdout.splitlines() if not line.startswith('frame ')] + assert got == expected, (got, expected) + return {'events': len(events), 'lines': len(got)} + + def width_of(character): return width(character) or 0 @@ -1154,6 +1204,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['live_display'] = live_display(vega, modules['todo'], work) print(json.dumps(report)) |
