aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-27 19:37:33 +0100
committereunhhu <[email protected]>2026-09-27 19:37:33 +0100
commit5fe8104744d3e0a48674b1368db6fd104a10bdd4 (patch)
treeed9ea83c6a84a09136f40dc7ff6141968e75faec
parentad3eb77af098f34c1ea8cb50f468a8b75fb7d61c (diff)
downloadvega-5fe8104744d3e0a48674b1368db6fd104a10bdd4.tar.gz
vega-5fe8104744d3e0a48674b1368db6fd104a10bdd4.zip
Show a Pure node's app on its display, as a surface in the node
The vega host builds a second way, as build/vega-surface.so, which pure-serve maps and runs beside its connection threads (pure-serve --surface vega-surface.so -- display ..., Pure's design 010). There the app it draws is a reader of the node's last save, read where it is, and what people type goes to the node's owner: a key press as the key input's event, the typed line as the text input's, each applied and saved with the node's requests. It waits on its input devices and the node's changes descriptor together, leaves timers and signals to the node, and stops with it. The library calls the app model pure-serve exports and links none of its own; it exports pure_surface_run alone. Without --live or --input it takes no input: the node's saves alone draw it. The check gives a node an item over HTTP, then runs the display in the node with recorded keys (an item, Enter, Tab, Space): the last frame is the one vega tui draws for the same state and keys, it came back from the display controller as drawn, and the node's views show both items.
-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))