Skip to main content

tui

Build, review, or debug OpenTUI terminal UIs — React JSX or core constructs, Yoga flexbox, gradient meters, colour screenshots. Any Bun/TypeScript TUI. Only Bun is tested; Node/Deno at own risk.

معلومات المصدر

المستودع
MadAppGang/magus
آخر نشاط في المصدر
٢٦ سبتمبر ٢٠٢٦ في ١٤:٣٦
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
١١
التفرعات
٤

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

مستكشف الملفات
67 ملفات

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
tui
description
Build, review, or debug OpenTUI terminal UIs — React JSX or core constructs, Yoga flexbox, gradient meters, colour screenshots. Any Bun/TypeScript TUI. Only Bun is tested; Node/Deno at own risk.
disable-model-invocation
true
# OpenTUI TUI Build Bun/TypeScript terminal UIs that look like **btop, gonzo, posting and lazygit** — colourful, dense, graph-and-badge-heavy — not like `cat` output. OpenTUI hands you a real layout engine (Yoga flexbox) and a React renderer, and **no** meter, chart, badge or colour maths, so under-baked is the *default* outcome — which the contract below and the shipped `assets/` prevent. You cannot judge a look from source: **build → screenshot in colour → look → fix**. ## The aesthetic this skill is for The bar is **more colour, more graphs, less text**; the failure mode is not ugliness but *under-baking* — printing `CPU 78%` where the row could carry a meter, a percentage and a sparkline. Reach for the rich form mechanically. | You have… | Default to… | Not… | |---|---|---| | a bounded value (a percentage — **`pct` is 0–100, never 0–1**) | a **gradient meter** — one colour per cell across the fill | a bare number, or a flat one-colour bar | | a time series / recent history | a **sparkline** (`▁▂▃▄▅▆▇█`) | the latest number alone | | a category distribution | a **bar chart** or stacked bar, coloured per category | a list of `name: count` lines | | counts over time × category | a **heat row** per category, brightness = magnitude | a table of numbers | | a discrete status / level / method | a **badge** — dark ink on a saturated fill | plain coloured text alone | | genuine free narrative (logs, detail) | text — **and only inside a log or detail panel** | text as the main surface | Three rules keep it from tipping into noise. **Colour is semantic, never decorative** — one colour means one thing app-wide; red as "error" here and "selected" there kills glanceability. **Make gradients smooth** — one blended colour per column at the widget's real width, never four `░▒▓█` glyphs sharing one. **Dim the chrome, saturate the signal.** **Negative control: if your first screenshot shows a single-colour bar or bare numbers, you failed** — as is a non-log panel over about half prose or numerals. Then run that count a **second time over the WHOLE frame, not per panel** (graphics body rows vs text rows across the capture): every panel can pass the table while the screen still reads as prose — one build's first wide capture was 28 of 38 rows text, every panel compliant. Under half, add a graphics panel. ## What OpenTUI does not ship — and how to get it No meter, gauge, progress bar, sparkline, chart, heatmap or badge exists in the library, nor the colour maths they need — no `lerp`/`mix`/`blend`/`gradient`/`darken`/`rgbToHsv` anywhere in `packages/core/src`, **no string-width helper at all**, and `setCellWithAlphaBlending` is *compositing*, not interpolation. That layer ships here as tested code. **Copy it in; never retype it** — the 0.80 heat floor, 24 ramp steps and `>128` ink threshold degrade silently. ```bash SKILL="${CLAUDE_PLUGIN_ROOT:+$CLAUDE_PLUGIN_ROOT/skills/tui}"; SKILL="${SKILL:-PASTE_THE_DIR_THIS_SKILL_MD_WAS_READ_FROM}" # that var is UNSET in a Bash tool (MEASURED) — so paste. `:?` would ABORT the block and copy nothing bun init -y # FROM EMPTY this is step 1: package.json, typescript + @types/bun — `tsc` and `"types": ["bun"]` below need both. Creates NO src/. It ALSO writes a tsconfig.json you MUST overwrite with the block below — its defaults carry no `jsxImportSource`, so nothing renders bun add @opentui/core @opentui/react react && bun add -d @types/react # the shims import from core. UNPINNED, so you get `latest` — 0.5.1 as of 2026-08-06, not the 0.4.5 this skill's own package.json pins. Both run clean; only the `--compile` row cares (versions-and-builds.md) mkdir -p src; if [ -d "$SKILL/assets" ]; then cp -r "$SKILL/assets/theme" src/theme && cp -r "$SKILL/assets/runtime" src/runtime # `cp -r` needs src/; copy BOTH — theme = tokens/color/text/widgets + 2 tests, runtime = shutdown + test + env.d.ts else echo "SKILL is still the placeholder — paste the real dir above, re-run these 2 lines; the install already ran"; fi ``` Three test files ship — two under `theme/`, `shutdown.test.ts` under `runtime/` — so copying only `theme/` leaves the teardown suite behind; `color.ts` imports `./tokens`, `widgets.tsx` imports both, the tests all four, and `opentui-env.d.ts` must fall inside your `tsconfig.json` `include`. **Leave the relative imports as they are** — in `src/theme/` they resolve untouched (MEASURED in a bare `bun init`: `diff -qr` identical, `tsc` clean, 119/119 pass); `@/theme/…` is 9×TS2307. ## Two API surfaces — never blend them > **INVARIANT.** Never mix the core **construct DSL** (`Text({…})`, `Box({…}, child)`, `new XRenderable(ctx, opts)`) > with **React intrinsic rendering** (`<text>`, `<box>`, `<span>`) in the same snippet, file or fenced block — such > code compiles under neither. **Importing from `@opentui/core` is not a violation.** Calling its DSL beside JSX is. **React JSX is the canonical surface**: lowercase intrinsics, flat props, children as children — **zero** capitalised renderables across three shipping apps. Capitalised in JSX means *your own* component; multi-word intrinsics are **kebab-case** (`<tab-select>`, `<ascii-font>`, `<line-number>`), `scrollbox` is one word, and Solid's underscore spellings **do not compile in React.** An invented kebab tag typechecks clean against a string index signature, so the compiler cannot catch a surface error — run the linter (**Acceptance**). ## Bootstrap: zero to first frame `bun create tui --template react` scaffolds this; by hand it is two files, meeting in `src/index.tsx` as function calls, never constructs. Call `process.exit()` nowhere else — a crash that skipped `destroy()` leaves the terminal in raw mode inside the alternate screen, recovered with `reset` or `stty sane`. ```tsx import { createCliRenderer } from "@opentui/core"; import { createRoot } from "@opentui/react" import { installShutdown } from "./runtime/shutdown"; import { App } from "./App" // never `OWNED_SIGNALS` const renderer = await createCliRenderer({ screenMode: "alternate-screen", // full-screen app; "main-screen" stays in the scrollback exitSignals: [], // BOTH, so the renderer registers NOTHING and installShutdown is the sole owner. exitOnCtrlC: false, // MEASURED under a real SIGTERM — `[]`: disposer→unmount→destroy, ONE destroy; }) // `["SIGTERM"]`: destroy→disposer→unmount→destroy, TWO, the tree torn down last const root = createRoot(renderer); const shutdown = installShutdown(renderer, root) // disposers → unmount → destroy → stderr → 50ms flush → exit(128+signum); 0 only on a clean quit // Ctrl+C IS NOT A SIGNAL HERE: raw mode clears ISIG, so the tty hands you 0x03 and SIGINT is never raised. Deleting renderer.keyInput.on("keypress", (k) => { if (k.ctrl && k.name === "c") void shutdown() }) // this makes Ctrl+C INERT, not redundant root.render(<App onQuit={shutdown} />) // installShutdown's own SIGINT handler still catches `kill -INT` from outside the tty ``` ```jsonc // tsconfig.json — complete and copy-pasteable, the keys the tested skill root uses. BOTH jsx keys or nothing renders; // "target"+"module" are REQUIRED beside moduleResolution "bundler" (omit either → TS5095, not a UI); no "DOM" in lib // (span/b/i/u/a collide with HTML tags); skipLibCheck else 16x bun:ffi TS2307. { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "@opentui/react", "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", "lib": ["ESNext"], "types": ["bun"], "strict": true, "noEmit": true, "skipLibCheck": true }, "include": ["src/**/*"] } ``` ## Filling the terminal: header / body / footer The React root does **not** inherit the terminal's height — omit `height` and the tree collapses to content, floating the footer. Pin it from `useTerminalDimensions()`; one region claims the leftover with `flexGrow={1} minHeight={0}`: ```tsx const { width, height } = useTerminalDimensions(); return ( <box flexDirection="column" height={height}> {/* root: pin height; nothing else grows */} <box height={1} paddingLeft={1}><text>{`svcmon ${width}x${height}`}</text></box> <box flexDirection="row" flexGrow={1} minHeight={0}> {/* body: every row the chrome left over */} <box flexDirection="column" flexGrow={1} minWidth={0}> <Panel title="Load" flexShrink={0}>{/* content-sized: flexShrink={0} or the grower below starves it */}</Panel> <Panel title="Errors/min" flexGrow={1} flexBasis={0} minHeight={0}>{/* eats the leftover; scrollbox inside */}</Panel> </box> <box width={22}><Panel title="Services" flexShrink={0}>{/* fixed-width side column */}</Panel></box> </box> <box height={1} paddingLeft={1}><text>q quit · tab focus</text></box> </box> ) ``` **Sizing is a call-site concern, chrome is not.** `<Panel>` accepts every layout prop a parent legitimately owns (`PanelLayout`: growth, size, `min`/`max`, `alignSelf`, `margin*`) and nothing that changes appearance — a shipped test asserts a call site cannot override `borderStyle` or the colours. MEASURED at 80×24: `flexGrow={1} minHeight={0}` paints 18 rows and the footer holds row 24 — **but only while the grower's own child is content-sized.** **A grower holding a `<scrollbox>` starves its SIBLINGS**, because a scrollbox's intrinsic height is its entire content: the panel asks for 40 rows of a 22-row body, and Yoga's default `flexShrink: 1` spreads that shortfall across *every* sibling. MEASURED on this snippet at 80×24, 40 lines in `Errors/min`: `Load` collapsed from five rows to one, its five `<text>` children overprinting into `iot` — which reads as a broken widget, not a layout mistake. Cure it with props `PanelLayout` already exposes: **`flexShrink={0}` on every content-sized panel, `flexBasis={0}` on the grower** (either alone fixed the measured case; write both). This is `react-patterns.md`'s height starvation one level up — the same last-wins collapse, inflicted by a sibling rather than by the box's own `height`. **The leftover-eating panel must be a `<scrollbox>` stocked deeper than the tallest viewport it will ever occupy.** `flexGrow={1}` grants rows, it does not fill them — a grower holding five rows paints five and leaves the rest black, the unpainted hole a screenshot fails you for. MEASURED: 30 rows filled that side panel at 80×24 and still left a gap at 145×45, so stock for the WIDE capture — which is also what keeps the screen-wide density count passing at 40 rows. ## Core workflow 1. **New project → the bootstrap above; existing project → follow its pin** — read the installed `.d.ts`, not the docs site (which names no version), and do not migrate 0.1.x ↔ 0.4.x unless asked. 2. **Choose one surface and stay on it**: React unless the app has none (`core-api.md` owns the imperative surface). 3. **Flexbox for boxes, arithmetic for widget widths.** Yoga accounts for border and padding, so *heights*, panel placement and growth need no size budget — `gap`, `minWidth`, `flexWrap`, `flexGrow`, `flexBasis` all work (undocumented). But the data widgets take a **numeric `width`** that cannot `flexGrow`, so a full-width visual IS a column budget: **`Panel` costs 4 columns of chrome, a `<scrollbox>` inside it 1 more** (both MEASURED; the probe and the resample rule: `react-patterns.md`). Narrower than the Bubble Tea reflex, not a return to it — heights are flexbox, widths of data widgets are arithmetic. 4. **Build each visual as one row of styled text**, imported rather than retyped with hex literals: `Meter`, `Sparkline`, `HeatRow`, `StackedBar`, `Panel`, `Badge` and `BadgeSpan` (the span form, for a badge sharing a `<text>`) all ship in `assets/theme/widgets.tsx`. Never reimplement them; 2D only → `components-and-charts.md`. 5. **Screenshot in colour and look** (below), then **test behaviour and appearance** with `testRender`: `captureCharFrame()` for text, `captureSpans()` for colour — no args, and `renderOnce()` first or every cell is filler. ## Shipping it: pick your pin by artifact **Run from source, or a plain npm package → 0.4.x or 0.5.x.** An unpinned `bun add` gives **0.5.1** today (2026-08-06); 0.4.5 and 0.5.1 both run clean, so take the default unless you need the compile row. **A standalone `bun build --compile` binary → 0.1.107, and no `--external`** — it leaves the binary unable to resolve the module. The costliest mistake is `bun add @opentui/core@latest` then `--compile`, so **re-test 0.4.x `--compile` before accepting that downgrade**: MEASURED 2026-07-30, 0.4.5 compiles fine and the *binary* exits 1 resolving its native library inside `/$bunfs/root/` — upstream bug, not design, and 0.1.x is abandoned. Nothing cross-compiles. **Only Bun is tested**; Node/Deno, the matrix and the `--external` trap: `versions-and-builds.md`. ## Seeing your UI: the screenshot loop A plain text capture strips the colour that *is* the point; this turns a **running app into a colour-accurate PNG** you can `Read`. **Prerequisites: `aha` (`brew install aha` / `apt-get install aha`), a Chromium-family browser and tmux.** ```bash OUT=$(mktemp -d); SOCK=otui-$$; SESS=tui-$$ # unique per run: a fixed dir, socket or session collides with a parallel capture SKILL="${CLAUDE_PLUGIN_ROOT:+$CLAUDE_PLUGIN_ROOT/skills/tui}"; SKILL="${SKILL:-PASTE_THE_DIR_THIS_SKILL_MD_WAS_READ_FROM}" # as above shot() { local A="$OUT/${1}x${2}.ansi" # ${1}x${2} cells → a ${3}-pixel PNG. `-f /dev/null` on EVERY invocation, never just tmux -f /dev/null -L "$SOCK" new-session -d -s "$SESS" -x "${1}" -y "${2}" "bun run src/index.tsx" # new-session: a .tmux.conf for _ in $(seq 40); do sleep 0.25; tmux -f /dev/null -L "$SOCK" capture-pane -p -e -t "$SESS" >"$A" 2>/dev/null && grep -q $'\x1b' "$A" && break; done # that auto-creates sessions spawns ALL of them, tmux -f /dev/null -L "$SOCK" kill-window -t "$SESS" 2>/dev/null # and a client whose server died starts one that reads it. Poll for a COLOURED frame — a fixed sleep races the recreate grep -q $'\x1b' "$A" || { echo "NO ESC BYTES in $A — no -e, or it never drew. STOP: render nothing, conclude nothing"; return 1; } # a gate EXITS; an echo does not bun run "$SKILL/scripts/ansi-to-png.ts" "$A" "$OUT/${1}x${2}.png" "${3}" # colour census (`grep -o … | sort -u`): screenshot-workflow.md } shot 80 24 720x480 && shot 145 45 1300x900 && ls -l "$OUT"/*.png # BOTH sizes in one command — the wide one is not optional ```
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub