From 2d7024ba01875a8fc7a2c99e78c62911bcc7fe3f Mon Sep 17 00:00:00 2001 From: eunhhu Date: Fri, 25 Sep 2026 15:52:01 +0100 Subject: Keep a surface's app in a store Every surface takes --store PATH and runs the app on Pure's tree store, so a list typed in the terminal is there the next time, on any surface. Vega and the app carry no saving code; Vega saves the last frame changes when it ends. Checks let the CLI and the sentence surface take turns on one store and match the model each time, and reopen an interactive session's store. --- Makefile | 3 ++- README.md | 3 ++- docs/design/001-first-component.md | 8 +++++++ src/main.c | 31 ++++++++++++++++++------- tests/check.py | 46 +++++++++++++++++++++++++++++++++++--- 5 files changed, 78 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index 9b15acc..7447bef 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,8 @@ BUILD ?= build PURE ?= ../pure/prototypes/connections AUP ?= ../aup -PURE_SOURCES = $(PURE)/module.c $(PURE)/module-list.c $(PURE)/module-text.c $(PURE)/store-tree.c +PURE_SOURCES = $(PURE)/module.c $(PURE)/module-list.c $(PURE)/module-save.c $(PURE)/module-store.c \ + $(PURE)/module-text.c $(PURE)/store-tree.c SOURCES = src/screen.c src/draw.c src/width.c src/main.c .PHONY: all check diff --git a/README.md b/README.md index 6f0a0a1..ce490dd 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ Vega는 **인터페이스 구현자**이며 하나의 **aup 라이브러리**다 ```sh make # build/vega (옆의 ../pure 체크아웃을 링크) AUP_PATH=lib ../aup/build/aup build examples/todo.aup -o todo.pmod -build/vega tui todo.pmod # 터미널. 글자 입력 후 Enter, Tab으로 키 모드, Ctrl-C로 끝 +build/vega tui todo.pmod --store ~/.todo # 터미널. 글자 입력 후 Enter, Tab으로 키 모드, Ctrl-C로 끝. + # --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 make check # 참조 모델 대조, 프레임 검사, 대화형 pty 검사, 견고성 diff --git a/docs/design/001-first-component.md b/docs/design/001-first-component.md index 434c0fe..db04bad 100644 --- a/docs/design/001-first-component.md +++ b/docs/design/001-first-component.md @@ -46,6 +46,12 @@ Tab이 키 모드로 바꾸면 글자·숫자·Space가 `KeyA`·`Digit0`·`Space 표시 폭은 Vega가 맡는다. 한글·CJK·그림 이모지는 두 칸, 결합 문자·변이 선택자·ZWJ는 0칸, 제어 문자는 제어 그림(U+2400대)으로 보인다. grapheme 단위 자르기는 아직 없어 ZWJ 이모지 묶음은 근사다. +## 영속 + +모든 표면은 `--store 경로`를 받는다. 앱의 상태는 Pure가 그 트리 저장소에 두므로, 터미널에서 적은 할 일을 다음 실행의 +CLI나 문장 표면이 그대로 이어받는다. Vega에도 앱에도 저장 코드가 없다([Pure 008](/srv/git/pure.git) 상태 영속). +끝날 때 frame 변경까지 저장한다. + ## 검증 (`make check`) - `examples/todo.aup`에 무작위 추가·이동·토글·긴급·삭제 스크립트를 주고, CLI 출력과 NUI 문장이 앱과 Vega 규약의 @@ -55,6 +61,8 @@ Tab이 키 모드로 바꾸면 글자·숫자·Space가 `KeyA`·`Digit0`·`Space 2,000항목 목록도 한 프레임에 25칸만 계산한다. - `examples/scores.aup`의 표가 표시 폭으로 정렬되고(한글·결합 문자 포함) 행 톤이 표시된다. - 가상 터미널에서 대화형 TUI를 실제로 띄워 입력·확정·키 모드·종료·터미널 복원을 확인한다. +- 한 저장소를 CLI와 문장 표면이 번갈아 이어 쓰며(시드마다 7번) 매번 모델과 같은 목록을 보이고, 대화형 세션에서 입력한 + 것이 다음 실행에 남는다. - aup와 Vega의 예제 11개를 세 표면에 무작위 입력으로 돌려 비정상 종료가 없다. sanitizer 빌드로도 같은 검사를 통과한다. ## 남은 것 diff --git a/src/main.c b/src/main.c index 3c3ba0f..1d8cc4b 100644 --- a/src/main.c +++ b/src/main.c @@ -1,5 +1,7 @@ #define _DEFAULT_SOURCE #include "vega.h" +#include "module-store.h" +#include "store.h" #include #include #include @@ -8,11 +10,13 @@ #include #include -/* vega SURFACE MODULE: one module, one screen, several surfaces. +/* vega SURFACE MODULE [--store PATH]: one module, one screen, several surfaces. * tui interactive terminal * tui --dump WxH the terminal's frames as text, driven by a script * cli plain lines for pipes, driven by a script * say sentences (the first natural-language surface), driven by a script + * With --store the app's states live in that tree store: the app continues + * where it was, whichever surface runs it. * Script commands, one per line: key KEYNAME | text HEX|- | type HEX | enter | * tab | frame [N] | 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 @@ -25,7 +29,7 @@ static char entry[VEGA_TEXT]; static size_t entry_length; static bool typing; -static bool load(const char *path) { +static bool load(const char *path, const char *store) { static unsigned char bytes[1 << 17]; FILE *file = fopen(path, "rb"); size_t size = file ? fread(bytes, 1, sizeof(bytes), file) : 0; @@ -37,7 +41,9 @@ static bool load(const char *path) { fprintf(stderr, "vega: %s: %s\n", path, error); return false; } - if (pure_app_start(&app, &module, (PureData){0}) != PURE_OK) { fprintf(stderr, "vega: cannot start\n"); 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}); + if (started != PURE_OK) { fprintf(stderr, "vega: cannot start (%d)\n", (int)started); return false; } vega_screen(&screen, &module); typing = screen.prompt >= 0; return true; @@ -362,17 +368,26 @@ static int script(Surface surface, unsigned width, unsigned height) { int main(int argc, char **argv) { unsigned width = 0, height = 0; - bool dumped = argc == 5 && !strcmp(argv[3], "--dump") && sscanf(argv[4], "%ux%u", &width, &height) == 2 && - width && height && width <= 1000 && height <= 1000; - if ((argc != 3 && !dumped) || (dumped && strcmp(argv[1], "tui"))) { - fputs("vega tui|cli|say MODULE.pmod\nvega tui MODULE.pmod --dump WIDTHxHEIGHT < script\n", stderr); + bool dumped = false, usage = argc < 3 || argc % 2 == 0; + const char *store = NULL; + for (int i = 3; !usage && i + 1 < argc; i += 2) { + if (!strcmp(argv[i], "--store")) store = argv[i + 1]; + else if (!strcmp(argv[i], "--dump") && sscanf(argv[i + 1], "%ux%u", &width, &height) == 2 && width && height && + width <= 1000 && height <= 1000) dumped = true; + else usage = true; + } + if (usage || (dumped && strcmp(argv[1], "tui"))) { + fputs("vega tui|cli|say MODULE.pmod [--store PATH]\n" + "vega tui MODULE.pmod [--store PATH] --dump WIDTHxHEIGHT < script\n", stderr); return 2; } Surface surface = !strcmp(argv[1], "cli") ? SURFACE_CLI : !strcmp(argv[1], "say") ? SURFACE_SAY : !strcmp(argv[1], "tui") ? SURFACE_TUI : (Surface)-1; if ((int)surface < 0) { fprintf(stderr, "vega: unknown surface %s\n", argv[1]); return 2; } - if (!load(argv[2])) return 2; + if (!load(argv[2], store)) return 2; int status = surface == SURFACE_TUI && !dumped ? interactive() : script(surface, width, height); + /* 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; } pure_app_stop(&app); return status; } diff --git a/tests/check.py b/tests/check.py index 2d01c98..6256023 100644 --- a/tests/check.py +++ b/tests/check.py @@ -14,6 +14,8 @@ 3. Tables: examples/scores.aup aligns its columns by display width and marks row tones. 4. Every aup example runs on every surface under random keys without a crash. +5. Stored: surfaces take turns on one store and each continues the list; an + interactive session's entries are there in the next one. """ import argparse import json @@ -146,6 +148,39 @@ def cli_and_say(vega, module, seeds, steps=300): return compared, failures +def stored_surfaces(vega, module, work, seeds, steps=240): + """One store, several surfaces in turn: each run continues the list where + the last one left it, and prints or says what the model says.""" + compared = 0 + for seed in seeds: + rng = random.Random(seed + 500) + model, store = Todo(), work / f'store-{seed}' + cuts = sorted(rng.sample(range(1, steps), 6)) + for number, (start, end) in enumerate(zip([0] + cuts, cuts + [steps])): + surface = ['cli', 'say'][number % 2] + script, expected = [], [] + for _ in range(start, end): + if rng.random() < 0.3: + word = rng.choice(WORDS) + script.append('text ' + (word.encode().hex() or '-')) + ok, name = model.event('add', word), 'add' + else: + name = rng.choice(list(KEYS) + ['drop']) + script.append(f'key {KEYS[name]}') + ok = model.event(name) + if surface == 'say': + expected.append(model.focus() if ok else f'Could not {name}.') + script.append('show') + expected += model.cli() if surface == 'cli' else model.say() + out = run(vega, surface, module, '--store', store, stdin='\n'.join(script) + '\n') + assert out.returncode == 0 and out.stdout.splitlines() == expected, \ + (seed, number, [(a, b) for a, b in zip(out.stdout.splitlines(), expected) if a != b][:3]) + compared += len(expected) + frame = run(vega, 'tui', module, '--store', store, '--dump', '40x12', stdin='show\n').stdout.splitlines() + assert frame[2].startswith(f'|tasks_list ({len(model.tasks)})'), frame + return compared + + def frames(output, height): lines = output.splitlines() for start in range(0, len(lines), height + 1): @@ -229,14 +264,14 @@ def scores(vega, module): return len(out) -def interactive(vega, module): +def interactive(vega, module, store=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.""" import fcntl, pty, select, struct, termios, time pid, fd = pty.fork() if pid == 0: - os.execv(str(vega), [str(vega), 'tui', str(module)]) + os.execv(str(vega), [str(vega), 'tui', str(module)] + (['--store', str(store)] if store else [])) fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack('HHHH', 10, 40, 0, 0)) os.kill(pid, 28) # SIGWINCH: take the size @@ -263,6 +298,9 @@ def interactive(vega, module): assert os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0, status assert '\x1b[?1049h' in text and text.rstrip().endswith('\x1b[?1049l'), text[-80:] assert 'milk' in text and 'béton' in text and '(2)' in text and 'done' in text, text[-400:] + if store: # what was typed is still there in the next session + again = run(vega, 'cli', module, '--store', store, stdin='show\n').stdout.splitlines() + assert again == ['To do', 'left: 2 tasks', 'tasks_list (2)', '> milk [done] (muted)', ' béton'], again return 1 @@ -316,7 +354,9 @@ 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['interactive_sessions'] = interactive(vega, modules['todo']) + report['interactive_sessions'] = interactive(vega, modules['todo']) + \ + interactive(vega, modules['todo'], work / 'interactive-store') + report['stored_surface_lines'] = stored_surfaces(vega, modules['todo'], work, range(args.seeds)) report['examples'], report['robustness_runs'] = robustness(vega, aup, work, args.rounds) print(json.dumps(report)) -- cgit v1.2.3