aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile15
-rw-r--r--README.md12
-rw-r--r--surfaces/input.c1
-rw-r--r--surfaces/main.c125
-rw-r--r--surfaces/vega.h4
-rw-r--r--tests/check.py84
6 files changed, 222 insertions, 19 deletions
diff --git a/Makefile b/Makefile
index fe05415..5506e78 100644
--- a/Makefile
+++ b/Makefile
@@ -15,7 +15,7 @@ SURFACES = surfaces/screen.c surfaces/draw.c surfaces/width.c surfaces/web.c sur
surfaces/input.c surfaces/main.c
.PHONY: all check FORCE
-all: $(BUILD)/vega $(BUILD)/vega-symbols
+all: $(BUILD)/vega $(BUILD)/vega-symbols $(BUILD)/vega-surface.so
$(BUILD):
mkdir -p $@
@@ -24,7 +24,16 @@ $(LIBPURE): FORCE
$(MAKE) -C $(PURE) build/libpure.a
$(BUILD)/vega: $(SURFACES) surfaces/vega.h symbols/symbols.c symbols/symbols.h $(LIBPURE) | $(BUILD)
- $(CC) $(CPPFLAGS) $(CFLAGS) -I$(PURE)/core -Isymbols $(SURFACES) symbols/symbols.c $(LIBPURE) $(LDFLAGS) -lm $(LDLIBS) -o $@
+ $(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
+# serve/surface.h): the node's display. It calls the app model pure-serve
+# exports, so it links no libpure.a of its own; it exports pure_surface_run
+# alone, and what only the vega command uses is left out.
+$(BUILD)/vega-surface.so: $(SURFACES) surfaces/vega.h symbols/symbols.c symbols/symbols.h | $(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 $@
# Building symbol files reads fonts through FreeType; reading them does not.
FREETYPE_CFLAGS ?= $(shell pkg-config --cflags freetype2)
@@ -36,7 +45,7 @@ $(BUILD)/vega-symbols: symbols/symbols-tool.c $(SYMBOLS) symbols/symbols.h symbo
$(CC) $(CPPFLAGS) $(CFLAGS) $(FREETYPE_CFLAGS) symbols/symbols-tool.c $(SYMBOLS) $(LDFLAGS) \
$(FREETYPE_LIBS) $(GL_LIBS) -lm $(LDLIBS) -o $@
-check: $(BUILD)/vega $(BUILD)/vega-symbols
+check: $(BUILD)/vega $(BUILD)/vega-symbols $(BUILD)/vega-surface.so
$(MAKE) -C $(AUP) build/aup
$(MAKE) -C $(PURE) build/pure-serve
python3 tests/check.py --vega $(BUILD)/vega --symbols $(BUILD)/vega-symbols --aup $(AUP)/build/aup --serve $(PURE)/build/pure-serve
diff --git a/README.md b/README.md
index 8bd69b4..3b2ed3c 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,15 @@ curl -s -X POST -H 'Content-Type: application/json' -d '{"title": "ship it", "la
curl -s 'localhost:8767/views/cards_list?from=0&count=20'
```
-한 저장소는 한 번에 한 호스트가 연다(상태의 소유자는 하나다).
+한 저장소는 한 번에 한 호스트가 연다(상태의 소유자는 하나다). 사람과 프로그램이 동시에 쓰려면 Vega를 노드 안에서
+돌린다. `pure-serve`가 `build/vega-surface.so`를 매핑해 제 스레드에서 돌린다([Pure 설계 010](/srv/git/pure.git):
+`docs/design/010-surfaces.md`). 디스플레이는 노드의 마지막 저장을 제자리에서 읽어 그리고, 사람이 치는 키와 글자는 HTTP
+요청과 같은 줄로 노드의 소유자에게 간다. 그 사이에 HTTP나 JSON이 없다.
+
+```sh
+../pure/build/pure-serve --module board.pmod --store ~/.board \
+ --surface build/vega-surface.so -- display --symbols ui.vsym --live # 디스플레이와 HTTP가 한 노드를 쓴다
+```
글자와 아이콘은 하나의 바이너리 형식(심볼 파일)으로 그린다. 폰트와 SVG는 빌드할 때 한 번만 해석한다
([설계 002](docs/design/002-symbol-format.md)).
@@ -50,7 +58,7 @@ build/vega-symbols render ui.vsym 0xE000 48 icon.pgm
| 디렉터리 | 무엇 | 만드는 것 |
|---|---|---|
| `lib/` | aup 라이브러리(`use vega`): 목록·표·포커스·톤 같은 인터페이스 어휘 | aup가 읽는다 |
-| `surfaces/` | 모듈을 사람에게 보이는 표면: TUI, CLI, 문장(NUI), 웹, 픽셀, 디스플레이(KMS). Pure의 `libpure.a`를 링크한다 | `build/vega` |
+| `surfaces/` | 모듈을 사람에게 보이는 표면: TUI, CLI, 문장(NUI), 웹, 픽셀, 디스플레이(KMS). Pure의 `libpure.a`를 링크한다. 같은 호스트가 Pure 노드 안의 디스플레이도 된다(`pure-serve --surface`, 노드가 내보내는 앱 모델을 부른다) | `build/vega`, `build/vega-surface.so` |
| `symbols/` | 글리프와 아이콘을 한 형식으로, CPU와 GPU로 그리기([설계 002](docs/design/002-symbol-format.md)) | `build/vega-symbols` |
Pure와 같은 규칙이다([Pure 009](/srv/git/pure.git): `docs/design/009-layout.md`). 최상위 디렉터리가 역할별 부품이고,
diff --git a/surfaces/input.c b/surfaces/input.c
index b358d47..9403a75 100644
--- a/surfaces/input.c
+++ b/surfaces/input.c
@@ -84,6 +84,7 @@ VegaInput *vega_input_open(const char *const *paths, unsigned count, unsigned wi
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 (!paths) return input; /* none */
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);
diff --git a/surfaces/main.c b/surfaces/main.c
index 26aa4eb..a7488ab 100644
--- a/surfaces/main.c
+++ b/surfaces/main.c
@@ -2,6 +2,7 @@
#include "vega.h"
#include "module-store.h"
#include "store.h"
+#include "surface.h"
#include <poll.h>
#include <signal.h>
#include <stdlib.h>
@@ -42,11 +43,19 @@
* (0 first) of a list or table; click is a mouse click on the last shown TUI
* frame (1 first, as terminals report it); time sets the time the next
* events see (live surfaces use the clock) and tick ticks a timer (live
- * surfaces tick them on time); swap replaces the module as --watch does. */
+ * surfaces tick them on time); swap replaces the module as --watch does.
+ *
+ * Built with VEGA_SURFACE the same host is a library a Pure node maps
+ * (pure-serve --surface vega-surface.so -- display ARGUMENTS, Pure's
+ * serve/surface.h): the display shows the node's app as of each save, read
+ * where it is, and what people type goes to the node's owner. */
static PureModule modules[2]; /* the running one (app.module) and the next */
static unsigned current;
static PureApp app;
+/* In a node: the node, whose owner applies the events; app then shows its
+ * last save (a reader) and applies none itself. */
+static const PureSurface *node;
static VegaScreen screen;
static VegaGrid *drawn; /* the TUI's last frame, which a click lands on */
static char entry[VEGA_TEXT];
@@ -153,10 +162,16 @@ static int key_input(const char *key) { return vega_key_input(app.module, key);
/* A key press and release; false when the app has no such key or the press failed. */
static bool press(const char *key, int *input) {
*input = key_input(key);
+ if (*input >= 0 && node) return node->press(node->host, (uint32_t)*input) == PURE_OK;
return *input >= 0 && vega_press(&app, key);
}
-static bool commit(const char *text, size_t length) { return vega_commit(&app, &screen, text, length); }
+static bool commit(const char *text, size_t length) {
+ if (node)
+ return screen.prompt >= 0 &&
+ node->type(node->host, (uint32_t)screen.prompt, (PureSpan){(const unsigned char *)text, length}) == PURE_OK;
+ return vega_commit(&app, &screen, text, length);
+}
static const VegaPart *part_named(const char *name) {
for (unsigned i = 0; i < screen.count; ++i)
@@ -179,7 +194,7 @@ static bool mouse(unsigned button, unsigned line) {
item = button == 64 ? focus - 1 : focus + 1;
}
} else if (button) return false;
- return vega_point(&app, part, item);
+ return !node && vega_point(&app, part, item); /* a node takes keys and text, not pointing yet */
}
static int hex(char c) { return c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1; }
@@ -552,23 +567,29 @@ static void show_pointer(uint32_t *out, size_t stride, int x, int y, unsigned wi
}
/* 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). */
+ * end of recorded events: the last frame is then printed as text). In a
+ * node: draw its last save, wait for input or its next save, until it stops;
+ * its owner runs the timers, and the signals are its own. */
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);
+ if (!node) {
+ 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) {
+ while (!stopping && !(node && node->stopping(node->host))) {
bool ticked = false;
- long due = vega_timers_run(&timers, &app, vega_now(), &ticked);
- vega_draw(&grid, &app, &screen, entry, entry_length, typing);
+ long due = node ? -1 : vega_timers_run(&timers, &app, vega_now(), &ticked);
+ PureSnapshot *snapshot = node ? node->read(node->host, &app) : NULL;
+ if (!node || snapshot) vega_draw(&grid, &app, &screen, entry, entry_length, typing);
+ if (snapshot) node->release(node->host, snapshot);
unsigned width, height;
size_t stride;
uint32_t *out = vega_display_buffer(display, &width, &height, &stride);
@@ -580,18 +601,25 @@ 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; }
if (!open_input) break;
- struct pollfd waits[32];
+ struct pollfd waits[33];
unsigned count = vega_input_fds(input, waits, 32);
- int timeout = screen.ticks ? 16 : -1;
+ if (node) waits[count++] = (struct pollfd){node->changes, POLLIN, 0}; /* the node saved: draw again */
+ 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);
continue;
}
- app.now = vega_now();
+ if (!node) app.now = vega_now();
for (unsigned i = 0; i < count; ++i) {
if (!(waits[i].revents & (POLLIN | POLLHUP | POLLERR))) continue;
+ if (node && waits[i].fd == node->changes) {
+ uint64_t saves;
+ if (read(node->changes, &saves, sizeof(saves)) < 0) { /* cleared already */ }
+ continue;
+ }
VegaEvent events[64];
bool ended;
unsigned n = vega_input_read(input, waits[i].fd, events, 64, &ended);
@@ -610,6 +638,7 @@ static int live(VegaInput *input, unsigned columns, unsigned rows, bool 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);
+ fflush(stdout);
}
drawn = NULL;
vega_grid_free(&grid);
@@ -746,6 +775,77 @@ static bool open_face(const char *path, float size) {
return ok; /* the bytes stay: the face reads them */
}
+#ifdef VEGA_SURFACE
+/* Vega in a Pure node (serve/surface.h): pure-serve ... --surface
+ * vega-surface.so -- display --symbols FONT.vsym [--size PX] [--card PATH]
+ * [--writeback WIDTHxHEIGHT --out PREFIX] [--live | --input EVENTS]. The
+ * display shows the node's app as of each save; keys and text go to its
+ * owner. Recorded events are read to their end, then the last frame is
+ * printed as text (on the node's standard output); live, it runs until the
+ * node stops. */
+__attribute__((visibility("default"))) int pure_surface_run(const PureSurface *surface, int argc, char **argv) {
+ if (surface->version != PURE_SURFACE_VERSION || surface->app_size != sizeof(PureApp) ||
+ surface->module_size != sizeof(PureModule)) {
+ fputs("vega: this surface was built for another Pure\n", stderr);
+ return 2;
+ }
+ const char *symbols_path = NULL, *card = "/dev/dri/card1", *recorded = NULL;
+ unsigned width = 0, height = 0;
+ float size = 16;
+ bool writeback = false, live_input = false, usage = argc < 2 || strcmp(argv[1], "display");
+ for (int i = 2; !usage && i < argc; i += 2) {
+ if (!strcmp(argv[i], "--live")) {
+ live_input = true;
+ --i;
+ continue;
+ }
+ if (i + 1 >= argc) usage = true;
+ else if (!strcmp(argv[i], "--symbols")) symbols_path = argv[i + 1];
+ else if (!strcmp(argv[i], "--size") && sscanf(argv[i + 1], "%f", &size) == 1) {}
+ else if (!strcmp(argv[i], "--card")) card = argv[i + 1];
+ else if (!strcmp(argv[i], "--input")) recorded = argv[i + 1];
+ else if (!strcmp(argv[i], "--out")) frames_prefix = argv[i + 1];
+ else if (!strcmp(argv[i], "--writeback") && sscanf(argv[i + 1], "%ux%u", &width, &height) == 2 && width && height)
+ writeback = true;
+ else usage = true;
+ }
+ if (usage || !symbols_path || (writeback && !frames_prefix) || (live_input && recorded)) {
+ fputs("vega-surface.so display --symbols FONT.vsym [--size PX] [--card PATH] [--writeback WIDTHxHEIGHT --out PREFIX]\n"
+ " [--live | --input EVENTS]\n", stderr);
+ return 2;
+ }
+ if (!open_face(symbols_path, size)) return 2;
+ char error[256];
+ if (!(display = vega_display_open(card, writeback, writeback ? width : 0, writeback ? height : 0, error, sizeof(error)))) {
+ fprintf(stderr, "vega: %s\n", error);
+ return 2;
+ }
+ unsigned pixels_across, pixels_down;
+ size_t stride;
+ vega_display_buffer(display, &pixels_across, &pixels_down, &stride);
+ node = surface;
+ vega_screen(&screen, surface->module);
+ typing = screen.prompt >= 0;
+ const char *paths[1] = {recorded};
+ /* Without --live or --input, no devices: the node's saves alone draw it. */
+ VegaInput *input = vega_input_open(live_input || recorded ? paths : NULL, recorded ? 1 : 0, pixels_across, pixels_down);
+ int status = 2;
+ if (!input) fprintf(stderr, "vega: cannot read %s\n", recorded ? recorded : "input devices");
+ else {
+ PureSnapshot *first = node->read(node->host, &app); /* app shows the node's states from here on */
+ if (first) node->release(node->host, first);
+ status = first ? live(input, pixels_across / (unsigned)face.cell_width, pixels_down / (unsigned)face.cell_height,
+ recorded != NULL)
+ : 2;
+ vega_input_close(input);
+ }
+ pure_app_stop(&app);
+ vega_display_close(display);
+ display = NULL;
+ node = NULL;
+ return status;
+}
+#else
int main(int argc, char **argv) {
unsigned width = 0, height = 0, port = 8080;
const char *symbols_path = NULL, *card = "/dev/dri/card1";
@@ -841,3 +941,4 @@ int main(int argc, char **argv) {
vega_display_close(display);
return status;
}
+#endif
diff --git a/surfaces/vega.h b/surfaces/vega.h
index ba7b61f..fd8780b 100644
--- a/surfaces/vega.h
+++ b/surfaces/vega.h
@@ -155,8 +155,8 @@ 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. */
+ * screen). With count paths, recorded events are read from those files
+ * instead, positions in screen pixels; with paths NULL, there is none. */
typedef enum { VEGA_EVENT_KEY, VEGA_EVENT_TEXT, VEGA_EVENT_PRESS, VEGA_EVENT_WHEEL } VegaEventKind;
typedef struct {
VegaEventKind kind;
diff --git a/tests/check.py b/tests/check.py
index 0e5c3bb..c0633c3 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -1160,6 +1160,89 @@ def live_display(vega, module, work):
return {'events': len(events), 'lines': len(got)}
+def node_display(vega, serve, module, work):
+ """Vega in a Pure node (vega-surface.so run by pure-serve --surface): the
+ node's app, which an HTTP client gave an item, is on its display, and
+ recorded keys typed there (an item, Enter, Tab, Space) go to the node's
+ owner. The last frame is the one the vega command draws for the same
+ state and keys, it came back from the display controller as drawn, and
+ the node's HTTP views show what was typed."""
+ import http.client
+ import select
+ import shutil
+ import signal
+ import struct
+ import time
+ card, face, library = Path('/dev/dri/card1'), work / 'ui.vsym', vega.parent / 'vega-surface.so'
+ if not card.exists() or not face.exists() or not library.exists():
+ return None
+ store = work / 'node-store'
+
+ def start(*extra):
+ """The node, its port, and what it printed after the line that tells it."""
+ process = subprocess.Popen([str(a) for a in (serve, '--module', module, '--store', store, '--port', 0, *extra)],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ printed, deadline = '', time.time() + 10
+ while '\n' not in printed and time.time() < deadline:
+ if select.select([process.stdout], [], [], 1)[0]:
+ printed += os.read(process.stdout.fileno(), 65536).decode()
+ assert '\n' in printed, 'node did not start'
+ first, rest = printed.split('\n', 1)
+ return process, json.loads(first)['port'], rest
+
+ def post(port, path, body):
+ connection = http.client.HTTPConnection('127.0.0.1', port, timeout=10)
+ connection.request('POST', path, body)
+ status = connection.getresponse().status
+ connection.close()
+ return status
+
+ node, port, _ = start()
+ try:
+ assert post(port, '/inputs/add', b'eggs') == 200
+ finally:
+ node.send_signal(signal.SIGTERM)
+ node.communicate(timeout=30)
+ before = work / 'node-store-before'
+ shutil.copytree(store, before)
+ codes = {'m': 50, 'i': 23, 'l': 38, 'k': 37, 'enter': 28, 'tab': 15, 'space': 57}
+ events = [] # (type, code, value): EV_KEY 1, EV_SYN 0
+ for name in ('m', 'i', 'l', 'k', 'enter', 'tab', 'space'):
+ events.extend([(1, codes[name], 1), (0, 0, 0), (1, codes[name], 0), (0, 0, 0)])
+ recording = work / 'node-events.bin'
+ recording.write_bytes(b''.join(struct.pack('llHHi', 0, 0, *event) for event in events))
+ node, port, printed = start('--surface', library, '--', 'display', '--symbols', face, '--size', 18, '--card', card,
+ '--writeback', '640x480', '--out', work / 'node', '--input', recording)
+ lines = []
+ try:
+ deadline = time.time() + 30
+ while '\nframe ' not in '\n' + printed and time.time() < deadline:
+ ready, _, _ = select.select([node.stdout], [], [], 1)
+ if ready:
+ printed += os.read(node.stdout.fileno(), 65536).decode()
+ lines = printed.splitlines()
+ assert any(line.startswith('frame ') for line in lines), lines
+ left = http.client.HTTPConnection('127.0.0.1', port, timeout=10)
+ left.request('GET', '/views/left')
+ shown = left.getresponse().read().decode()
+ left.close()
+ finally:
+ node.send_signal(signal.SIGTERM)
+ _, error = node.communicate(timeout=30)
+ if node.returncode == 1 and b'writeback' in error:
+ return None
+ assert node.returncode == 0 and b'"returned":0' in error, error[-800:]
+ assert shown == '1 of 2', shown # eggs to do, milk done
+ frame = next(line.split() for line in lines if line.startswith('frame '))
+ assert frame[3] == '0', frame # came back as drawn
+ script = f'type {"milk".encode().hex()}\nenter\ntab\nkey Space\nshow\n'
+ tui = run(vega, 'tui', module, '--store', before, '--dump', frame[5], stdin=script)
+ expected = [line for line in tui.stdout.splitlines() if not line.startswith('cells ')]
+ got = [line for line in lines 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
@@ -1205,6 +1288,7 @@ def main():
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)
+ report['node_display'] = node_display(vega, args.serve.resolve(), modules['todo'], work)
print(json.dumps(report))