Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 45 additions & 18 deletions template/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,17 +199,42 @@ a subscription callback, or `Promise.resolve().then(() => sig.set(…))`.
`"fit"`, `"50vw"`, `Size.min/max/clamp/add/sub(...)`. **Frame the root with
`1fr` or a fixed size or the tree collapses to 0.**

**Color & faces** — combinators wrap a string (innermost wins):
`fg(color)`, `bg(color)`, `bold`, `dim`, `italic`, `underline`, `reverse`,
`strike`. Colors: `idx(0..255)`, `rgb(0xRRGGBB)` / `rgb(r,g,b)`,
`rgba(...,a)`, `color("#ff0080")`, `DEFAULT`.
**Color & faces** — a `<Text>`'s bare attributes *are* its face: `fg`, `bg`,
and the boolean SGR flags `bold`/`dim`/`italic`/`underline`/`reverse`/`strike`.
Colors: a hex string anywhere (`"#ff0080"`, `"#f08"`, `"#ff0080cc"`), or
`idx(0..255)`, `rgb(0xRRGGBB)` / `rgb(r,g,b)`, `rgba(...,a)`, `DEFAULT`.

```jsx
<Text>{() => bold(fg(idx(2))(`${pct(frac.get())}`))}</Text>
<Text bold fg={idx(2)}>{() => pct(frac.get())}</Text>
```

**Uniform style → bare attrs. Per-span → nest. Runtime-computed → `face()`.**
Bare attrs face the *whole* Text, so a line with per-span colors nests `<Text>`
runs as children — the inner face merges over the outer, so it wins:

```jsx
<Text>
<Text fg={out}>↑</Text>
<Text fg={idx(8)}>{n}</Text>
<Text fg={role}>{name}</Text>
</Text>
```

When the face itself is computed at runtime, `face(patch)` applies a patch
object to content — the programmatic form behind `<Text>`, and the escape hatch
when bare attrs can't carry a dynamic value:

```jsx
import { face } from "yeet:tui";
<Text>{() => face({ fg: heat(frac.get()), bold: frac.get() > 0.9 })(label)}</Text>
```

(There's also a separate `style.red(s)` / `style.bold(s)` global — that's for
raw `tty.write` line-mode tools, *not* the JSX tree. Use face combinators here.)
raw `tty.write` line-mode tools, *not* the JSX tree.)

> The named combinators `fg(c)(s)` / `bold(s)` / `dim(s)` … are **deprecated**,
> kept only for back-compat. Reach for bare attrs, nested `<Text>`, or
> `face(patch)` instead.

## Data sources

Expand Down Expand Up @@ -296,7 +321,9 @@ writes atomically. `yeet.exit()` tears the script down.
3. **Set-during-render throws** — defer signal writes out of the render path.
4. **`column` is the default Box direction** (Yoga, not CSS) — set
`direction="row"` explicitly for horizontal.
5. **Use `fg`/`bg`/`bold`** in the tree — never `color`/`style`/`backgroundColor`.
5. **Style with bare `<Text>` attrs** (`fg`/`bg`/`bold`/…) — never
`color`/`style`/`backgroundColor`, and not the deprecated `fg(c)(s)`/`bold(s)`
combinators (use `face(patch)` when the style is computed at runtime).
6. **Ring-buffer events are wrapped** under the `btf_struct` name — unwrap with
`w?.<struct> ?? w`.
7. **`@/` and `#/` are bundle-time only** — the runtime resolver doesn't know
Expand Down Expand Up @@ -333,7 +360,7 @@ widths read `frac`, so each fill is a thunk child that re-mints its Box when

```jsx
// components/gauge.jsx
import { Box, Text, bold, fg, idx } from "yeet:tui";
import { Box, Text, idx } from "yeet:tui";

const RAIL = idx(238);
const heat = (f) => (f < 0.6 ? idx(2) : f < 0.85 ? idx(3) : idx(1));
Expand All @@ -343,10 +370,10 @@ const lpad = (s, n) => `${s}`.padStart(n);
export default function Gauge({ frac, label }) {
return (
<Box height="1" direction="row">
<Text width="8">{fg(idx(244))(label)}</Text>
<Text width="8" fg={idx(244)}>{label}</Text>
{() => <Box width={`${1 + Math.round(frac.get() * 998)}fr`} bg={heat(frac.get())} />}
{() => <Box width={`${1 + Math.round((1 - frac.get()) * 998)}fr`} bg={RAIL} />}
<Text width="5">{() => bold(lpad(pct(frac.get()), 5))}</Text>
<Text width="5" bold>{() => lpad(pct(frac.get()), 5)}</Text>
</Box>
);
}
Expand Down Expand Up @@ -390,7 +417,7 @@ export const cpuPct = computed(() => Math.round(cpu.get() * 100));

```jsx
// main.jsx
import { Box, Text, bold, mount } from "yeet:tui";
import { Box, Text, mount } from "yeet:tui";
import { cpu } from "@/probes/sysload.js";
import Gauge from "@/components/gauge.jsx";

Expand All @@ -400,7 +427,7 @@ tty.on("keydown", (e) => {

const Root = () => (
<Box>
<Text height="1">{bold(" sysload — q to quit")}</Text>
<Text height="1" bold>{" sysload — q to quit"}</Text>
<Box height="1fr" overflow="hidden">
<Gauge frac={cpu} label="cpu" />
</Box>
Expand Down Expand Up @@ -557,7 +584,7 @@ The other egress: the kernel aggregates into an array map, JS just reads slots.

```jsx
// components/histogram.jsx — `latency` is a signal of per-bucket counts
import { Box, Text, fg, idx } from "yeet:tui";
import { Box, Text, idx } from "yeet:tui";

const BARS = "▁▂▃▄▅▆▇█";
const lo = (i) => (i === 0 ? 0 : 1 << (i - 1)); // log2 bucket lower bound (ns)
Expand All @@ -571,7 +598,7 @@ export default function Histogram({ latency }) {
return slots.map((n, i) => (
<Text height="1">
{`${String(lo(i)).padStart(12)}ns `}
{fg(idx(4))(BARS[Math.min(7, Math.floor((n / peak) * 7.99))].repeat(Math.ceil((n / peak) * 40)))}
<Text fg={idx(4)}>{BARS[Math.min(7, Math.floor((n / peak) * 7.99))].repeat(Math.ceil((n / peak) * 40))}</Text>
{` ${n}`}
</Text>
));
Expand Down Expand Up @@ -637,7 +664,7 @@ The crash screen itself is just a `bg`-filled box — no special API:

```jsx
// components/bsod.jsx
import { Box, Text, bold, fg, idx } from "yeet:tui";
import { Box, Text, idx } from "yeet:tui";

const BLUE = idx(20);
const WHITE = idx(15);
Expand All @@ -646,11 +673,11 @@ export default function Bsod({ error }) {
const lines = String(error?.stack ?? error?.message ?? error).split("\n");
return (
<Box bg={BLUE} width="1fr" height="1fr" padding={2}>
<Text height="1">{bold(fg(WHITE)(":( your dashboard hit an error"))}</Text>
<Text height="1" bold fg={WHITE}>{":( your dashboard hit an error"}</Text>
<Text height="1">{" "}</Text>
{lines.map((l) => <Text height="1">{fg(WHITE)(l)}</Text>)}
{lines.map((l) => <Text height="1" fg={WHITE}>{l}</Text>)}
<Text height="1">{" "}</Text>
<Text height="1">{fg(idx(250))("press q to quit")}</Text>
<Text height="1" fg={idx(250)}>{"press q to quit"}</Text>
</Box>
);
}
Expand Down
18 changes: 8 additions & 10 deletions template/src/components/detail.jsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
// The selected CPU's recent context switches, streamed live: prev -> next
// with the outgoing task's on-CPU slice. Reads `cpus` + `selected`.
import { Box, Text, bold, fg, idx } from "yeet:tui";
import { Box, Text, idx } from "yeet:tui";
import { fmtDuration, lpad, pad } from "@/lib/format.js";

export default ({ cpus, selected, rows }) => (
<Box>
<Text height="1">
{() => bold(fg(idx(214))(` ▸ CPU ${selected.get()}`))}
<Text height="1" bold fg={idx(214)}>
{() => ` ▸ CPU ${selected.get()}`}
</Text>
<Box>
{() => {
const cpu = cpus.get()[selected.get()];
const recent = cpu ? cpu.recent : [];
const n = Math.max(1, rows);
if (!recent.length) return [<Text height="1">{fg(idx(240))(" (idle — no switches in window)")}</Text>];
if (!recent.length) return [<Text height="1" fg={idx(240)}>{" (idle — no switches in window)"}</Text>];
return recent.slice(0, n).map((s) => (
<Text height="1" break="none">
{[
fg(idx(252))(` ${pad(s.prev, 16)}`),
fg(idx(240))(" → "),
fg(idx(45))(pad(s.next, 16)),
fg(idx(244))(lpad(fmtDuration(s.slice), 8)),
]}
<Text fg={idx(252)}>{` ${pad(s.prev, 16)}`}</Text>
<Text fg={idx(240)}>{" → "}</Text>
<Text fg={idx(45)}>{pad(s.next, 16)}</Text>
<Text fg={idx(244)}>{lpad(fmtDuration(s.slice), 8)}</Text>
</Text>
));
}}
Expand Down
6 changes: 3 additions & 3 deletions template/src/components/footer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
// strips as trailing break-whitespace). Each shortcut is a raised key-cap —
// the key glyph in bold gold on a tile a shade lighter than the rail —
// followed by a dimmed label, so the keys pop out of the line.
import { Box, Text, bg, bold, fg, idx } from "yeet:tui";
import { Box, Text, idx } from "yeet:tui";

const RAIL = idx(235); // hint-rail background
const CAP = idx(238); // key-cap tile, a shade lighter than the rail
const GLYPH = idx(222); // bright gold key text
const LABEL = idx(247); // dimmed description

const hint = (keys, label) => [
bg(CAP)(bold(fg(GLYPH)(` ${keys} `))),
fg(LABEL)(` ${label} `),
<Text bg={CAP} bold fg={GLYPH}>{` ${keys} `}</Text>,
<Text fg={LABEL}>{` ${label} `}</Text>,
];

export default () => (
Expand Down
13 changes: 8 additions & 5 deletions template/src/components/heatmap.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@
// brighter than quiet ones. `gridCols`/`maxRows` come from the responsive
// layout: it renders the most recent `gridCols` of history and, when there
// are more cores than rows, windows the list to keep the selection in view.
import { Box, Text, bold, fg, idx } from "yeet:tui";
import { Box, Text, idx } from "yeet:tui";
import { heat } from "@/lib/format.js";

const HEADER = " cores × time (newest → right) brightness = switch rate";

export default ({ cpus, selected, gridCols, maxRows }) => (
<Box>
<Text height="1">
<Text height="1" fg={idx(244)}>
{() => {
const total = cpus.get().length;
const shown = Math.min(maxRows, total);
return fg(idx(244))(total > shown ? `${HEADER} · ${total} cores` : HEADER);
return total > shown ? `${HEADER} · ${total} cores` : HEADER;
}}
</Text>
<Box>
Expand All @@ -39,10 +39,13 @@ export default ({ cpus, selected, gridCols, maxRows }) => (
const label = `${on ? "▸" : " "} cpu ${String(i).padStart(2)} `;
const cells = c.hist
.slice(-gridCols)
.map((v) => fg(heat(v <= 0 ? 0 : Math.sqrt(v / peak)))("█"));
.map((v) => <Text fg={heat(v <= 0 ? 0 : Math.sqrt(v / peak))}>{"█"}</Text>);
const head = on
? <Text bold fg={idx(231)}>{label}</Text>
: <Text fg={idx(245)}>{label}</Text>;
return (
<Text height="1" break="none">
{[on ? bold(fg(idx(231))(label)) : fg(idx(245))(label), ...cells]}
{[head, ...cells]}
</Text>
);
});
Expand Down
14 changes: 6 additions & 8 deletions template/src/components/histogram.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Run-queue latency as a log2 histogram. Reads the polled `latency` signal
// (cumulative counts per bucket) and draws a bar per bucket, the label
// being the bucket's lower bound.
import { Box, Text, fg, idx } from "yeet:tui";
import { Box, Text, idx } from "yeet:tui";
import { fmtDuration, lpad } from "@/lib/format.js";

const LO = 8; // 2^8 ns = 256ns
Expand All @@ -10,8 +10,8 @@ const BARW = 16;

export default ({ latency, maxRows }) => (
<Box>
<Text height="1">{fg(idx(244))(" runqueue latency")}</Text>
<Text height="1">{fg(idx(244))(" (wakeup → on-cpu)")}</Text>
<Text height="1" fg={idx(244)}>{" runqueue latency"}</Text>
<Text height="1" fg={idx(244)}>{" (wakeup → on-cpu)"}</Text>
<Box>
{() => {
const slots = latency.get();
Expand All @@ -36,11 +36,9 @@ export default ({ latency, maxRows }) => (
const w = Math.round((n / peak) * BARW);
rows.push(
<Text height="1" break="none">
{[
fg(idx(244))(`${lpad(fmtDuration(2 ** i), 7)} `),
fg(idx(75))("█".repeat(w)),
fg(idx(240))(n ? ` ${n}` : ""),
]}
<Text fg={idx(244)}>{`${lpad(fmtDuration(2 ** i), 7)} `}</Text>
<Text fg={idx(75)}>{"█".repeat(w)}</Text>
<Text fg={idx(240)}>{n ? ` ${n}` : ""}</Text>
</Text>,
);
}
Expand Down
18 changes: 8 additions & 10 deletions template/src/components/procs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,24 @@
// heatmap's inferno ramp; the label is the command and its share of total CPU
// time. Only switches the kernel emitted are counted, so sub-`min-slice` tasks
// don't show — it tracks CPU hogs, not every wakeup.
import { Box, Text, bold, fg, idx } from "yeet:tui";
import { Box, Text, idx } from "yeet:tui";
import { heat, lpad, pad } from "@/lib/format.js";

const NAMEW = 15; // command column width
const PCTW = 7; // "100.0%" + a space

export default ({ procs, width, maxRows }) => (
<Box>
<Text height="1">
{fg(idx(244))(" top processes · on-cpu time (per 500ms window)")}
<Text height="1" fg={idx(244)}>
{" top processes · on-cpu time (per 500ms window)"}
</Text>
<Box>
{() => {
const list = procs.get();
const barW = Math.max(6, width - NAMEW - PCTW - 3);
const shown = list.slice(0, maxRows);
if (!shown.length) {
return [<Text height="1">{fg(idx(240))(" (idle — no qualifying switches yet)")}</Text>];
return [<Text height="1" fg={idx(240)}>{" (idle — no qualifying switches yet)"}</Text>];
}
const top = shown[0].pct || 1; // scale bars to the busiest process

Expand All @@ -31,12 +31,10 @@ export default ({ procs, width, maxRows }) => (
const w = Math.max(1, Math.round(frac * barW));
return (
<Text height="1" break="none">
{[
fg(idx(252))(` ${pad(p.comm, NAMEW)}`),
fg(heat(frac))("█".repeat(w)),
fg(idx(237))("·".repeat(Math.max(0, barW - w))),
bold(fg(idx(250))(lpad(`${p.pct.toFixed(1)}%`, PCTW))),
]}
<Text fg={idx(252)}>{` ${pad(p.comm, NAMEW)}`}</Text>
<Text fg={heat(frac)}>{"█".repeat(w)}</Text>
<Text fg={idx(237)}>{"·".repeat(Math.max(0, barW - w))}</Text>
<Text bold fg={idx(250)}>{lpad(`${p.pct.toFixed(1)}%`, PCTW)}</Text>
</Text>
);
});
Expand Down
10 changes: 5 additions & 5 deletions template/src/components/titlebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@
// min-slice knob value (what +/- patches into the kernel). A one-row Box
// tinted as the rail via the container's own bg — full width, no fragile
// space-fill.
import { Box, Text, bold, fg, idx } from "yeet:tui";
import { Box, Text, idx } from "yeet:tui";
import { fmtDuration, fmtRate } from "@/lib/format.js";

const RAIL = idx(235);
const sep = () => <Text fg={idx(240)}>{" ▏ "}</Text>;

export default ({ cpus, minSlice }) => (
<Box height="1" direction="row" bg={RAIL}>
<Text break="none">
{() => {
const total = cpus.get().reduce((s, c) => s + c.rate, 0);
const sep = fg(idx(240))(" ▏ ");
return [
bold(fg(idx(214))(" ● cpusched ")), sep,
bold(`${fmtRate(total)}`), fg(idx(245))(" switches/s"), sep,
fg(idx(245))("min-slice ≥ "), bold(fmtDuration(minSlice.get() * 1000)), fg(idx(240))(" [ +/- ]"),
<Text bold fg={idx(214)}>{" ● cpusched "}</Text>, sep(),
<Text bold>{fmtRate(total)}</Text>, <Text fg={idx(245)}>{" switches/s"}</Text>, sep(),
<Text fg={idx(245)}>{"min-slice ≥ "}</Text>, <Text bold>{fmtDuration(minSlice.get() * 1000)}</Text>, <Text fg={idx(240)}>{" [ +/- ]"}</Text>,
];
}}
</Text>
Expand Down
4 changes: 2 additions & 2 deletions template/src/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*
* To ship a pure-JS script, delete src/bpf/, bin/, and probes/cpusched.js.
*/
import { Box, Text, fg, idx, mount, signal } from "yeet:tui";
import { Box, Text, idx, mount, signal } from "yeet:tui";
import { numCpus } from "@/probes/probe.js";
import { cpus, minSlice, procs, setMinSlice } from "@/probes/cpusched.js";
import { latency } from "@/probes/runqlat.js";
Expand Down Expand Up @@ -52,7 +52,7 @@ tty.on?.("mousedown", (e) => {
if (i >= 0 && i < numCpus) selected.set(i);
});

const Rule = () => <Text height="1">{fg(idx(238))("─".repeat(400))}</Text>;
const Rule = () => <Text height="1" fg={idx(238)}>{"─".repeat(400)}</Text>;

// One layout cell → its component, filled with the extents the layout chose.
const cell = (c) => {
Expand Down
Loading