aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile3
-rw-r--r--README.md3
-rw-r--r--docs/design/001-first-component.md8
-rw-r--r--src/main.c31
-rw-r--r--tests/check.py46
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 <poll.h>
#include <signal.h>
#include <stdlib.h>
@@ -8,11 +10,13 @@
#include <termios.h>
#include <unistd.h>
-/* 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))