geneseed 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +938 -0
- package/DESIGN.md +1423 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +48 -0
- package/README.md +336 -0
- package/SETUP.md +1027 -0
- package/SHIPPED.md +70 -0
- package/adapters/bob/README.md +100 -0
- package/adapters/claude-code/README.md +101 -0
- package/adapters/claude-code/settings.json +47 -0
- package/adapters/claude-code/skills/geneseed/SKILL.md +72 -0
- package/adapters/copilot/README.md +76 -0
- package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +434 -0
- package/adapters/opencode/HOW-OPENCODE-LOADS.md +158 -0
- package/adapters/opencode/README.md +582 -0
- package/adapters/opencode/agents/orchestrator.md +25 -0
- package/adapters/opencode/opencode.json +44 -0
- package/adapters/opencode/plugins/geneseed-activity.js +430 -0
- package/adapters/opencode/plugins/geneseed-context.js +876 -0
- package/adapters/opencode/plugins/geneseed-guard.js +290 -0
- package/adapters/opencode/plugins/geneseed-learn.js +396 -0
- package/adapters/opencode/plugins/geneseed-notify.js +142 -0
- package/adapters/opencode/plugins/geneseed-ponytail.js +148 -0
- package/adapters/opencode/plugins/geneseed-workflow.js +183 -0
- package/adapters/opencode/workflows/_runtime.js +277 -0
- package/adapters/opencode/workflows/council.js +64 -0
- package/adapters/opencode/workflows/dispatch.js +89 -0
- package/adapters/opencode/workflows/research-plan-implement.js +43 -0
- package/adapters/opencode/workflows/review.js +57 -0
- package/bin/geneseed-cli.mjs +412 -0
- package/bin/geneseed-hook.mjs +129 -0
- package/bin/geneseed.mjs +1086 -0
- package/docs/declined.md +149 -0
- package/docs/limits.md +270 -0
- package/docs/opencode-plugin-setup.md +64 -0
- package/docs/token-footprint.md +68 -0
- package/docs/web/_groups.json +45 -0
- package/docs/web/about.md +6 -0
- package/docs/web/adapters-claude-code.md +8 -0
- package/docs/web/adapters-opencode-loads.md +8 -0
- package/docs/web/adapters-opencode-spec.md +8 -0
- package/docs/web/adapters-opencode.md +8 -0
- package/docs/web/agents.md +8 -0
- package/docs/web/authoring.md +11 -0
- package/docs/web/autostart.md +9 -0
- package/docs/web/claude-hooks.md +36 -0
- package/docs/web/cli.md +6 -0
- package/docs/web/collaboration.md +27 -0
- package/docs/web/configure-wiki.md +9 -0
- package/docs/web/design.md +7 -0
- package/docs/web/env-knobs.md +9 -0
- package/docs/web/exclusions.md +26 -0
- package/docs/web/first-session.md +28 -0
- package/docs/web/footprint.md +31 -0
- package/docs/web/foreman-mode.md +33 -0
- package/docs/web/glossary.md +6 -0
- package/docs/web/harnesses.md +28 -0
- package/docs/web/headless.md +9 -0
- package/docs/web/ingest-docs.md +9 -0
- package/docs/web/install-paths.md +9 -0
- package/docs/web/install-quick.md +35 -0
- package/docs/web/lsp-overview.md +37 -0
- package/docs/web/mcp-claude-code.md +10 -0
- package/docs/web/mcp-filesystem.md +9 -0
- package/docs/web/mcp-gitlab.md +9 -0
- package/docs/web/mcp-markitdown.md +9 -0
- package/docs/web/mcp-overview.md +34 -0
- package/docs/web/mcp-trouble.md +9 -0
- package/docs/web/mcp-verify.md +9 -0
- package/docs/web/memory.md +7 -0
- package/docs/web/migrate.md +39 -0
- package/docs/web/model.md +20 -0
- package/docs/web/notebook.md +7 -0
- package/docs/web/pipelines.md +24 -0
- package/docs/web/plugin-activity.md +23 -0
- package/docs/web/plugin-context.md +26 -0
- package/docs/web/plugin-guard.md +24 -0
- package/docs/web/plugin-learn.md +27 -0
- package/docs/web/plugin-notify.md +24 -0
- package/docs/web/plugin-ponytail.md +23 -0
- package/docs/web/plugin-setup.md +7 -0
- package/docs/web/plugin-workflow.md +24 -0
- package/docs/web/plugins.md +17 -0
- package/docs/web/project-context.md +9 -0
- package/docs/web/rules.md +8 -0
- package/docs/web/run-anywhere.md +9 -0
- package/docs/web/self-improve.md +8 -0
- package/docs/web/setup-choices.md +39 -0
- package/docs/web/skills.md +8 -0
- package/docs/web/themes-author.md +8 -0
- package/docs/web/themes.md +9 -0
- package/docs/web/token-footprint.md +7 -0
- package/docs/web/trouble.md +38 -0
- package/docs/web/upgrade.md +9 -0
- package/docs/web/verify.md +30 -0
- package/docs/web/wiki.md +7 -0
- package/docs/web/worktree.md +10 -0
- package/docs/web-ui.md +144 -0
- package/docs/wiki.md +43 -0
- package/geneseed +39 -0
- package/geneseed.cmd +34 -0
- package/harness.config.json +6 -0
- package/js/anim.mjs +326 -0
- package/js/catalog.mjs +112 -0
- package/js/checkout.mjs +88 -0
- package/js/cli-table.json +1034 -0
- package/js/cli.mjs +498 -0
- package/js/diff.mjs +384 -0
- package/js/doctor.mjs +1364 -0
- package/js/emit.mjs +1373 -0
- package/js/excludes.mjs +271 -0
- package/js/generate.mjs +379 -0
- package/js/hooks.mjs +1026 -0
- package/js/hosts.mjs +253 -0
- package/js/installs.mjs +378 -0
- package/js/inventory.mjs +175 -0
- package/js/lib/fs.mjs +913 -0
- package/js/lib/proc.mjs +28 -0
- package/js/lib/udiff.mjs +275 -0
- package/js/link.mjs +286 -0
- package/js/mcp.mjs +335 -0
- package/js/memory.mjs +108 -0
- package/js/menu.mjs +118 -0
- package/js/migrate.mjs +251 -0
- package/js/native.mjs +426 -0
- package/js/opencode.mjs +325 -0
- package/js/registry.mjs +110 -0
- package/js/render.mjs +282 -0
- package/js/settings.mjs +924 -0
- package/js/setup.mjs +492 -0
- package/js/status.mjs +281 -0
- package/js/themes.mjs +195 -0
- package/js/tui.mjs +444 -0
- package/js/uninstall.mjs +1137 -0
- package/js/update.mjs +910 -0
- package/js/web/actions.mjs +868 -0
- package/js/web/activity.mjs +284 -0
- package/js/web/api.mjs +873 -0
- package/js/web/docs.mjs +531 -0
- package/js/web/graph.mjs +0 -0
- package/js/web/jobs.mjs +480 -0
- package/js/web/server.mjs +1135 -0
- package/package.json +54 -0
- package/registry.json +474 -0
- package/src/AGENT.md.tmpl +390 -0
- package/src/agents/_template.md +76 -0
- package/src/agents/advocate.md +36 -0
- package/src/agents/architect.md +55 -0
- package/src/agents/developer.md +66 -0
- package/src/agents/docs.md +54 -0
- package/src/agents/empiricist.md +39 -0
- package/src/agents/explorer.md +54 -0
- package/src/agents/framer.md +36 -0
- package/src/agents/historian.md +51 -0
- package/src/agents/operator.md +37 -0
- package/src/agents/pragmatist.md +37 -0
- package/src/agents/reviewer.md +52 -0
- package/src/agents/security.md +57 -0
- package/src/agents/skeptic.md +48 -0
- package/src/agents/steward.md +36 -0
- package/src/agents/tester.md +54 -0
- package/src/agents/user-advocate.md +36 -0
- package/src/agents/visionary.md +36 -0
- package/src/laws/universal.md +387 -0
- package/src/memory/README.md +109 -0
- package/src/memory/gitignore +6 -0
- package/src/modes/README.md +26 -0
- package/src/modes/direct.md +2 -0
- package/src/modes/foreman.md +19 -0
- package/src/notebook/README.md +39 -0
- package/src/notebook/gitignore +7 -0
- package/src/postures/README.md +31 -0
- package/src/postures/artisan.md +14 -0
- package/src/postures/assistant.md +12 -0
- package/src/postures/expert.md +11 -0
- package/src/postures/mentor.md +11 -0
- package/src/postures/peer.md +12 -0
- package/src/skills/_template.md +47 -0
- package/src/skills/brainstorm.md +28 -0
- package/src/skills/clarify.md +28 -0
- package/src/skills/codebase-design.md +44 -0
- package/src/skills/commit.md +48 -0
- package/src/skills/council.md +34 -0
- package/src/skills/crash-course.md +28 -0
- package/src/skills/daydream/.claude-plugin/plugin.json +9 -0
- package/src/skills/daydream/SKILL.md +74 -0
- package/src/skills/daydream/VENDOR.md +25 -0
- package/src/skills/daydream/critic-prompt.md +38 -0
- package/src/skills/daydream/instructions.md +422 -0
- package/src/skills/daydream/synthesizer-prompt.md +41 -0
- package/src/skills/debug.md +37 -0
- package/src/skills/decode.md +28 -0
- package/src/skills/develop.md +29 -0
- package/src/skills/document-project.md +79 -0
- package/src/skills/domain-modeling.md +38 -0
- package/src/skills/drill.md +28 -0
- package/src/skills/feynman.md +26 -0
- package/src/skills/forge-mcp.md +48 -0
- package/src/skills/fresh-eyes.md +29 -0
- package/src/skills/frontend-design.md +58 -0
- package/src/skills/gap-detector.md +27 -0
- package/src/skills/geneseed-code-review.md +33 -0
- package/src/skills/geneseed.md +29 -0
- package/src/skills/git-archaeology.md +47 -0
- package/src/skills/git-rescue.md +68 -0
- package/src/skills/handoff.md +29 -0
- package/src/skills/herdr.md +164 -0
- package/src/skills/ingest.md +49 -0
- package/src/skills/learning-path.md +26 -0
- package/src/skills/migrate.md +45 -0
- package/src/skills/opencode-theme.md +52 -0
- package/src/skills/parallel-agents.md +28 -0
- package/src/skills/pipeline.md +52 -0
- package/src/skills/plan.md +45 -0
- package/src/skills/ponytail.md +29 -0
- package/src/skills/profile.md +58 -0
- package/src/skills/prose.md +42 -0
- package/src/skills/prototype.md +44 -0
- package/src/skills/react-view-transitions/AGENTS.md +955 -0
- package/src/skills/react-view-transitions/README.md +42 -0
- package/src/skills/react-view-transitions/SKILL.md +320 -0
- package/src/skills/react-view-transitions/VENDOR.md +16 -0
- package/src/skills/react-view-transitions/metadata.json +12 -0
- package/src/skills/react-view-transitions/references/css-recipes.md +242 -0
- package/src/skills/react-view-transitions/references/implementation.md +182 -0
- package/src/skills/react-view-transitions/references/nextjs.md +176 -0
- package/src/skills/react-view-transitions/references/patterns.md +262 -0
- package/src/skills/refactor.md +32 -0
- package/src/skills/release.md +43 -0
- package/src/skills/repo-map.md +36 -0
- package/src/skills/research.md +42 -0
- package/src/skills/review-response.md +36 -0
- package/src/skills/roast-me.md +28 -0
- package/src/skills/rule.md +100 -0
- package/src/skills/ship.md +49 -0
- package/src/skills/tdd.md +31 -0
- package/src/skills/tickets.md +44 -0
- package/src/skills/token-report/SKILL.md +76 -0
- package/src/skills/token-report/VENDOR.md +23 -0
- package/src/skills/token-report/scripts/token_report.mjs +1301 -0
- package/src/skills/wayfinder.md +49 -0
- package/src/skills/wiki.md +68 -0
- package/src/skills/workflow.md +38 -0
- package/themes/README.md +69 -0
- package/themes/_TEMPLATE.json +147 -0
- package/themes/biker.json +147 -0
- package/themes/commentator.json +147 -0
- package/themes/cyberpunk.json +147 -0
- package/themes/gamer.json +147 -0
- package/themes/imperial.json +147 -0
- package/themes/joker.json +147 -0
- package/themes/marvin.json +147 -0
- package/themes/mean.json +147 -0
- package/themes/military.json +147 -0
- package/themes/neutral.json +147 -0
- package/themes/opencode/README.md +58 -0
- package/themes/opencode/catppuccin.json +27 -0
- package/themes/opencode/dracula.json +27 -0
- package/themes/opencode/everforest.json +27 -0
- package/themes/opencode/gruvbox.json +27 -0
- package/themes/opencode/kanagawa.json +27 -0
- package/themes/opencode/neon.json +27 -0
- package/themes/opencode/nord.json +27 -0
- package/themes/opencode/rosepine.json +27 -0
- package/themes/opencode/tokyonight.json +27 -0
- package/themes/pirate.json +147 -0
- package/themes/sports.json +147 -0
- package/themes/verstappen.json +147 -0
- package/themes/wizard.json +147 -0
- package/web/dist/assets/About-DX1-VTBP.js +1 -0
- package/web/dist/assets/Activity-Dp9NaXOn.js +1 -0
- package/web/dist/assets/ActivityDetail-HpOBtrXS.js +1 -0
- package/web/dist/assets/Diff-Ct4G3Se8.js +5 -0
- package/web/dist/assets/Doctor-DGjkZFuA.js +1 -0
- package/web/dist/assets/Graph-b8o-S6kv.js +1 -0
- package/web/dist/assets/Harnesses-gr3-aHBF.js +1 -0
- package/web/dist/assets/Laws-DTwF5tTC.js +1 -0
- package/web/dist/assets/Library-BEZscyaQ.js +1 -0
- package/web/dist/assets/Markdown-k_LSLBGq.js +1 -0
- package/web/dist/assets/Profile-DydukTxP.js +1 -0
- package/web/dist/assets/Rules-CIRG4Ldt.js +1 -0
- package/web/dist/assets/Skills-Buoi1IBr.js +1 -0
- package/web/dist/assets/StatusBadge-CE_AJttl.js +1 -0
- package/web/dist/assets/Themes-CI4o3gJC.js +2 -0
- package/web/dist/assets/activity-BCW7e6Cy.js +1 -0
- package/web/dist/assets/audiowide-latin-400-normal-6GFCX7ni.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-400-normal-DKaoCDn5.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-500-normal-C3sF8Y1B.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-600-normal-BHzjB6_C.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-400-italic-Bserb8X5.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-400-normal-BG6hkEXj.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-500-italic-BGfwwhek.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-500-normal-DrDcrrxK.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-600-normal-CIXX6EOa.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-700-normal-CeQ8H3UY.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-400-normal-BIGslYFI.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-500-normal-REKAfBVG.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-600-normal-DAxitmSu.woff2 +0 -0
- package/web/dist/assets/index-B1PIx7RM.js +1 -0
- package/web/dist/assets/index-BnTQUSUz.js +12 -0
- package/web/dist/assets/index-Ck2iCdaC.js +2 -0
- package/web/dist/assets/index-DUCsDv7z.css +1 -0
- package/web/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/web/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/web/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/web/dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
- package/web/dist/assets/marked.esm-JKvyS0Pp.js +46 -0
- package/web/dist/assets/oswald-latin-400-normal-BAFo8sP8.woff2 +0 -0
- package/web/dist/assets/oswald-latin-500-normal-B-eyT0Dw.woff2 +0 -0
- package/web/dist/assets/oswald-latin-600-normal-plFmSr5g.woff2 +0 -0
- package/web/dist/assets/oswald-latin-700-normal-vy6hmKSr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/web/dist/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/web/dist/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- package/web/dist/assets/react-DOpiTn5v.js +32 -0
- package/web/dist/assets/roman-DkYN0LX9.js +1 -0
- package/web/dist/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-600-normal-DjKNqYRj.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-400-italic-YylcN9Ay.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-400-normal-Rg4St2Dn.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-700-italic-vNvENeTh.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-700-normal-mWgeinG7.woff2 +0 -0
- package/web/dist/icon-maskable.svg +10 -0
- package/web/dist/icon.svg +8 -0
- package/web/dist/index.html +21 -0
- package/web/dist/manifest.webmanifest +14 -0
- package/web/dist/sw.js +40 -0
- package/web/src/pages/Laws.jsx +230 -0
package/js/tui.mjs
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tui` — and the TUI's pure half, which is the part of the panel that has no curses in it.
|
|
3
|
+
*
|
|
4
|
+
* WHAT CROSSES HERE AND WHAT DOES NOT.
|
|
5
|
+
*
|
|
6
|
+
* `cmd_tui` (`rituals/_harness_tui.py`) is twenty-six lines and three arms. Off a TTY it
|
|
7
|
+
* prints one line and returns 1; when `import curses` fails it prints one line and returns
|
|
8
|
+
* 1; otherwise it builds the inventory and hands the screen to `curses.wrapper(_tui_loop)`.
|
|
9
|
+
* **Every cell in `tests/harness_golden.py` runs off a TTY**, so the first arm is the whole
|
|
10
|
+
* of `tui`'s behaviour that the acceptance matrix can reach, and it is ported byte for byte.
|
|
11
|
+
*
|
|
12
|
+
* THE PANEL ITSELF IS P7C'S, AND THIS ENTRY FALLS BACK RATHER THAN INVENTING ONE — the same
|
|
13
|
+
* decision `js/menu.mjs` made in P7a, for the same reason. `cmd_tui` already owns a fourth
|
|
14
|
+
* path: an `except Exception` around `curses.wrapper` that prints `[tui] full-screen panel
|
|
15
|
+
* unavailable (<reason>)` and returns 1, written for the Windows shim's `Unsupported`. That
|
|
16
|
+
* is the reference's own contract for "no full-screen panel is available here", so the port
|
|
17
|
+
* speaks it with a reason that is permanent rather than machine-dependent. A Node panel that
|
|
18
|
+
* differed from the reference on every screen, that no cell could reach, and that would be
|
|
19
|
+
* deleted the day the real one crossed, would be a second user interface and not a port.
|
|
20
|
+
*
|
|
21
|
+
* WHAT DOES CROSS is the alignment contract — the functions below that decide what a row
|
|
22
|
+
* looks like before any curses call is made. They have no caller in this file yet, and that
|
|
23
|
+
* is deliberate and dated: **P7c is the due date**, when `_tui_loop` and the eighteen
|
|
24
|
+
* screens of `_harness_tui_views.py` arrive and consume them. They are here now because they
|
|
25
|
+
* are the half that can be gated exhaustively, and `tests/test_pure_function_parity.py`
|
|
26
|
+
* gates every one of them in both glyph modes. Deleting any of them turns that gate red —
|
|
27
|
+
* this port has shipped code before whose absence nothing would have noticed, and these are
|
|
28
|
+
* not that.
|
|
29
|
+
*
|
|
30
|
+
* WHAT IS DELIBERATELY NOT HERE: `_bx` (its non-ASCII arm returns `curses.ACS_*` chtype ints,
|
|
31
|
+
* which belong with the window), `_wrap_lines` (no consumer until the screens arrive, so
|
|
32
|
+
* porting it now would be a reduced port with no caller to check it against), and every
|
|
33
|
+
* `(stdscr, curses, pal)` drawing helper.
|
|
34
|
+
*
|
|
35
|
+
* **DO NOT WRITE A SECOND LINE BREAKER.** This block used to argue that `textwrap.wrap` was
|
|
36
|
+
* "a second `difflib` — a stdlib payload with no Node twin". It has one since P1:
|
|
37
|
+
* `pyTextWrap` in `js/cli.mjs`, written because `--help` was the caller that finally
|
|
38
|
+
* required it, frozen at `tests/__snapshots__/textwrap.json` and swept against the running
|
|
39
|
+
* interpreter at every width from 11 to 198. When P7c brings `_wrap_lines` across it wraps
|
|
40
|
+
* THAT, and the only open question is the ASCII/glyph width the panel measures in. `_parse_laws`, `load_registry`, `entity_status` and
|
|
41
|
+
* `_tui_inventory` crossed in P6c and live in `js/inventory.mjs`.
|
|
42
|
+
*/
|
|
43
|
+
import path from 'node:path';
|
|
44
|
+
|
|
45
|
+
import { THEMES } from './checkout.mjs';
|
|
46
|
+
import { readJsonMaybe } from './installs.mjs';
|
|
47
|
+
import { pySplitLines } from './lib/udiff.mjs';
|
|
48
|
+
import { pyPrint } from './lib/fs.mjs';
|
|
49
|
+
|
|
50
|
+
// ---- display tiers -------------------------------------------------------------------
|
|
51
|
+
//
|
|
52
|
+
// The reference reads these into MODULE CONSTANTS at import time (`_TUI_ASCII = bool(
|
|
53
|
+
// os.environ.get(...))`), so a process answers one way for its whole life and the corpus
|
|
54
|
+
// runs one probe process per mode rather than monkeypatching. Read once here too, for the
|
|
55
|
+
// same reason. `bool(os.environ.get(x))` and `!!process.env.x` agree on the case that
|
|
56
|
+
// matters: an EMPTY value is not set.
|
|
57
|
+
const TUI_ASCII = !!process.env.GENESEED_TUI_ASCII;
|
|
58
|
+
const TUI_PLAIN = !!process.env.GENESEED_TUI_PLAIN;
|
|
59
|
+
const TUI_EMOJI = !(TUI_ASCII || TUI_PLAIN);
|
|
60
|
+
const TUI_ANIM = TUI_EMOJI;
|
|
61
|
+
|
|
62
|
+
/** `_glyphs` — the one glyph table for the whole TUI, unicode or ASCII stand-ins. */
|
|
63
|
+
export function glyphs(asciiMode) {
|
|
64
|
+
return {
|
|
65
|
+
sel: asciiMode ? '>' : '▸',
|
|
66
|
+
up: asciiMode ? '^' : '▴',
|
|
67
|
+
down: asciiMode ? 'v' : '▾',
|
|
68
|
+
head: asciiMode ? '*' : '◆',
|
|
69
|
+
skill: asciiMode ? '*' : '✦',
|
|
70
|
+
law: asciiMode ? '#' : '§',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const GLYPH = glyphs(TUI_ASCII);
|
|
75
|
+
|
|
76
|
+
// ---- display width -------------------------------------------------------------------
|
|
77
|
+
//
|
|
78
|
+
// `_dwidth` is the one function in this phase with NO equivalent at any rung of the ladder.
|
|
79
|
+
// It calls `unicodedata.east_asian_width` and `unicodedata.combining`; JavaScript has
|
|
80
|
+
// neither. `RegExp` Unicode property escapes carry `\p{General_Category}` and `\p{Script}`
|
|
81
|
+
// but NOT `East_Asian_Width`, and `\p{Mn}|\p{Me}` is not the predicate `combining(ch) != 0`
|
|
82
|
+
// — measured, it disagrees on 897 codepoints across 229 ranges, because a spacing mark is
|
|
83
|
+
// `Mc` with combining class 0 and a great many `Mn` have class 0 too. So the tables below
|
|
84
|
+
// are the port's own, and no dependency was added to get them.
|
|
85
|
+
//
|
|
86
|
+
// A HAND-CARRIED UNICODE TABLE IS THE KIND OF THING THAT IS RIGHT UNTIL IT IS NOT, so the
|
|
87
|
+
// gate over it is not a case list. `tests/test_pure_function_parity.py` walks EVERY
|
|
88
|
+
// codepoint from U+0000 to U+2FFFF on both sides, run-length encodes the widths, and
|
|
89
|
+
// compares the run lists — 196 608 inputs, and a range this table gets wrong has nowhere to
|
|
90
|
+
// hide. The comparison is against Python's LIVE `unicodedata`, never against a copy of it,
|
|
91
|
+
// so the day the reference's Unicode version moves under these tables the gate says so
|
|
92
|
+
// instead of agreeing with itself.
|
|
93
|
+
//
|
|
94
|
+
// THE VERSION IS DECLARED, not left in a comment, and that changed on the day the gate fired
|
|
95
|
+
// for real. `python3.14` ships unidata 16.0.0, which assigned U+0897 a combining class these
|
|
96
|
+
// tables (built from 15.1.0) do not carry — so the sweep went red naming a codepoint, exactly
|
|
97
|
+
// as designed, on a machine whose only difference was the interpreter. That is not a defect
|
|
98
|
+
// in either implementation and it must not read as one: the sweep is a comparison against a
|
|
99
|
+
// SPECIFIC Unicode version, and a comparison is meaningless against a different one. So the
|
|
100
|
+
// version is a constant the test reads, `.github/workflows/ci.yml` pins the interpreter that
|
|
101
|
+
// supplies it, and `tests/test_pure_function_parity.py` asserts the pin and the constant
|
|
102
|
+
// still agree. Regenerating the tables means bumping all three together.
|
|
103
|
+
export const DWIDTH_UNIDATA = '15.1.0';
|
|
104
|
+
|
|
105
|
+
//: East_Asian_Width W or F, below U+1F000 — above it the reference's own `>= 0x1F000` rule
|
|
106
|
+
//: already answers 2, so the table stops where that rule takes over.
|
|
107
|
+
const WIDE = [
|
|
108
|
+
0x1100, 0x115F, 0x231A, 0x231B, 0x2329, 0x232A, 0x23E9, 0x23EC, 0x23F0, 0x23F0,
|
|
109
|
+
0x23F3, 0x23F3, 0x25FD, 0x25FE, 0x2614, 0x2615, 0x2648, 0x2653, 0x267F, 0x267F,
|
|
110
|
+
0x2693, 0x2693, 0x26A1, 0x26A1, 0x26AA, 0x26AB, 0x26BD, 0x26BE, 0x26C4, 0x26C5,
|
|
111
|
+
0x26CE, 0x26CE, 0x26D4, 0x26D4, 0x26EA, 0x26EA, 0x26F2, 0x26F3, 0x26F5, 0x26F5,
|
|
112
|
+
0x26FA, 0x26FA, 0x26FD, 0x26FD, 0x2705, 0x2705, 0x270A, 0x270B, 0x2728, 0x2728,
|
|
113
|
+
0x274C, 0x274C, 0x274E, 0x274E, 0x2753, 0x2755, 0x2757, 0x2757, 0x2795, 0x2797,
|
|
114
|
+
0x27B0, 0x27B0, 0x27BF, 0x27BF, 0x2B1B, 0x2B1C, 0x2B50, 0x2B50, 0x2B55, 0x2B55,
|
|
115
|
+
0x2E80, 0x2E99, 0x2E9B, 0x2EF3, 0x2F00, 0x2FD5, 0x2FF0, 0x303E, 0x3041, 0x3096,
|
|
116
|
+
0x3099, 0x30FF, 0x3105, 0x312F, 0x3131, 0x318E, 0x3190, 0x31E3, 0x31EF, 0x321E,
|
|
117
|
+
0x3220, 0x3247, 0x3250, 0x4DBF, 0x4E00, 0xA48C, 0xA490, 0xA4C6, 0xA960, 0xA97C,
|
|
118
|
+
0xAC00, 0xD7A3, 0xF900, 0xFAFF, 0xFE10, 0xFE19, 0xFE30, 0xFE52, 0xFE54, 0xFE66,
|
|
119
|
+
0xFE68, 0xFE6B, 0xFF01, 0xFF60, 0xFFE0, 0xFFE6, 0x16FE0, 0x16FE4, 0x16FF0, 0x16FF1,
|
|
120
|
+
0x17000, 0x187F7, 0x18800, 0x18CD5, 0x18D00, 0x18D08, 0x1AFF0, 0x1AFF3, 0x1AFF5, 0x1AFFB,
|
|
121
|
+
0x1AFFD, 0x1AFFE, 0x1B000, 0x1B122, 0x1B132, 0x1B132, 0x1B150, 0x1B152, 0x1B155, 0x1B155,
|
|
122
|
+
0x1B164, 0x1B167, 0x1B170, 0x1B2FB,
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
//: Canonical combining class != 0 — `unicodedata.combining(ch)` truthy.
|
|
126
|
+
const COMBINING = [
|
|
127
|
+
0x0300, 0x034E, 0x0350, 0x036F, 0x0483, 0x0487, 0x0591, 0x05BD, 0x05BF, 0x05BF,
|
|
128
|
+
0x05C1, 0x05C2, 0x05C4, 0x05C5, 0x05C7, 0x05C7, 0x0610, 0x061A, 0x064B, 0x065F,
|
|
129
|
+
0x0670, 0x0670, 0x06D6, 0x06DC, 0x06DF, 0x06E4, 0x06E7, 0x06E8, 0x06EA, 0x06ED,
|
|
130
|
+
0x0711, 0x0711, 0x0730, 0x074A, 0x07EB, 0x07F3, 0x07FD, 0x07FD, 0x0816, 0x0819,
|
|
131
|
+
0x081B, 0x0823, 0x0825, 0x0827, 0x0829, 0x082D, 0x0859, 0x085B, 0x0898, 0x089F,
|
|
132
|
+
0x08CA, 0x08E1, 0x08E3, 0x08FF, 0x093C, 0x093C, 0x094D, 0x094D, 0x0951, 0x0954,
|
|
133
|
+
0x09BC, 0x09BC, 0x09CD, 0x09CD, 0x09FE, 0x09FE, 0x0A3C, 0x0A3C, 0x0A4D, 0x0A4D,
|
|
134
|
+
0x0ABC, 0x0ABC, 0x0ACD, 0x0ACD, 0x0B3C, 0x0B3C, 0x0B4D, 0x0B4D, 0x0BCD, 0x0BCD,
|
|
135
|
+
0x0C3C, 0x0C3C, 0x0C4D, 0x0C4D, 0x0C55, 0x0C56, 0x0CBC, 0x0CBC, 0x0CCD, 0x0CCD,
|
|
136
|
+
0x0D3B, 0x0D3C, 0x0D4D, 0x0D4D, 0x0DCA, 0x0DCA, 0x0E38, 0x0E3A, 0x0E48, 0x0E4B,
|
|
137
|
+
0x0EB8, 0x0EBA, 0x0EC8, 0x0ECB, 0x0F18, 0x0F19, 0x0F35, 0x0F35, 0x0F37, 0x0F37,
|
|
138
|
+
0x0F39, 0x0F39, 0x0F71, 0x0F72, 0x0F74, 0x0F74, 0x0F7A, 0x0F7D, 0x0F80, 0x0F80,
|
|
139
|
+
0x0F82, 0x0F84, 0x0F86, 0x0F87, 0x0FC6, 0x0FC6, 0x1037, 0x1037, 0x1039, 0x103A,
|
|
140
|
+
0x108D, 0x108D, 0x135D, 0x135F, 0x1714, 0x1715, 0x1734, 0x1734, 0x17D2, 0x17D2,
|
|
141
|
+
0x17DD, 0x17DD, 0x18A9, 0x18A9, 0x1939, 0x193B, 0x1A17, 0x1A18, 0x1A60, 0x1A60,
|
|
142
|
+
0x1A75, 0x1A7C, 0x1A7F, 0x1A7F, 0x1AB0, 0x1ABD, 0x1ABF, 0x1ACE, 0x1B34, 0x1B34,
|
|
143
|
+
0x1B44, 0x1B44, 0x1B6B, 0x1B73, 0x1BAA, 0x1BAB, 0x1BE6, 0x1BE6, 0x1BF2, 0x1BF3,
|
|
144
|
+
0x1C37, 0x1C37, 0x1CD0, 0x1CD2, 0x1CD4, 0x1CE0, 0x1CE2, 0x1CE8, 0x1CED, 0x1CED,
|
|
145
|
+
0x1CF4, 0x1CF4, 0x1CF8, 0x1CF9, 0x1DC0, 0x1DFF, 0x20D0, 0x20DC, 0x20E1, 0x20E1,
|
|
146
|
+
0x20E5, 0x20F0, 0x2CEF, 0x2CF1, 0x2D7F, 0x2D7F, 0x2DE0, 0x2DFF, 0x302A, 0x302F,
|
|
147
|
+
0x3099, 0x309A, 0xA66F, 0xA66F, 0xA674, 0xA67D, 0xA69E, 0xA69F, 0xA6F0, 0xA6F1,
|
|
148
|
+
0xA806, 0xA806, 0xA82C, 0xA82C, 0xA8C4, 0xA8C4, 0xA8E0, 0xA8F1, 0xA92B, 0xA92D,
|
|
149
|
+
0xA953, 0xA953, 0xA9B3, 0xA9B3, 0xA9C0, 0xA9C0, 0xAAB0, 0xAAB0, 0xAAB2, 0xAAB4,
|
|
150
|
+
0xAAB7, 0xAAB8, 0xAABE, 0xAABF, 0xAAC1, 0xAAC1, 0xAAF6, 0xAAF6, 0xABED, 0xABED,
|
|
151
|
+
0xFB1E, 0xFB1E, 0xFE20, 0xFE2F, 0x101FD, 0x101FD, 0x102E0, 0x102E0, 0x10376, 0x1037A,
|
|
152
|
+
0x10A0D, 0x10A0D, 0x10A0F, 0x10A0F, 0x10A38, 0x10A3A, 0x10A3F, 0x10A3F, 0x10AE5, 0x10AE6,
|
|
153
|
+
0x10D24, 0x10D27, 0x10EAB, 0x10EAC, 0x10EFD, 0x10EFF, 0x10F46, 0x10F50, 0x10F82, 0x10F85,
|
|
154
|
+
0x11046, 0x11046, 0x11070, 0x11070, 0x1107F, 0x1107F, 0x110B9, 0x110BA, 0x11100, 0x11102,
|
|
155
|
+
0x11133, 0x11134, 0x11173, 0x11173, 0x111C0, 0x111C0, 0x111CA, 0x111CA, 0x11235, 0x11236,
|
|
156
|
+
0x112E9, 0x112EA, 0x1133B, 0x1133C, 0x1134D, 0x1134D, 0x11366, 0x1136C, 0x11370, 0x11374,
|
|
157
|
+
0x11442, 0x11442, 0x11446, 0x11446, 0x1145E, 0x1145E, 0x114C2, 0x114C3, 0x115BF, 0x115C0,
|
|
158
|
+
0x1163F, 0x1163F, 0x116B6, 0x116B7, 0x1172B, 0x1172B, 0x11839, 0x1183A, 0x1193D, 0x1193E,
|
|
159
|
+
0x11943, 0x11943, 0x119E0, 0x119E0, 0x11A34, 0x11A34, 0x11A47, 0x11A47, 0x11A99, 0x11A99,
|
|
160
|
+
0x11C3F, 0x11C3F, 0x11D42, 0x11D42, 0x11D44, 0x11D45, 0x11D97, 0x11D97, 0x11F41, 0x11F42,
|
|
161
|
+
0x16AF0, 0x16AF4, 0x16B30, 0x16B36, 0x16FF0, 0x16FF1, 0x1BC9E, 0x1BC9E, 0x1D165, 0x1D169,
|
|
162
|
+
0x1D16D, 0x1D172, 0x1D17B, 0x1D182, 0x1D185, 0x1D18B, 0x1D1AA, 0x1D1AD, 0x1D242, 0x1D244,
|
|
163
|
+
0x1E000, 0x1E006, 0x1E008, 0x1E018, 0x1E01B, 0x1E021, 0x1E023, 0x1E024, 0x1E026, 0x1E02A,
|
|
164
|
+
0x1E08F, 0x1E08F, 0x1E130, 0x1E136, 0x1E2AE, 0x1E2AE, 0x1E2EC, 0x1E2EF, 0x1E4EC, 0x1E4EF,
|
|
165
|
+
0x1E8D0, 0x1E8D6, 0x1E944, 0x1E94A,
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
/** Binary search over a flat [start, end, start, end, …] range table. */
|
|
169
|
+
function inRanges(table, cp) {
|
|
170
|
+
let lo = 0;
|
|
171
|
+
let hi = table.length / 2 - 1;
|
|
172
|
+
while (lo <= hi) {
|
|
173
|
+
const mid = (lo + hi) >> 1;
|
|
174
|
+
if (cp < table[mid * 2]) hi = mid - 1;
|
|
175
|
+
else if (cp > table[mid * 2 + 1]) lo = mid + 1;
|
|
176
|
+
else return true;
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* `_dwidth` — display width of `s` in terminal columns.
|
|
183
|
+
*
|
|
184
|
+
* `for (const ch of s)` iterates by CODE POINT, which is what `for ch in s` does on the
|
|
185
|
+
* reference side; a `for (let i …)` loop over UTF-16 units would count every emoji twice.
|
|
186
|
+
*/
|
|
187
|
+
export function dwidth(s) {
|
|
188
|
+
let w = 0;
|
|
189
|
+
let prev = 0;
|
|
190
|
+
for (const ch of s) {
|
|
191
|
+
if (ch === '️') { // emoji-presentation selector: base becomes wide
|
|
192
|
+
if (prev === 1) { w += 1; prev = 2; }
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (ch === '︎') continue; // text-presentation selector: leave the base as-is
|
|
196
|
+
const cp = ch.codePointAt(0);
|
|
197
|
+
if (inRanges(COMBINING, cp)) { prev = 0; continue; }
|
|
198
|
+
const cw = (cp >= 0x1F000 || inRanges(WIDE, cp)) ? 2 : 1;
|
|
199
|
+
w += cw;
|
|
200
|
+
prev = cw;
|
|
201
|
+
}
|
|
202
|
+
return w;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** `_truncd` — truncate to at most `width` display columns, never splitting a glyph. */
|
|
206
|
+
export function truncd(s, width) {
|
|
207
|
+
if (width <= 0) return '';
|
|
208
|
+
if (dwidth(s) <= width) return s;
|
|
209
|
+
let out = '';
|
|
210
|
+
let w = 0;
|
|
211
|
+
for (const ch of s) {
|
|
212
|
+
const cw = dwidth(ch);
|
|
213
|
+
if (w + cw > width) break;
|
|
214
|
+
out += ch;
|
|
215
|
+
w += cw;
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** `_fit` — truncate to `width` display columns, then pad to exactly `width`. */
|
|
221
|
+
export function fit(s, width) {
|
|
222
|
+
const t = truncd(s, width);
|
|
223
|
+
return t + ' '.repeat(Math.max(0, width - dwidth(t)));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---- glyph tables --------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
const ICONS = {
|
|
229
|
+
browse: ['📖', '▤', '#'],
|
|
230
|
+
diff: ['🔍', '≈', '~'],
|
|
231
|
+
setup: ['🧩', '⊞', '%'],
|
|
232
|
+
theme: ['🎨', '◈', '*'],
|
|
233
|
+
update: ['🔄', '↻', '@'],
|
|
234
|
+
bootstrap: ['🚀', '⇧', '^'],
|
|
235
|
+
build: ['🔨', '⚒', '+'],
|
|
236
|
+
memory: ['🧠', '❖', '&'],
|
|
237
|
+
status: ['📊', '▦', '='],
|
|
238
|
+
settings: ['🔧', '⚙', '%'],
|
|
239
|
+
quit: ['🚪', '✕', 'x'],
|
|
240
|
+
doctor: ['🩺', '✚', '+'],
|
|
241
|
+
mcp: ['🔌', '⊕', '&'],
|
|
242
|
+
link: ['🔗', '∞', '&'],
|
|
243
|
+
unlink: ['🔓', '∝', '-'],
|
|
244
|
+
uninstall: ['🗑', '⊗', 'x'],
|
|
245
|
+
back: ['🔙', '←', '<'],
|
|
246
|
+
agent: ['🤖', '◆', '@'],
|
|
247
|
+
skill: ['✨', '✦', '*'],
|
|
248
|
+
law: ['📜', '§', '#'],
|
|
249
|
+
library: ['📚', '❒', 'L'],
|
|
250
|
+
notebook: ['📓', '✎', 'N'],
|
|
251
|
+
wiki: ['📘', '▥', 'W'],
|
|
252
|
+
config: ['🧾', '⌗', 'C'],
|
|
253
|
+
badge: ['🧬', '⬡', 'G'],
|
|
254
|
+
web: ['🌐', '◍', 'W'],
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/** `_icon` — the icon for `name` in the active display tier. */
|
|
258
|
+
export function icon(name) {
|
|
259
|
+
// `_ICONS.get(name, (...))`, and `Object.hasOwn` rather than a truthiness test so a name
|
|
260
|
+
// like `constructor` cannot reach up the prototype chain and answer with a function.
|
|
261
|
+
const [emoji, sym, asc] = Object.hasOwn(ICONS, name) ? ICONS[name] : ['•', '•', '*'];
|
|
262
|
+
return TUI_ASCII ? asc : (TUI_EMOJI ? emoji : sym);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const MARKS = {
|
|
266
|
+
ok: ['✅', '✓', '+'],
|
|
267
|
+
fail: ['❌', '✗', 'x'],
|
|
268
|
+
warn: ['⚠️', '!', '!'],
|
|
269
|
+
info: ['ℹ️', '·', '-'],
|
|
270
|
+
pending: ['·', '·', '-'],
|
|
271
|
+
edited: ['📝', '~', '~'],
|
|
272
|
+
added: ['🆕', '+', '+'],
|
|
273
|
+
missing: ['🗑', '-', '-'],
|
|
274
|
+
mcp_on: ['🟢', '●', 'x'],
|
|
275
|
+
mcp_off: ['⚪', '○', '~'],
|
|
276
|
+
mcp_absent: ['⚫', '·', ' '],
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/** `_mark` — status glyph for ok/fail/warn/info in the active tier. */
|
|
280
|
+
export function mark(kind) {
|
|
281
|
+
const [emoji, sym, asc] = Object.hasOwn(MARKS, kind) ? MARKS[kind] : ['•', '·', '-'];
|
|
282
|
+
return TUI_ASCII ? asc : (TUI_EMOJI ? emoji : sym);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const SPIN = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏';
|
|
286
|
+
|
|
287
|
+
/** `_spin` — one spinner frame for tick `i`; a static dot in the non-animated tiers. */
|
|
288
|
+
export function spin(i) {
|
|
289
|
+
if (!TUI_ANIM) return TUI_ASCII ? '-' : '·';
|
|
290
|
+
const frames = TUI_ASCII ? '|/-\\' : SPIN;
|
|
291
|
+
// `frames[i % len]` on the reference indexes CODE POINTS; the braille frames are BMP so
|
|
292
|
+
// UTF-16 indexing agrees, but spelling it through `[...frames]` keeps that an argument
|
|
293
|
+
// about the data rather than a coincidence. Python's `%` on a negative `i` returns a
|
|
294
|
+
// non-negative remainder and JavaScript's does not, so the modulo is corrected.
|
|
295
|
+
const f = [...frames];
|
|
296
|
+
return f[((i % f.length) + f.length) % f.length];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const LOGO_FONT = {
|
|
300
|
+
G: [' ### ', '# ', '# ## ', '# # ', ' ### '],
|
|
301
|
+
E: ['#### ', '# ', '### ', '# ', '#### '],
|
|
302
|
+
N: ['# #', '## #', '# # #', '# ##', '# #'],
|
|
303
|
+
S: [' ####', '# ', ' ### ', ' #', '#### '],
|
|
304
|
+
D: ['### ', '# # ', '# #', '# # ', '### '],
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/** `_logo_lines` — the GENESEED wordmark as 5 text rows. */
|
|
308
|
+
export function logoLines() {
|
|
309
|
+
const ink = TUI_ASCII ? '#' : '█';
|
|
310
|
+
const rows = [];
|
|
311
|
+
for (let r = 0; r < 5; r += 1) {
|
|
312
|
+
rows.push([...'GENESEED'].map((c) => LOGO_FONT[c][r].replaceAll('#', ink)).join(' '));
|
|
313
|
+
}
|
|
314
|
+
return rows;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ---- layout maths --------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
/** `_clamp` — clamp a scroll offset so [top, top+viewH) stays inside `total`. */
|
|
320
|
+
export function clamp(top, total, viewH) {
|
|
321
|
+
return Math.max(0, Math.min(top, Math.max(0, total - viewH)));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const BAR_EIGHTHS = ' ▏▎▍▌▋▊▉█';
|
|
325
|
+
|
|
326
|
+
/** `_progress_bar` — a determinate bar exactly `width` display columns wide. */
|
|
327
|
+
export function progressBar(frac, width = 24) {
|
|
328
|
+
const f = Math.max(0.0, Math.min(1.0, frac));
|
|
329
|
+
if (TUI_ASCII) {
|
|
330
|
+
const filled = pyRound(f * width);
|
|
331
|
+
return '#'.repeat(filled) + '-'.repeat(width - filled);
|
|
332
|
+
}
|
|
333
|
+
const eighths = pyRound(f * width * 8);
|
|
334
|
+
const full = Math.floor(eighths / 8);
|
|
335
|
+
const rem = eighths % 8;
|
|
336
|
+
if (full >= width) return '█'.repeat(width);
|
|
337
|
+
return '█'.repeat(full) + [...BAR_EIGHTHS][rem] + ' '.repeat(width - full - 1);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* `round()` — Python rounds HALF TO EVEN and `Math.round` rounds half UP, and this is the
|
|
342
|
+
* one place in the file where that shows: a bar at exactly 0.5 of a cell is the frontier
|
|
343
|
+
* case the eighths resolution exists to draw, and `round(0.5)` is 0 there and 1 here.
|
|
344
|
+
*/
|
|
345
|
+
function pyRound(x) {
|
|
346
|
+
const f = Math.floor(x);
|
|
347
|
+
const d = x - f;
|
|
348
|
+
if (d > 0.5) return f + 1;
|
|
349
|
+
if (d < 0.5) return f;
|
|
350
|
+
return f % 2 === 0 ? f : f + 1;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ---- theme reads ---------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
/** `_theme_preview` — right-panel preview rows for a theme, as (kind, text) pairs. */
|
|
356
|
+
export function themePreview(key) {
|
|
357
|
+
const data = readJsonMaybe(path.join(THEMES, `${key}.json`));
|
|
358
|
+
if (!data || typeof data !== 'object') return [['dim', '(no preview available)']];
|
|
359
|
+
const lines = [['title', key], ['', '']];
|
|
360
|
+
if (data.TAGLINE) lines.push(['dim', data.TAGLINE], ['', '']);
|
|
361
|
+
if (data.LOADED_SIGIL) lines.push(['ok', data.LOADED_SIGIL], ['', '']);
|
|
362
|
+
if (data.VOICE) lines.push(['', `Voice — ${data.VOICE}`], ['', '']);
|
|
363
|
+
if (data.LEX_I) lines.push(['', `e.g. Rule I — ${data.LEX_I}`]);
|
|
364
|
+
if (data.BENEDICTION) lines.push(['', ''], ['dim', data.BENEDICTION]);
|
|
365
|
+
return lines;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** `_theme_flair` — the voice elements the setup chrome speaks in once a theme is chosen. */
|
|
369
|
+
export function themeFlair(theme) {
|
|
370
|
+
const data = readJsonMaybe(path.join(THEMES, `${theme}.json`)) || {};
|
|
371
|
+
const banner = data.BANNER ?? '';
|
|
372
|
+
return {
|
|
373
|
+
accent: data.ACCENT ?? 'cyan',
|
|
374
|
+
tagline: data.TAGLINE ?? '',
|
|
375
|
+
sigil: data.LOADED_SIGIL ?? '',
|
|
376
|
+
// `str.splitlines()` — `js/lib/udiff.mjs`' twin, already gated. It answers `[]` for
|
|
377
|
+
// the empty string where `''.split('\n')` answers `['']`, which would put a blank row
|
|
378
|
+
// in the banner of every theme that omits one; and its boundary set is Python's rather
|
|
379
|
+
// than `\s`, which P6d measured apart.
|
|
380
|
+
banner: pySplitLines(banner),
|
|
381
|
+
benediction: data.BENEDICTION ?? '',
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ---- the inventory's two consumers ----------------------------------------------------
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* `_tui_entries` — the ordered (kind, label, data) rows for the left list. `head` is a
|
|
389
|
+
* section divider and is not selectable.
|
|
390
|
+
*/
|
|
391
|
+
export function tuiEntries(inv) {
|
|
392
|
+
const rows = [['head', `AGENTS (${inv.agents.length})`, null]];
|
|
393
|
+
for (const e of inv.agents) rows.push(['agent', e.name, e]);
|
|
394
|
+
rows.push(['head', `SKILLS (${inv.skills.length})`, null]);
|
|
395
|
+
for (const e of inv.skills) rows.push(['skill', e.name, e]);
|
|
396
|
+
rows.push(['head', `LAWS (${inv.laws.length})`, null]);
|
|
397
|
+
for (const e of inv.laws) rows.push(['law', `Rule ${e.num} — ${e.title}`, e]);
|
|
398
|
+
return rows;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* `_detail_lines` — right-pane content for the selected entry.
|
|
403
|
+
*
|
|
404
|
+
* The reference falls off the end for every other `kind`, so it returns `None` — not `[]`.
|
|
405
|
+
* `null` here, because a caller that treated the two alike would render an empty pane where
|
|
406
|
+
* the reference renders nothing at all.
|
|
407
|
+
*
|
|
408
|
+
* ⚠ A REFERENCE BUG LIVES ON THIS RETURN, FOUND BY P7B'S CORPUS AND DELIBERATELY NOT FIXED
|
|
409
|
+
* (it is in the panel, which is P7c's). `_tui_loop` does, at `_harness_tui.py`:614:
|
|
410
|
+
*
|
|
411
|
+
* wrapped = _wrap_lines(_detail_lines(kind, label, data), riw)
|
|
412
|
+
*
|
|
413
|
+
* and `_wrap_lines` iterates its argument. For a `head` row `_detail_lines` returns `None`,
|
|
414
|
+
* so that is a `TypeError: 'NoneType' object is not iterable`. It is normally unreachable
|
|
415
|
+
* because line 554 moves `sel` onto the first SELECTABLE row — but line 555 is
|
|
416
|
+
* `if not selectable: sel = 0`, and row 0 is always the AGENTS header. So an inventory with
|
|
417
|
+
* no agents, no skills AND no laws crashes the panel on its first frame instead of drawing
|
|
418
|
+
* an empty state. `_TUI_INV_EMPTY` in `tests/test_pure_function_parity.py` is the corpus
|
|
419
|
+
* entry that surfaced it. **P7c must not reproduce the crash when it ports `_tui_loop`** —
|
|
420
|
+
* the fix is one `or []` at the call site, and it belongs in the same change as the port.
|
|
421
|
+
*/
|
|
422
|
+
export function detailLines(kind, label, data) {
|
|
423
|
+
if (kind === 'law') return [label, ''].concat(data ? pySplitLines(data.body) : []);
|
|
424
|
+
if ((kind === 'agent' || kind === 'skill') && data) return pySplitLines(data.body);
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ---- the verb ------------------------------------------------------------------------
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* `cmd_tui`. Arm one is the reachable one and is byte for byte; the panel is P7c's and this
|
|
432
|
+
* falls back through `cmd_tui`'s own `except` arm — see this module's header.
|
|
433
|
+
*/
|
|
434
|
+
export function cmdTui() {
|
|
435
|
+
if (!process.stdin.isTTY) {
|
|
436
|
+
pyPrint('[tui] not an interactive terminal. Use `harness setup`, `doctor`, or `build`.\n');
|
|
437
|
+
return 1;
|
|
438
|
+
}
|
|
439
|
+
// The pointer to the Python panel is gone for `js/menu.mjs`'s reason: the
|
|
440
|
+
// panel it named is the Python being deleted. What is left is true and stays true.
|
|
441
|
+
pyPrint('[tui] full-screen panel unavailable (this entry carries the TUI\'s layout '
|
|
442
|
+
+ 'half, not its screens). Use `harness setup`, `doctor`, or `build`.\n');
|
|
443
|
+
return 1;
|
|
444
|
+
}
|