Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a22c9b2
feat(examples): wire OSC 22 pointer shapes into the interactive demos
natemoo-re Oct 9, 2026
5bfb534
fix(examples): start the stdin reader when the input stream is subscr…
natemoo-re Oct 9, 2026
fa7c38b
feat(examples): TTY_POINTER_SHAPES=1 opt-in for set-only terminals
natemoo-re Oct 9, 2026
ab3cb0a
feat(examples): cursor demo — a shape catalog with half-block bevels
natemoo-re Oct 9, 2026
771e435
refactor(examples): brand hues and border-only tiles in the cursor demo
natemoo-re Oct 9, 2026
f42195e
refactor(examples): trim the cursor demo to ghostty-confirmed shapes
natemoo-re Oct 9, 2026
8cddaac
fix(examples): set width on the cursor demo's status line
natemoo-re Oct 9, 2026
88e4de6
fix(examples): semantic hue assignment in the cursor demo
natemoo-re Oct 9, 2026
db9c117
feat(examples): grab/grabbing toggle tile and status refinements in t…
natemoo-re Oct 9, 2026
750d2d2
feat(examples): grab tile switches on pointer down/up, not click
natemoo-re Oct 9, 2026
5ac7047
chore(examples): cursor demo title
natemoo-re Oct 9, 2026
9903145
feat(examples): drag shield keeps the grabbing cursor anywhere while …
natemoo-re Oct 9, 2026
fd5f6e6
feat(examples): @bomb.sh/tty ▪ cursor title bar in the cursor demo
natemoo-re Oct 9, 2026
e87acf1
feat(examples): ambient brightness wave in the cursor demo
natemoo-re Oct 9, 2026
061c6e0
feat(examples): brightness wave on the title spacers, hover persists …
natemoo-re Oct 9, 2026
f939eef
feat(examples): title text joins the brightness wave; keycap hint
natemoo-re Oct 9, 2026
0b26920
feat(examples): name the open() cursor property after CSS (was pointe…
natemoo-re Oct 11, 2026
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
38 changes: 31 additions & 7 deletions examples/2048/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ import {
type Stream,
until,
} from "effection";
import { createTerm, type InputEvent, type PointerEvent } from "../../mod.ts";
import {
createTerm,
detectTerminal,
type InputEvent,
type PointerEvent,
} from "../../mod.ts";
import {
alternateBuffer,
cursor,
Expand All @@ -36,6 +41,7 @@ import {
} from "../../settings.ts";
import { useInput } from "../use-input.ts";
import { useStdin } from "../use-stdin.ts";
import { optInPointerShapes } from "../pointer-shape-opt-in.ts";
import {
clearFlags,
cloneGame,
Expand Down Expand Up @@ -279,20 +285,29 @@ function consoleSize(): { width: number; height: number } {
await main(function* () {
let { width, height } = consoleSize();

let info = yield* until(detectTerminal());

Deno.stdin.setRaw(true);
yield* ensure(() => Deno.stdin.setRaw(false));

let stdin = yield* useStdin();
let input = useInput(stdin);
let input = useInput(stdin, { terminfo: info });

let term = yield* until(createTerm({ width, height }));
let term = yield* until(createTerm({ width, height, terminfo: info }));

let tty = settings(alternateBuffer(), cursor(false), mouseTracking());
Deno.stdout.writeSync(tty.apply);
// Ask the terminal about itself (OSC color queries, XTGETTCAP, DECRPM, DA1).
// Responses arrive on the input stream as CapabilityEvents and are routed to
// term.update() in the loop below, which is what switches on features like
// pointer shapes (renderer-spec §7.9) once the terminal confirms support.
Deno.stdout.writeSync(info.probe);
yield* ensure(() => {
Deno.stdout.writeSync(tty.revert);
});

optInPointerShapes(term);

// The native term has fixed-size buffers, and the input parser never emits
// resize events, so we bridge SIGWINCH into the event stream ourselves. The
// payload is just a wake-up; the handler reads the live size.
Expand Down Expand Up @@ -368,15 +383,18 @@ await main(function* () {

for (let ev of yield* each(events)) {
if (ev !== undefined && typeof ev === "object" && "type" in ev) {
// Resize: rebuild the term at the new size, wipe stale cells, repaint.
// A drag-resize fires many SIGWINCHes; we read the live size each time
// and skip when unchanged, so only real size changes rebuild the term.
// Resize: tell the term the new size (the next render repaints every
// cell) and wipe stale cells. A drag-resize fires many SIGWINCHes; we
// read the live size each time and skip when unchanged, so only real
// size changes touch the term.
if ((ev as { type: string }).type === "resize") {
let next = consoleSize();
if (next.width !== width || next.height !== height) {
width = next.width;
height = next.height;
term = yield* until(createTerm({ width, height }));
Deno.stdout.writeSync(
term.update([{ type: "resize", width, height }]),
);
Deno.stdout.writeSync(CLEAR_SCREEN);
draw();
}
Expand All @@ -386,6 +404,12 @@ await main(function* () {

let e = ev as InputEvent | PointerEvent;

if (e.type === "capability") {
Deno.stdout.writeSync(term.update([e]));
yield* each.next();
continue;
}

if (e.type === "keydown") {
if (e.ctrl && e.key === "c") break;
if (e.key === "q") break;
Expand Down
5 changes: 5 additions & 0 deletions examples/2048/primitives/button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ export function button(
// below the button) when focus changes. Focus/hover feedback is instant
// instead, which is crisp and correct. Revisit once transitions support
// color-only animation without the position-interaction coupling.
// OSC 22 pointer shape: clickable buttons show the hand; a disabled
// button shows not-allowed, mirroring CSS `cursor` conventions. Only
// emitted when the terminal confirmed OSC 22 support (renderer-spec
// §7.9); otherwise this property is ignored.
cursor: state.disabled ? "not-allowed" : "pointer",
}),
text(label, { color: fg }),
close(),
Expand Down
53 changes: 50 additions & 3 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,48 @@ Build the generated WebAssembly bundle before running the examples:
make
```

## Pointer shapes

The cursor, keyboard, 2048, and text-input demos change the mouse pointer over
their interactive elements (OSC 22, renderer-spec §7.9). Support is detected by
the probe (kitty) and by the known-support table in `detectTerminal()` (ghostty,
foot, xterm ≥ 367), so those work out of the box. For anything else, assert
support by hand with:

```sh
TTY_POINTER_SHAPES=1 deno run examples/keyboard/index.ts
```

## Cursor

Path: `examples/cursor/index.ts`

Run it with:

```sh
deno run examples/cursor/index.ts
# or
node examples/cursor/index.ts
```

What it shows:

- a catalog of 9 pointer shapes — the subset confirmed working in ghostty — as
border-only rounded tiles in Bombshell brand hues, declaring their `cursor`
(renderer-spec §7.9); hovering a tile shows that exact cursor in terminals
that support OSC 22
- the grab tile is double-wide: holding the pointer down shows grabbing anywhere
on screen while held — a capture-mode drag shield (userland drag persistence,
matching CSS drag behavior); releasing returns to grab
- an ambient brightness wave travels the title bar: the ▪ spacers breathe on a
phase-offset sine, so a ripple of light crosses the brand title (~12fps
ticker)
- hover feedback lights the tile's ring and name, so the hover state reads even
in terminals without OSC 22
- a muted status line whose hover label takes the hovered tile's hue, with a
feature indicator (green ● supported, red ■ not) pinned to the right edge
- a final frame on exit that restores the default pointer (§7.9 restore)

## Keyboard

Path: `examples/keyboard/index.ts`
Expand All @@ -34,6 +76,8 @@ What it shows:
- raw keyboard input decoded into structured key events
- progressive keyboard protocol support
- pointer tracking and hover/click-driven UI updates
- OSC 22 pointer shapes: hovering the on-screen keys shows the hand pointer when
the terminal confirms support (renderer-spec §7.9)
- terminal mode configuration such as alternate buffer, hidden cursor, and mouse
reporting

Expand Down Expand Up @@ -99,15 +143,18 @@ What it shows:
- the `animating` render signal gating a follow-up frame loop, so the process
only renders while something is moving
- pointer hit testing and keyboard focus working together on the chrome buttons
- OSC 22 pointer shapes on the chrome buttons: hand cursor while hovering,
not-allowed on the disabled Undo button, when the terminal confirms support
(renderer-spec §7.9)
- an fps readout in the footer: a sliding-window count of frames pushed to
stdout in the last second. It measures how fast frames are _produced_, not how
fast the terminal _paints_ them — a CPU-rendered terminal (e.g. Terminal.app)
can coalesce or drop frames downstream where the process can't observe it, so
motion can look steppy even while this number stays high
- live resize handling: a `SIGWINCH` listener is bridged into the Effection
event loop, and because the native term has fixed-size buffers, each real size
change rebuilds the term, clears the screen, and repaints so the layout
re-centers to the new dimensions
event loop, and each real size change is folded into the term with
`term.update([{ type: "resize", width, height }])`, after which the screen is
cleared and repainted so the layout re-centers to the new dimensions
- a keycaster overlay: recent key presses and button clicks appear as a centered
row of caps near the bottom (a floating, pointer-passthrough element) that
retire on a timer, handy for screen recordings and demos
Expand Down
Loading
Loading