@cssints/tui

cssints in a terminal: the runtime of cssints/tui styles (states, the mouse pointer, the caret, themes) and its adapters for OpenTUI and @bomb.sh/tty. The styles are written with cssints/tui, which the cssints Vite plugin compiles to prop objects; this package puts them on renderables. The design is in docs/research/tui.md.

// styles.ts
import * as css from "cssints/tui" with { type: "cssints" };

export const t = css.createTokens({
  color: { $type: "color", bg: { $value: "#1e1e2e" }, accent: { $value: "#89b4fa" } },
});
css.createGlobalTheme(t, "light", { color: { bg: "#eff1f5", accent: "#1e66f5" } });

export const button = css.cn(
  css.border("rounded", "#585b70"),
  css.p(0, 1),
  css.bg(t.color.bg),
  css.cursor("pointer"),
  css.hover(css.borderColor(t.color.accent)),
  css.active(css.borderStyle("double")),
);

The plugin replaces button by:

export const button = {
  border: true,
  borderStyle: "rounded",
  borderColor: "#585b70",
  paddingTop: 0,
  paddingRight: 1,
  paddingBottom: 0,
  paddingLeft: 1,
  backgroundColor: "#1e1e2e",
  cursor: "pointer",
  ":hover": { borderColor: "#89b4fa" },
  ":active": { border: true, borderStyle: "double" },
  themes: { light: { backgroundColor: "#eff1f5", ":hover": { borderColor: "#1e66f5" } } },
  vars: { backgroundColor: "color-bg", ":hover": { borderColor: "color-accent" } }, // for a run-time theme
};
import { attach, setTheme, use } from "@cssints/tui/opentui";

<box {...use(button)}>
  <text>Save</text>
</box>; // Solid or React
<text {...use(() => (active() ? tabActive : tab))}>Home</text>; // a style that changes
attach(new BoxRenderable(renderer, {}), button); // core
setTheme("light"); // every attached renderable

Rules

The system theme

A theme computed at run time from the terminal's own colours: each colour token is a role, a hue source (an ANSI colour, or the terminal's fg/bg) and contrast rules against the tokens it sits on.

// theme.ts: run time, not a cssints module
import { createSystemTheme } from "@cssints/tui/system";

import { t } from "./styles.ts";

export const system = createSystemTheme(
  t,
  {
    color: {
      bg: { surface: true }, // the terminal's background
      card: { surface: true, on: [t.color.bg], contrast: 1.1 }, // a panel 1.1:1 from it
      text: { hue: "fg", on: [t.color.bg, t.color.card], contrast: 7 },
      accent: { hue: "blue", on: [t.color.bg, t.color.card], contrast: 3 },
    },
  },
  { hues: { blue: "#89b4fa" }, themes: { light: "light" } },
);
// OpenTUI
import { followSystemTheme } from "@cssints/tui/opentui";

const renderer = await createCliRenderer();
await followSystemTheme(renderer, system); // the palette, or 100 ms; sets the theme "system"

@bomb.sh/tty

bomb.sh draws a frame from a list of ops (open, text, close) and returns the pointer events of that frame. The adapter @cssints/tui/bomb turns a style, in the state of its element id, into the options of open() and text(), and its tracker keeps the states across frames:

import { close, createTerm } from "@bomb.sh/tty";
import { createTracker, setTheme } from "@cssints/tui/bomb";

const term = await createTerm({ width: 80, height: 24 });
const ui = createTracker();
const build = () => [
  ui.open("save", button), // open("save", options of `button` in the state of "save")
  ui.text("Save", label, "save"), // text("Save", options of `label` in the state of "save")
  close(),
];
// each mouse event: the pointer is { x, y, down }
process.stdout.write(ui.render(term, build, { pointer }).output);
setTheme("light"); // the next frame takes the light values
process.stdout.write(ui.reset()); // on exit: the default pointer and caret
const follow = followSystemTheme(system, { write: (s) => process.stdout.write(s) });
process.stdin.on("data", (bytes) => dispatch(input.scan(follow.feed(bytes)).events));
await follow.ready;

Limits

No terminal HMR, no inheritance of text styles, no per-side borders or titles (the renderable's own options), no units or calc(). See "Out of scope" in docs/research/tui.md.

Credits

The adapter is for OpenTUI (MIT). No code of it is copied.

Tests

bun run test        # node test/system.ts (the solver on Gruvbox dark, Catppuccin Latte and Nord, the fallbacks, the replies
                    # as bytes, the bomb.sh follower), node test/tui.mjs (the plugin: dev transform, SSR build, errors),
                    # bun test/render.ts (OpenTUI's test renderer; the system theme on a fake terminal that answers OSC 4/10/11),
                    # node test/bomb.ts (bomb.sh's renderer at 30x8, the cells read back from its output)
bun run example     # examples/bomb.ts: a list with hover, press and click, `t` swaps the theme, `q` quits
bun run typecheck   # src and test/types/tui.ts (what the types reject)