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
23e5882
feat(examples): wire OSC 22 pointer shapes into the interactive demos
natemoo-re Oct 9, 2026
debc966
fix(examples): start the stdin reader when the input stream is subscr…
natemoo-re Oct 9, 2026
8d4d9c8
feat(examples): TTY_POINTER_SHAPES=1 opt-in for set-only terminals
natemoo-re Oct 9, 2026
e7db28a
feat(examples): cursor demo — a shape catalog with half-block bevels
natemoo-re Oct 9, 2026
8d800a8
refactor(examples): brand hues and border-only tiles in the cursor demo
natemoo-re Oct 9, 2026
c88b1ab
refactor(examples): trim the cursor demo to ghostty-confirmed shapes
natemoo-re Oct 9, 2026
01c2a66
fix(examples): set width on the cursor demo's status line
natemoo-re Oct 9, 2026
7311370
fix(examples): semantic hue assignment in the cursor demo
natemoo-re Oct 9, 2026
d6d7069
feat(examples): grab/grabbing toggle tile and status refinements in t…
natemoo-re Oct 9, 2026
b137a82
feat(examples): grab tile switches on pointer down/up, not click
natemoo-re Oct 9, 2026
1e3ead2
chore(examples): cursor demo title
natemoo-re Oct 9, 2026
4c39a81
feat(examples): drag shield keeps the grabbing cursor anywhere while …
natemoo-re Oct 9, 2026
2dea5a1
feat(examples): @bomb.sh/tty ▪ cursor title bar in the cursor demo
natemoo-re Oct 9, 2026
30f69d9
feat(examples): ambient brightness wave in the cursor demo
natemoo-re Oct 9, 2026
c579ca7
feat(examples): brightness wave on the title spacers, hover persists …
natemoo-re Oct 9, 2026
b1d7b97
feat(examples): title text joins the brightness wave; keycap hint
natemoo-re Oct 9, 2026
19de122
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