diff options
| -rw-r--r-- | Makefile | 3 | ||||
| -rw-r--r-- | README.md | 14 | ||||
| -rw-r--r-- | docs/design/001-first-component.md | 4 | ||||
| -rw-r--r-- | examples/board.aup | 58 | ||||
| -rw-r--r-- | tests/check.py | 44 |
5 files changed, 120 insertions, 3 deletions
@@ -21,4 +21,5 @@ $(BUILD)/vega: $(SOURCES) src/vega.h $(PURE_SOURCES) | $(BUILD) check: $(BUILD)/vega $(MAKE) -C $(AUP) build/aup - python3 tests/check.py --vega $(BUILD)/vega --aup $(AUP)/build/aup + $(MAKE) -C $(PURE) build/pure-serve + python3 tests/check.py --vega $(BUILD)/vega --aup $(AUP)/build/aup --serve $(PURE)/build/pure-serve @@ -18,6 +18,20 @@ build/vega web todo.pmod --watch # 브라우저(GUI). --watch: 모� make check # 참조 모델 대조, 프레임 검사, 대화형 pty 검사, 견고성 ``` +사람과 프로그램이 같은 앱을 쓴다. `examples/board.aup`는 칸반 보드다. 사람은 Vega 표면으로, 프로그램은 같은 모듈과 +저장소를 서빙하는 Pure의 `pure-serve`로 쓴다(라우트 코드가 없다. 보기가 GET, 입력이 POST다). + +```sh +AUP_PATH=lib ../aup/build/aup build examples/board.aup -o board.pmod +build/vega tui board.pmod --store ~/.board # 사람: 제목을 쓰고 Enter, Tab 뒤 ←→로 칸을 옮긴다 +../pure/prototypes/connections/build/pure-serve --module board.pmod --store ~/.board --threads 2 & +curl -s localhost:8767/views/counts # 1 to do, 1 doing, 0 done +curl -s -X POST -H 'Content-Type: application/json' -d '{"title": "ship it", "lane": 2}' localhost:8767/inputs/post +curl -s 'localhost:8767/views/cards_list?from=0&count=20' +``` + +한 저장소는 한 번에 한 호스트가 연다(상태의 소유자는 하나다). + ## 위치 | 층 | 역할 | 비유 | diff --git a/docs/design/001-first-component.md b/docs/design/001-first-component.md index f1db4d8..6028297 100644 --- a/docs/design/001-first-component.md +++ b/docs/design/001-first-component.md @@ -163,13 +163,15 @@ CLI나 문장 표면이 그대로 이어받는다. Vega에도 앱에도 저장 � - 가상 터미널에서 대화형 TUI를 실제로 띄워 입력·확정·키 모드·종료·터미널 복원을 확인한다. - 한 저장소를 CLI와 문장 표면이 번갈아 이어 쓰며(시드마다 7번) 매번 모델과 같은 목록을 보이고, 대화형 세션에서 입력한 것이 다음 실행에 남는다. -- aup와 Vega의 예제 15개를 세 표면에 무작위 입력으로 돌려 비정상 종료가 없다. sanitizer 빌드로도 같은 검사를 통과한다. +- aup와 Vega의 예제 16개를 세 표면에 무작위 입력으로 돌려 비정상 종료가 없다. sanitizer 빌드로도 같은 검사를 통과한다. - 규약을 여덟 가지로 어긴 명세에서 `vega check`가 아홉 곳을 모두 정확한 문장으로 알리고, 예제에서는 아무것도 알리지 않는다. - 걸러진 두 목록(`examples/today.aup`)의 CLI 출력이 무작위 추가·이동·완료·양쪽 목록 가리키기 1,600단계에서 모델과 같다(포커스는 그 항목을 가진 목록에만 있다). 웹 JSON은 다른 목록의 포커스를 null로 준다. - 무작위 스크립트에 `point`(범위 밖 포함)와 TUI `click`을 섞어도 CLI·문장·프레임이 모델과 같다. 가상 터미널에서 마우스로 행을 클릭하면 포커스가 옮겨지고, 저장소를 쓰는 다음 실행이 그 포커스에서 시작한다. +- 호스트 사이: 보드(`examples/board.aup`)를 CLI가 저장소에 쓰고, 같은 저장소를 `pure-serve`가 JSON API로 서빙하며 + (프로그램이 카드를 옮기고 더한다), 다시 연 CLI가 그 변경과 같은 카드에 머문 포커스를 보인다. - 시간: 스톱워치가 스크립트의 시각에서 정확한 줄을 내고(시작, 틱, 랩, 멈춤, 초기화), 가상 터미널에서는 실제 시계로 1초 남짓 흐른 시간을 보이며, 웹에서는 1초 동안 열 번 넘게 틱 알림이 온다. - 모듈 교체: 스크립트의 `swap`, 웹의 `--watch`(새 제목과 필드, 같은 항목, 모든 페이지에 알림), 1초 안에 같은 크기로 diff --git a/examples/board.aup b/examples/board.aup new file mode 100644 index 0000000..ed73e75 --- /dev/null +++ b/examples/board.aup @@ -0,0 +1,58 @@ +# A board of cards in lanes, for people and programs alike: vega tui, web, +# cli and say show it, and pure-serve serves the same module and store as a +# JSON API (GET /views/NAME, POST /inputs/NAME) with no routes written. +use vega + +record Card { + title: text + lane = 0 # 0 to do, 1 doing, 2 done + since = 0 # when it came into its lane: milliseconds since 1970 +} + +state cards: list of Card by title +state at: index of cards + +input add = text # a person types a title +input post = message { # a program posts {"title": ..., "lane": ...} + title: text + lane: u32 +} +input forward = key ArrowRight +input back = key ArrowLeft +input up = key ArrowUp +input down = key ArrowDown +input drop = key Delete + +fn lane_name(lane) = if lane == 0 { "to do" } else if lane == 1 { "doing" } else { "done" } + +on add { + cards.add(Card { title: add, since: now }) + at = cards[add] +} +on post { + if !cards.has(post.title) { cards.add(Card { title: post.title }) } + cards[post.title].lane = min(post.lane, 2) + cards[post.title].since = now + reply post.title +} +on forward.down { + at.lane = min(at.lane + 1, 2) + at.since = now +} +on back.down { + at.lane = max(at.lane - 1, 0) + at.since = now +} +on up.down { at = at - 1 } +on down.down { at = at + 1 } +on drop.down { cards.remove(at) } + +view title = "Board" +view counts = "{count(c in cards where c.lane == 0)} to do, {count(c in cards where c.lane == 1)} doing, {count(c in cards where c.lane == 2)} done" + +view cards_list = each c in cards { + label = c.title + badge = lane_name(c.lane) + tone = if c.lane == 2 { vega.muted } else { vega.when(c.lane == 1, vega.accent) } +} +view cards_list_focus = at.position diff --git a/tests/check.py b/tests/check.py index ff531fe..f0c495e 100644 --- a/tests/check.py +++ b/tests/check.py @@ -674,6 +674,46 @@ def timing(vega, module): return len(out), heard +def hosts(vega, serve, module, work): + """examples/board.aup, one app and one store for people and programs: the + CLI adds and moves cards, pure-serve serves the same store as a JSON API + (a program moves one and adds another), and the CLI then shows what the + program did, its focus still on the card it was on.""" + import http.client + store = work / 'board-store' + script = 'time 1000\ntext ' + 'write docs'.encode().hex() + '\ntext ' + 'ship it'.encode().hex() + '\nkey ArrowRight\nshow\n' + before = run(vega, 'cli', module, '--store', store, stdin=script).stdout.splitlines() + assert before[-3:] == ['cards_list (2)', '> ship it [doing] (accent)', ' write docs [to do]'], before + server = subprocess.Popen([str(serve), '--module', str(module), '--store', str(store), '--port', '0'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + try: + port = json.loads(server.stdout.readline())['port'] + + def call(method, path, body=None): + connection = http.client.HTTPConnection('127.0.0.1', port, timeout=10) + headers = {'Content-Type': 'application/json'} if body else {} + connection.request(method, path, body, headers) + response = connection.getresponse() + answer = response.status, response.read() + connection.close() + return answer + + assert call('GET', '/views/counts') == (200, b'1 to do, 1 doing, 0 done') + assert call('POST', '/inputs/post', '{"title": "write docs", "lane": 2}') == (200, b'write docs') + assert call('POST', '/inputs/post', '{"title": "from api", "lane": 1}') == (200, b'from api') + rows = json.loads(call('GET', '/views/cards_list')[1])['items'] + assert [(r['label'], r['badge']) for r in rows] == [('from api', 'doing'), ('ship it', 'doing'), + ('write docs', 'done')], rows + finally: + import signal + server.send_signal(signal.SIGTERM) + assert server.wait(timeout=10) == 0, server.stderr.read() + after = run(vega, 'cli', module, '--store', store, stdin='show\n').stdout.splitlines() + assert after == ['Board', 'counts: 0 to do, 2 doing, 1 done', 'cards_list (3)', ' from api [doing] (accent)', + '> ship it [doing] (accent)', ' write docs [done] (muted)'], after + return len(rows) + + def scores(vega, module): joins = ['ann', '범수', 'Zoë'] script = [f'text {name.encode().hex()}' for name in joins] @@ -866,6 +906,7 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument('--vega', type=Path, required=True) parser.add_argument('--aup', type=Path, required=True) + parser.add_argument('--serve', type=Path, required=True) parser.add_argument('--seeds', type=int, default=8) parser.add_argument('--rounds', type=int, default=3) args = parser.parse_args() @@ -875,7 +916,7 @@ def main(): work = Path(directory) env = dict(os.environ, AUP_PATH=str(ROOT / 'lib')) modules = {} - for name in ('todo', 'scores', 'today', 'stopwatch'): + for name in ('todo', 'scores', 'today', 'stopwatch', 'board'): modules[name] = work / f'{name}.pmod' built = run(aup, 'build', ROOT / 'examples' / f'{name}.aup', '-o', modules[name], env=env) assert built.returncode == 0, built.stderr @@ -884,6 +925,7 @@ def main(): 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['timed_lines'], report['ticks_heard'] = timing(vega, modules['stopwatch']) + report['board_rows_across_hosts'] = hosts(vega, args.serve.resolve(), modules['board'], work) 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'] = \ |
