aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--docs/design/001-first-component.md24
-rw-r--r--src/main.c133
-rw-r--r--src/screen.c1
-rw-r--r--src/vega.h6
-rw-r--r--src/web.c9
-rw-r--r--tests/__pycache__/check.cpython-313.pycbin0 -> 65599 bytes
-rw-r--r--tests/check.py90
8 files changed, 222 insertions, 43 deletions
diff --git a/README.md b/README.md
index ce490dd..6c89767 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,8 @@ build/vega tui todo.pmod --store ~/.todo # 터미널. 글자 입력 후 Ent
# --store가 있으면 목록이 다음 실행에도 남는다(어느 표면이든)
printf 'text 6d696c6b\nkey Space\nshow\n' | build/vega cli todo.pmod
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)
make check # 참조 모델 대조, 프레임 검사, 대화형 pty 검사, 견고성
```
diff --git a/docs/design/001-first-component.md b/docs/design/001-first-component.md
index 5d1adce..0a747fc 100644
--- a/docs/design/001-first-component.md
+++ b/docs/design/001-first-component.md
@@ -93,6 +93,28 @@ TUI는 매 변경마다 격자를 앱에서 다시 그리지만(보이는 칸만
무작위 입력 4,800 프레임(40×12, 24×9, 80×30)에서 모두 같았다. 한 프레임이 보내는 바이트는 평균 1,220 B(전체)에서
143 B(변경분)로 줄었다. SSH처럼 느린 터미널에서 입력 한 번의 화면 갱신이 그만큼 가볍다.
+## 고쳐 쓰는 동안 (`--watch`)
+
+```sh
+AUP_PATH=lib aup watch app.aup -o app.pmod & # 저장할 때마다 다시 빌드
+vega tui app.pmod --watch --store ~/.app # 또는 vega web app.pmod --watch
+```
+
+`--watch`(tui, web)는 모듈 파일이 다시 쓰이면 실행 중인 앱의 모듈을 새것으로 바꾼다. 상태는 Pure의
+`pure_app_replace`가 이름으로 이어받는다. 목록의 항목은 필드 이름으로, 포커스와 완료 표시는 그대로 남고, 새 상태는
+초기값으로 시작한다. 화면(제목·필드·목록·입력줄)은 새 모듈에서 다시 읽고, 웹은 열린 모든 페이지에 알린다.
+스크립트에서는 `swap MODULE`이 같은 일을 한다.
+
+- **바이트로 본다.** 파일 시각이 아니라 읽은 바이트의 digest를 비교한다. 1초 안에 같은 크기로 다시 쓴 모듈도
+ 놓치지 않는다. 확인은 초당 다섯 번까지이고, 모듈 하나(수 KB)를 읽는 비용이다.
+- **로드되지 않는 모듈은 거절한다.** 검증을 통과하지 못하거나 바꿀 수 없는 모듈이면 실행 중인 앱을 그대로 두고
+ stderr에 이유를 한 번 쓴다. 같은 바이트는 다시 시도하지 않는다. aup는 모듈을 임시 파일에 쓰고 rename하므로 반쯤
+ 쓰인 모듈을 읽는 일이 없다. 다른 도구가 제자리에 쓰면 읽다 만 모듈은 검증에서 거절되고, 다 쓴 모듈이 다음
+ 확인에서 들어온다.
+- **두 칸.** 모듈은 두 칸을 번갈아 쓴다. 새 모듈을 빈 칸에 로드하고 바꾸는 데 성공해야 그 칸이 실행 칸이 된다.
+ 그래서 실패한 로드가 실행 중인 모듈을 건드리지 않는다. 로드된 모듈은 파일 바이트를 붙잡지 않으므로 읽기 버퍼는
+ 하나다.
+
## 규약 검사 (`vega check`)
`vega check app.pmod`는 모듈이 뜻한 듯하지만 규약에 맞지 않는 곳을 알린다. 표면은 이런 보기를 조용히 다른
@@ -140,6 +162,8 @@ CLI나 문장 표면이 그대로 이어받는다. Vega에도 앱에도 저장 �
같다(포커스는 그 항목을 가진 목록에만 있다). 웹 JSON은 다른 목록의 포커스를 null로 준다.
- 무작위 스크립트에 `point`(범위 밖 포함)와 TUI `click`을 섞어도 CLI·문장·프레임이 모델과 같다. 가상 터미널에서
마우스로 행을 클릭하면 포커스가 옮겨지고, 저장소를 쓰는 다음 실행이 그 포커스에서 시작한다.
+- 모듈 교체: 스크립트의 `swap`, 웹의 `--watch`(새 제목과 필드, 같은 항목, 모든 페이지에 알림), 1초 안에 같은 크기로
+ 다시 쓴 모듈, 깨진 모듈의 거절, 가상 터미널 TUI의 `--watch`(포커스와 완료 표시 유지)를 확인한다.
## 남은 것
diff --git a/src/main.c b/src/main.c
index b9ea91e..ebda312 100644
--- a/src/main.c
+++ b/src/main.c
@@ -8,6 +8,7 @@
#include <string.h>
#include <sys/ioctl.h>
#include <termios.h>
+#include <time.h>
#include <unistd.h>
/* vega SURFACE MODULE [--store PATH]: one module, one screen, several surfaces.
@@ -20,15 +21,19 @@
* web [--listen ADDRESS] [--port N]
* a web page (a GUI), on 127.0.0.1 unless another address is given
* With --store the app's states live in that tree store: the app continues
- * where it was, whichever surface runs it.
+ * 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
+ * name: aup watch app.aup -o app.pmod beside it makes a saved edit show at once.
* Script commands, one per line: key KEYNAME | text HEX|- | type HEX | enter |
- * tab | frame [N] | point VIEW ROW | click LINE COLUMN | show. key presses and
+ * tab | frame [N] | point VIEW ROW | click LINE COLUMN | swap MODULE | show. key presses and
* releases the input bound to that key; text commits text to the screen's text
* input; type, enter and tab act on the TUI's input line; point focuses a row
* (0 first) of a list or table; click is a mouse click on the last shown TUI
- * frame (1 first, as terminals report it). */
+ * frame (1 first, as terminals report it); swap replaces the module as --watch
+ * does. */
-static PureModule module;
+static PureModule modules[2]; /* the running one (app.module) and the next */
+static unsigned current;
static PureApp app;
static VegaScreen screen;
static VegaGrid *drawn; /* the TUI's last frame, which a click lands on */
@@ -36,42 +41,91 @@ static char entry[VEGA_TEXT];
static size_t entry_length;
static bool typing;
-static bool load_module(const char *path) {
- static unsigned char bytes[1 << 17];
+/* The module file last read, and a digest of its bytes. A loaded module
+ * keeps nothing of them, so one buffer serves every load. */
+static unsigned char module_bytes[1 << 17];
+static uint64_t module_digest;
+
+/* Read a module file whole into module_bytes: its size, or -1. */
+static long read_module(const char *path) {
FILE *file = fopen(path, "rb");
- size_t size = file ? fread(bytes, 1, sizeof(bytes), file) : 0;
- bool ok = file && !ferror(file) && size < sizeof(bytes);
+ size_t size = file ? fread(module_bytes, 1, sizeof(module_bytes), file) : 0;
+ bool ok = file && !ferror(file) && size < sizeof(module_bytes);
if (file) fclose(file);
- if (!ok) { fprintf(stderr, "vega: cannot read %s\n", path); return false; }
+ if (!ok) return -1;
+ uint64_t h = UINT64_C(0xcbf29ce484222325) ^ size;
+ for (size_t i = 0; i < size; ++i) h = (h ^ module_bytes[i]) * UINT64_C(0x100000001b3);
+ module_digest = h;
+ return (long)size;
+}
+
+/* Decode the module that read_module read from path. */
+static bool decode(const char *path, long size, PureModule *module) {
+ if (size < 0) { fprintf(stderr, "vega: cannot read %s\n", path); return false; }
char error[128];
- if (pure_module_load(bytes, size, &module, error, sizeof(error)) != PURE_OK) {
+ if (pure_module_load(module_bytes, (size_t)size, module, error, sizeof(error)) != PURE_OK) {
fprintf(stderr, "vega: %s: %s\n", path, error);
return false;
}
return true;
}
+static bool load_module(const char *path, PureModule *module) { return decode(path, read_module(path), module); }
+
static void print_finding(void *path, const char *name, const char *message) {
printf("%s: %s: %s\n", (const char *)path, name, message);
}
static bool load(const char *path, const char *store) {
- if (!load_module(path)) return false;
+ if (!load_module(path, &modules[current])) return false;
if (store && pure_store_init(store) != PURE_OK) { fprintf(stderr, "vega: cannot open store %s\n", store); return false; }
- PureResult started = pure_app_start(&app, &module, store ? pure_tree_data(store) : (PureData){0});
+ PureResult started = pure_app_start(&app, &modules[current], store ? pure_tree_data(store) : (PureData){0});
if (started != PURE_OK) { fprintf(stderr, "vega: cannot start (%d)\n", (int)started); return false; }
- vega_screen(&screen, &module);
+ vega_screen(&screen, &modules[current]);
typing = screen.prompt >= 0;
return true;
}
-static int key_input(const char *key) { return vega_key_input(&module, key); }
+/* Replace the running app's module with the one read from path, states
+ * keeping their values by name; a module that does not load or replace
+ * leaves the app as it was. */
+static bool swap(const char *path, long size) {
+ PureModule *next = &modules[!current];
+ if (!decode(path, size, next)) return false;
+ PureResult replaced = pure_app_replace(&app, next);
+ if (replaced != PURE_OK) {
+ fprintf(stderr, "vega: keeping the running module: cannot replace it (%d)\n", (int)replaced);
+ return false;
+ }
+ bool had_prompt = screen.prompt >= 0;
+ current = !current;
+ vega_screen(&screen, next);
+ typing = screen.prompt >= 0 && (typing || !had_prompt);
+ return true;
+}
-static bool frames(void) {
- for (uint32_t h = 0; h < module.handlers; ++h) if (module.handler[h].event == 0) return true;
- return false;
+/* --watch: the module file's bytes when last read (their digest), so a
+ * module written again is seen however soon and whatever its size or file
+ * times. Read at most five times a second, however often asked; bytes that
+ * do not load are not tried again. */
+static const char *watched;
+static uint64_t watched_digest;
+static struct timespec watched_at;
+
+static bool module_changed(void) {
+ struct timespec now;
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ if (!watched || (now.tv_sec - watched_at.tv_sec) * 1000000000L + now.tv_nsec - watched_at.tv_nsec < 200000000L)
+ return false;
+ watched_at = now;
+ long size = read_module(watched);
+ if (size < 0 || module_digest == watched_digest) return false; /* a missing file is looked for again */
+ watched_digest = module_digest;
+ return swap(watched, size);
}
+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);
@@ -82,7 +136,8 @@ static bool commit(const char *text, size_t length) { return vega_commit(&app, &
static const VegaPart *part_named(const char *name) {
for (unsigned i = 0; i < screen.count; ++i)
- if (screen.parts[i].kind != VEGA_FIELD && !strcmp(module.view_names[screen.parts[i].view], name)) return &screen.parts[i];
+ if (screen.parts[i].kind != VEGA_FIELD && !strcmp(app.module->view_names[screen.parts[i].view], name))
+ return &screen.parts[i];
return NULL;
}
@@ -350,7 +405,6 @@ static int interactive(void) {
signal(SIGWINCH, on_resize);
VegaGrid grid = {0};
drawn = &grid;
- bool ticking = frames();
for (;;) {
if (resized) {
struct winsize size;
@@ -362,8 +416,12 @@ static int interactive(void) {
vega_draw(&grid, &app, &screen, entry, entry_length, typing);
paint_terminal(&grid);
struct pollfd wait = {STDIN_FILENO, POLLIN, 0};
- int ready = poll(&wait, 1, ticking ? 16 : -1);
- if (ready == 0) { pure_app_frame(&app); continue; }
+ int ready = poll(&wait, 1, screen.ticks ? 16 : watched ? 200 : -1);
+ if (ready == 0) {
+ if (screen.ticks) pure_app_frame(&app);
+ module_changed();
+ continue;
+ }
if (ready < 0) continue; /* a signal, such as a resize */
unsigned char in[256];
ssize_t got = read(STDIN_FILENO, in, sizeof(in));
@@ -422,6 +480,9 @@ static int script(Surface surface, unsigned width, unsigned height, bool paintin
if (surface != SURFACE_TUI) { fputs("vega: click needs the tui\n", stderr); continue; }
acted = sscanf(line, "%*s %u %u", &at_line, &at_column) == 2 && at_line && shown;
ok = acted && mouse(0, at_line - 1);
+ } else if (!strcmp(command, "swap")) {
+ acted = false;
+ if (!swap(argument, read_module(argument))) fprintf(stderr, "vega: kept the running module\n");
} else if (!strcmp(command, "frame")) {
long count = *argument ? strtol(argument, NULL, 10) : 1;
for (long i = 0; i < count && i < 600; ++i) ok = pure_app_frame(&app) == PURE_OK && ok;
@@ -447,9 +508,9 @@ static int script(Surface surface, unsigned width, unsigned height, bool paintin
fprintf(stderr, "vega: unknown command %s\n", command);
continue;
}
- if (!ok && input >= 0) fprintf(stderr, "vega: %s failed\n", module.input_names[input]);
+ if (!ok && input >= 0) fprintf(stderr, "vega: %s failed\n", app.module->input_names[input]);
if (surface == SURFACE_SAY && acted) {
- if (!ok) printf("Could not %s.\n", input >= 0 ? module.input_names[input] : verb);
+ if (!ok) printf("Could not %s.\n", input >= 0 ? app.module->input_names[input] : verb);
else vega_say(stdout, &app, &screen, false);
}
fflush(stdout);
@@ -461,10 +522,16 @@ static int script(Surface surface, unsigned width, unsigned height, bool paintin
int main(int argc, char **argv) {
unsigned width = 0, height = 0, port = 8080;
- bool dumped = false, painting = false, usage = argc < 3 || argc % 2 == 0;
+ 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 + 1 < argc; i += 2) {
- if (!strcmp(argv[i], "--store")) store = argv[i + 1];
+ for (int i = 3; !usage && i < argc; i += 2) {
+ if (!strcmp(argv[i], "--watch") && (!strcmp(argv[1], "tui") || !strcmp(argv[1], "web"))) {
+ watch = true;
+ --i; /* a flag without a value */
+ 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];
else if (!strcmp(argv[i], "--port") && !strcmp(argv[1], "web") && sscanf(argv[i + 1], "%u", &port) == 1 &&
port < 65536) {}
@@ -475,20 +542,24 @@ int main(int argc, char **argv) {
}
if (usage || (dumped && strcmp(argv[1], "tui")) || (!strcmp(argv[1], "check") && argc != 3)) {
fputs("vega tui|cli|say MODULE.pmod [--store PATH]\n"
- "vega tui MODULE.pmod [--store PATH] --dump|--paint WIDTHxHEIGHT < script\n"
- "vega web MODULE.pmod [--store PATH] [--listen ADDRESS] [--port N]\n"
+ "vega tui MODULE.pmod [--store PATH] [--watch] | --dump|--paint WIDTHxHEIGHT < script\n"
+ "vega web MODULE.pmod [--store PATH] [--listen ADDRESS] [--port N] [--watch]\n"
"vega check MODULE.pmod\n", stderr);
return 2;
}
if (!strcmp(argv[1], "check")) {
- if (!load_module(argv[2])) return 2;
- return vega_check(&module, print_finding, argv[2]) ? 1 : 0;
+ if (!load_module(argv[2], &modules[0])) return 2;
+ return vega_check(&modules[0], print_finding, argv[2]) ? 1 : 0;
}
Surface surface = !strcmp(argv[1], "cli") ? SURFACE_CLI : !strcmp(argv[1], "say") ? SURFACE_SAY :
!strcmp(argv[1], "tui") ? SURFACE_TUI : !strcmp(argv[1], "web") ? SURFACE_WEB : (Surface)-1;
if ((int)surface < 0) { fprintf(stderr, "vega: unknown surface %s\n", argv[1]); return 2; }
if (!load(argv[2], store)) return 2;
- int status = surface == SURFACE_WEB ? vega_web(&app, &screen, listen_on, port) :
+ if (watch && !dumped) {
+ 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) :
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/src/screen.c b/src/screen.c
index 26b9fb0..56c2cf9 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -20,6 +20,7 @@ void vega_screen(VegaScreen *screen, const PureModule *m) {
screen->prompt = -1;
for (uint32_t i = 0; i < m->inputs && screen->prompt < 0; ++i)
if (m->input[i].kind == PURE_KIND_TEXT) screen->prompt = (int)i;
+ for (uint32_t h = 0; h < m->handlers; ++h) screen->ticks = screen->ticks || m->handler[h].event == 0;
/* A <table>_focus view belongs to its table, not to the fields. */
bool focus_view[PURE_MODULE_VIEWS] = {false};
for (uint32_t i = 0; i < m->views; ++i) {
diff --git a/src/vega.h b/src/vega.h
index 59a663e..47e7e91 100644
--- a/src/vega.h
+++ b/src/vega.h
@@ -30,6 +30,7 @@ typedef struct {
const PureModule *module;
int title; /* text view, or -1 */
int prompt; /* the first text input, or -1 */
+ bool ticks; /* it handles frames: a surface runs them, about 60 a second */
VegaPart parts[VEGA_PARTS];
unsigned count;
} VegaScreen;
@@ -82,8 +83,9 @@ void vega_print(FILE *, PureApp *, const VegaScreen *, bool color);
/* NUI: the screen as sentences (all), or only the focused item of the first list. */
void vega_say(FILE *, PureApp *, const VegaScreen *, bool all);
/* GUI: the screen as a web page on listen:port (0: any free port, printed as
- * {"port":N}); runs until SIGINT or SIGTERM. */
-int vega_web(PureApp *, const VegaScreen *, const char *listen, unsigned port);
+ * {"port":N}); runs until SIGINT or SIGTERM. reload, when given, is asked
+ * a few times a second whether the module was replaced (the screen with it). */
+int vega_web(PureApp *, const VegaScreen *, const char *listen, unsigned port, bool (*reload)(void));
/* TUI: a grid of cells, redrawn from the app on every change. first keeps each
* list's scroll between frames; the focused row stays visible. shown is what
diff --git a/src/web.c b/src/web.c
index 149e888..8202815 100644
--- a/src/web.c
+++ b/src/web.c
@@ -364,7 +364,7 @@ static void drop(Client *c) {
c->fd = -1;
}
-int vega_web(PureApp *app, const VegaScreen *screen, const char *listen_on, unsigned port) {
+int vega_web(PureApp *app, const VegaScreen *screen, const char *listen_on, unsigned port, bool (*reload)(void)) {
int listener = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0), one = 1;
struct sockaddr_in address = {.sin_family = AF_INET, .sin_port = htons((uint16_t)port)};
socklen_t size = sizeof(address);
@@ -382,8 +382,6 @@ int vega_web(PureApp *app, const VegaScreen *screen, const char *listen_on, unsi
signal(SIGPIPE, SIG_IGN);
static Client clients[CLIENTS];
for (unsigned i = 0; i < CLIENTS; ++i) clients[i].fd = -1;
- bool ticking = false;
- for (uint32_t h = 0; h < app->module->handlers; ++h) ticking = ticking || app->module->handler[h].event == 0;
while (!stopping) {
struct pollfd fds[CLIENTS + 1];
unsigned slot[CLIENTS + 1], count = 1;
@@ -394,10 +392,11 @@ int vega_web(PureApp *app, const VegaScreen *screen, const char *listen_on, unsi
fds[count] = (struct pollfd){c->fd, (short)(POLLIN | (c->out_at < c->out.used ? POLLOUT : 0)), 0};
slot[count++] = i;
}
- int ready = poll(fds, count, ticking ? 33 : 1000);
+ int ready = poll(fds, count, screen->ticks ? 33 : reload ? 200 : 1000);
if (ready < 0 && errno != EINTR) break;
bool changed = false;
- if (ready == 0 && ticking) changed = pure_app_frame(app) == PURE_OK;
+ if (ready == 0 && screen->ticks) changed = pure_app_frame(app) == PURE_OK;
+ if (reload && reload()) changed = true; /* a new module: every page draws it */
if (fds[0].revents & POLLIN)
for (int fd; (fd = accept4(listener, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC)) >= 0;) {
unsigned free_slot = CLIENTS;
diff --git a/tests/__pycache__/check.cpython-313.pyc b/tests/__pycache__/check.cpython-313.pyc
new file mode 100644
index 0000000..04b698b
--- /dev/null
+++ b/tests/__pycache__/check.cpython-313.pyc
Binary files differ
diff --git a/tests/check.py b/tests/check.py
index 48487a3..a49a2a0 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -18,6 +18,11 @@
interactive session's entries are there in the next one.
6. vega check names every convention a spec seems to mean but misses, and
none in the examples.
+9. Hot swap: a new module replaces the running one, states keeping their
+ values by name, whether a script swaps it or --watch (web, and the TUI on
+ a pseudo-terminal) sees its file written again, even at the same size
+ within the same second; a module that does not load leaves the app as it
+ was.
7. Painting: the bytes the TUI sends, replayed by a small terminal emulator,
give the very frame the dump shows, while sending only what changed.
8. Web: the GUI surface's screen JSON matches the model after random keys,
@@ -29,6 +34,7 @@ import argparse
import json
import os
import random
+import shutil
import subprocess
import sys
import tempfile
@@ -540,6 +546,69 @@ def split(vega, module, work, seeds, steps=200):
return compared
+def hot_swap(vega, aup, work):
+ """Swap a module under a running app: by script, and by --watch in the
+ web surface when the file is written again (atomically, as aup watch
+ writes it). States keep their values by name; a broken module is refused."""
+ import time
+ env = dict(os.environ, AUP_PATH=str(ROOT / 'lib'))
+ source = (ROOT / 'examples/todo.aup').read_text()
+ changed = source.replace('view title = "To do"', 'view title = "Things"') + \
+ 'view done_count = count(t in tasks where t.done)\n'
+ modules = []
+ # again differs from after in one letter: a module of the same size, written within the same second.
+ for name, text in (('before', source), ('after', changed), ('again', changed.replace('"Things"', '"Thingz"'))):
+ (work / f'{name}.aup').write_text(text)
+ assert run(aup, 'build', work / f'{name}.aup', '-o', work / f'{name}.pmod', env=env).returncode == 0
+ modules.append(work / f'{name}.pmod')
+ script = f'text 61\ntext 62\nkey Space\nswap {modules[1]}\nkey ArrowUp\nshow\n'
+ out = run(vega, 'cli', modules[0], stdin=script).stdout.splitlines()
+ assert out == ['Things', 'left: 1 of 2', 'tasks_list (2)', '> a', ' b [done] (muted)', 'done_count: 1'], out
+ # --watch: the web surface takes the new module and tells every page.
+ live = work / 'live.pmod'
+ shutil.copy(modules[0], live)
+ surface = WebSurface(vega, live, '--watch')
+ import http.client
+ import threading
+ heard = []
+ stream = http.client.HTTPConnection('127.0.0.1', surface.port, timeout=10)
+ stream.request('GET', '/events')
+ events = stream.getresponse()
+ threading.Thread(target=lambda: [heard.append(line) for line in iter(events.readline, b'') if line.startswith(b'data:')],
+ daemon=True).start()
+ def replace(module, title):
+ """Write module over the watched file as aup does, and wait for the page to show title."""
+ shutil.copy(module, work / 'live.tmp')
+ os.replace(work / 'live.tmp', live)
+ for _ in range(50):
+ screen = json.loads(surface.call('GET', '/screen')[1])
+ if screen['title'] == title: return screen
+ time.sleep(0.05)
+ raise AssertionError(f'never shown: {title}: {screen}')
+
+ try:
+ for word in ('a', 'b'): surface.call('POST', '/text', word)
+ surface.call('POST', '/key', 'Space')
+ time.sleep(0.3)
+ before = len(heard)
+ screen = replace(modules[1], 'Things')
+ assert {'name': 'done_count', 'text': '1'} in screen['fields'], screen
+ assert [item['label'] for item in screen['parts'][0]['items']] == ['a', 'b'], screen
+ time.sleep(0.2)
+ assert len(heard) > before, 'the page hears of the new module'
+ assert modules[2].stat().st_size == modules[1].stat().st_size
+ replace(modules[2], 'Thingz')
+ (work / 'live.tmp').write_bytes(b'not a module')
+ os.replace(work / 'live.tmp', live)
+ time.sleep(0.6)
+ assert json.loads(surface.call('GET', '/screen')[1])['title'] == 'Thingz' # the broken one is refused
+ finally:
+ surface.stop()
+ shutil.copy(modules[0], work / 'tui.pmod')
+ interactive(vega, work / 'tui.pmod', replace=modules[1])
+ return len(out)
+
+
def scores(vega, module):
joins = ['ann', '범수', 'Zoë']
script = [f'text {name.encode().hex()}' for name in joins]
@@ -571,14 +640,16 @@ def scores(vega, module):
return len(out)
-def interactive(vega, module, store=None):
+def interactive(vega, module, store=None, replace=None):
"""The terminal surface on a real pseudo-terminal: typed text commits
with Enter, arrow keys and letters reach the app after Tab, Ctrl-C leaves
- and restores the terminal."""
+ and restores the terminal. With replace, it runs with --watch and replace
+ is then written over module: the terminal shows it, with the same tasks."""
import fcntl, pty, select, struct, termios, time
pid, fd = pty.fork()
if pid == 0:
- os.execv(str(vega), [str(vega), 'tui', str(module)] + (['--store', str(store)] if store else []))
+ os.execv(str(vega), [str(vega), 'tui', str(module)] + (['--store', str(store)] if store else []) +
+ (['--watch'] if replace else []))
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack('HHHH', 10, 40, 0, 0))
os.kill(pid, 28) # SIGWINCH: take the size
@@ -598,6 +669,10 @@ def interactive(vega, module, store=None):
for keys in (b'milk\r', 'b\u00e9ton\r'.encode(), b'\t', b'\x1b[A', b' ', b'u', b'\x1b[<0;3;5M\x1b[<0;3;5m'):
os.write(fd, keys)
screen += read_for(0.2)
+ if replace:
+ shutil.copy(replace, module.with_suffix('.tmp'))
+ os.replace(module.with_suffix('.tmp'), module)
+ screen += read_for(0.6)
os.write(fd, b'\x03')
screen += read_for(0.3)
_, status = os.waitpid(pid, 0)
@@ -609,8 +684,12 @@ def interactive(vega, module, store=None):
grid = [[[' ', 0, False] for _ in range(40)] for _ in range(10)]
replay(screen, grid)
final = shown(grid)
- assert 'milk' in '\n'.join(final) and '(2)' in final[2] and 'done' in final[3], final
- assert final[4].startswith('>> béton') and final[3].startswith('| milk'), final # the click moved the focus
+ if replace: # the new title and field, the same tasks, focus and done mark
+ assert final[:3] == ['|Things', '|left 1 of 2\tmuted', '|done_count 1\tmuted'], final
+ assert final[4].startswith('| milk') and 'done' in final[4] and final[5].startswith('>> béton'), final
+ else:
+ assert 'milk' in '\n'.join(final) and '(2)' in final[2] and 'done' in final[3], final
+ assert final[4].startswith('>> béton') and final[3].startswith('| milk'), final # the click moved the focus
assert '\x1b[?1000h' in text and '\x1b[?1000l' in text, 'mouse reports on, then off'
if store: # what was typed is still there in the next session
again = run(vega, 'cli', module, '--store', store, stdin='show\n').stdout.splitlines()
@@ -739,6 +818,7 @@ def main():
cli_and_say(vega, modules['todo'], range(args.seeds))
report['tui_frames'] = tui(vega, modules['todo'], range(args.seeds), [(40, 12), (24, 9), (80, 30), (16, 6)])
report['table_lines'] = scores(vega, modules['scores'])
+ report['hot_swap_lines'] = hot_swap(vega, aup, work)
report['split_list_lines'] = split(vega, modules['today'], work, range(args.seeds))
report['painted_frames'], report['first_paint_bytes'], report['later_paint_bytes'] = \
painting(vega, modules['todo'], range(args.seeds), [(40, 12), (24, 9), (80, 30)])