aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-26 18:04:31 +0100
committereunhhu <[email protected]>2026-09-26 18:04:31 +0100
commit9c2d1eb1d423625672cb469af6bf708a48d3cb71 (patch)
treeaffdf0b1ac702129521264ecf1aae64f20c06afb
parentb466603358f8fa5a74551a9819fdd54ad124ef0c (diff)
downloadvega-9c2d1eb1d423625672cb469af6bf708a48d3cb71.tar.gz
vega-9c2d1eb1d423625672cb469af6bf708a48d3cb71.zip
Add a board used by people and programs alike
examples/board.aup is a kanban board: people add and move cards on Vega's surfaces, programs post JSON to the same module served by pure-serve, with no routes written. The checks write cards with the CLI into a store, serve that store over HTTP where a program moves one and adds another, and read it again with the CLI, the focus still on the card it was on.
-rw-r--r--Makefile3
-rw-r--r--README.md14
-rw-r--r--docs/design/001-first-component.md4
-rw-r--r--examples/board.aup58
-rw-r--r--tests/check.py44
5 files changed, 120 insertions, 3 deletions
diff --git a/Makefile b/Makefile
index 2dde6d2..bc73568 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/README.md b/README.md
index 6c89767..0ab0430 100644
--- a/README.md
+++ b/README.md
@@ -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'] = \