Skip to content

Commit b555a1b

Browse files
committed
💄 format codebase and fix validate test render result
1 parent 10c686f commit b555a1b

8 files changed

Lines changed: 353 additions & 168 deletions

File tree

README.md

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,49 @@
11
# clayterm
22

3-
A low-level, platform-independent terminal renderer and event parser for JavaScript. You can use clayterm directly, or as the foundation for your own framework.
3+
A low-level, platform-independent terminal renderer and event parser for
4+
JavaScript. You can use clayterm directly, or as the foundation for your own
5+
framework.
46

57
## Features
68

7-
**Declarative terminal UI** — Build terminal interfaces the same way you'd
8-
build a web page. Clayterm uses [Clay](https://github.com/nicbarker/clay) under
9-
the hood, giving you flexbox-like layout, pointer detection, and scroll
10-
containers — all rendered to the terminal as box-drawing characters and ANSI
11-
escape sequences.
9+
**Declarative terminal UI** — Build terminal interfaces the same way you'd build
10+
a web page. Clayterm uses [Clay](https://github.com/nicbarker/clay) under the
11+
hood, giving you flexbox-like layout, pointer detection, and scroll containers —
12+
all rendered to the terminal as box-drawing characters and ANSI escape
13+
sequences.
1214

1315
**Zero I/O** — Clayterm never reads stdin or writes stdout. You feed it bytes
1416
and get bytes back. This makes it trivially embeddable in any framework, any
1517
runtime, any event loop. There are no opinions about how you do I/O, just pure
1618
computation.
1719

18-
**Runs everywhere** — The entire engine is compiled to WebAssembly, so
19-
clayterm will run anywhere JavaScript runs with no native
20-
dependencies, and no build step for consumers.
20+
**Runs everywhere** — The entire engine is compiled to WebAssembly, so clayterm
21+
will run anywhere JavaScript runs with no native dependencies, and no build step
22+
for consumers.
2123

2224
### Demo
2325

2426
The application in this demo uses Clayterm for all layout and input parsing
2527

2628
#### Keyboard Events
2729

28-
The input parser decodes raw terminal bytes into structured events.
29-
Here you can see each key event as the string "hello world" is typed.
30+
The input parser decodes raw terminal bytes into structured events. Here you can
31+
see each key event as the string "hello world" is typed.
3032

3133
![Keyboard events demo](demo/keyboard-key-events.gif)
3234

3335
#### Pointer Events
3436

35-
Here we see hover styles applied to UI elements in response to the
36-
pointer state. Clay drives the hit testing; no manual coordinate math
37-
required.
37+
Here we see hover styles applied to UI elements in response to the pointer
38+
state. Clay drives the hit testing; no manual coordinate math required.
3839

3940
![Pointer events demo](demo/keyboard-pointer-events.gif)
4041

41-
4242
## Architecture
4343

44-
Clayterm does not do any I/O itself. On the ouput side, it converts UI elements into a raw sequence of bytes and pointer events, and on the input side, it converts a stream of raw bytes into structured events.
44+
Clayterm does not do any I/O itself. On the ouput side, it converts UI elements
45+
into a raw sequence of bytes and pointer events, and on the input side, it
46+
converts a stream of raw bytes into structured events.
4547

4648
### Output
4749

@@ -126,7 +128,10 @@ let { output } = term.render([
126128
layout: { padding: { left: 1, right: 1 } },
127129
border: {
128130
color: rgba(0, 255, 0),
129-
left: 1, right: 1, top: 1, bottom: 1,
131+
left: 1,
132+
right: 1,
133+
top: 1,
134+
bottom: 1,
130135
},
131136
cornerRadius: { tl: 1, tr: 1, bl: 1, br: 1 },
132137
}),

demo/keyboard.ts

Lines changed: 95 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
// deno-lint-ignore-file no-fallthrough
2-
import { createChannel, each, ensure, main, race, resource, type Stream, until } from "effection";
2+
import {
3+
createChannel,
4+
each,
5+
ensure,
6+
main,
7+
race,
8+
resource,
9+
type Stream,
10+
until,
11+
} from "effection";
312
import {
413
close,
514
createTerm,
@@ -37,7 +46,8 @@ function isKeyEvent(e: InputEvent | PointerEvent): e is KeyEvent {
3746
}
3847

3948
function matches(k: KeyDef, event: InputEvent | PointerEvent): boolean {
40-
return isKeyEvent(event) && event.type === "keydown" && event.code.toUpperCase() === k.code.toUpperCase();
49+
return isKeyEvent(event) && event.type === "keydown" &&
50+
event.code.toUpperCase() === k.code.toUpperCase();
4151
}
4252

4353
let hovered = rgba(80, 80, 100);
@@ -310,13 +320,15 @@ function toggle(ops: Op[], enabled: boolean, name: string): void {
310320
);
311321
}
312322

313-
let flagNames: (keyof Omit<AppContext, "mode" | "event" | "logged" | "log" | "entered">)[] = [
314-
"Disambiguate escape codes",
315-
"Report event types",
316-
"Report alternate keys",
317-
"Report all keys as escapes",
318-
"Report associated text",
319-
];
323+
let flagNames:
324+
(keyof Omit<AppContext, "mode" | "event" | "logged" | "log" | "entered">)[] =
325+
[
326+
"Disambiguate escape codes",
327+
"Report event types",
328+
"Report alternate keys",
329+
"Report all keys as escapes",
330+
"Report associated text",
331+
];
320332

321333
let logEntries: { key: string; name: keyof EventFilter }[] = [
322334
{ key: "a", name: "keydown" },
@@ -332,10 +344,16 @@ let logEntries: { key: string; name: keyof EventFilter }[] = [
332344
{ key: "k", name: "pointerclick" },
333345
];
334346

335-
function logToggle(ops: Op[], entries: typeof logEntries, ctx: AppContext): void {
347+
function logToggle(
348+
ops: Op[],
349+
entries: typeof logEntries,
350+
ctx: AppContext,
351+
): void {
336352
for (let entry of entries) {
337353
ops.push(
338-
open(`log:${entry.name}`, { layout: { direction: "ltr", height: fixed(1), gap: 1 } }),
354+
open(`log:${entry.name}`, {
355+
layout: { direction: "ltr", height: fixed(1), gap: 1 },
356+
}),
339357
);
340358
ops.push(text(`${entry.key}.`, { color: dim }));
341359
toggle(ops, ctx.log[entry.name], entry.name);
@@ -364,7 +382,9 @@ function configPanel(ops: Op[], ctx: AppContext): void {
364382
for (let i = 0; i < flagNames.length; i++) {
365383
let name = flagNames[i];
366384
ops.push(
367-
open(`flag:${name}`, { layout: { direction: "ltr", height: fixed(1), gap: 1 } }),
385+
open(`flag:${name}`, {
386+
layout: { direction: "ltr", height: fixed(1), gap: 1 },
387+
}),
368388
);
369389
ops.push(text(`${i + 1}.`, { color: dim }));
370390
toggle(ops, ctx[name], name);
@@ -437,12 +457,21 @@ function keyboard(ctx: AppContext): Op[] {
437457
let badgeHint = ctx.mode === "input"
438458
? "Ctrl+X Ctrl+X to enter config"
439459
: "Set flags with keys [0-5], Enter to save";
440-
let mouseBg = ctx["Capture mouse events"] ? rgba(40, 180, 80) : rgba(80, 80, 80);
460+
let mouseBg = ctx["Capture mouse events"]
461+
? rgba(40, 180, 80)
462+
: rgba(80, 80, 80);
441463
let mouseLabel = ctx["Capture mouse events"] ? "capture" : "system";
442464
ops.push(
443-
open("badges", { layout: { direction: "ttb", gap: 1, padding: { top: 1 } } }),
444-
open("badge:mode", { layout: { direction: "ltr", height: fixed(1), padding: { bottom: 1 } } }),
445-
open("", { layout: { padding: { left: 1, right: 1 } }, bg: rgba(60, 60, 60) }),
465+
open("badges", {
466+
layout: { direction: "ttb", gap: 1, padding: { top: 1 } },
467+
}),
468+
open("badge:mode", {
469+
layout: { direction: "ltr", height: fixed(1), padding: { bottom: 1 } },
470+
}),
471+
open("", {
472+
layout: { padding: { left: 1, right: 1 } },
473+
bg: rgba(60, 60, 60),
474+
}),
446475
text("mode", { color: rgba(220, 220, 220) }),
447476
close(),
448477
open("", { layout: { padding: { left: 1, right: 1 } }, bg: badgeBg }),
@@ -451,7 +480,10 @@ function keyboard(ctx: AppContext): Op[] {
451480
text(` ${badgeHint}`, { color: dim }),
452481
close(),
453482
open("badge:mouse", { layout: { direction: "ltr", height: fixed(1) } }),
454-
open("", { layout: { padding: { left: 1, right: 1 } }, bg: rgba(60, 60, 60) }),
483+
open("", {
484+
layout: { padding: { left: 1, right: 1 } },
485+
bg: rgba(60, 60, 60),
486+
}),
455487
text("mouse", { color: rgba(220, 220, 220) }),
456488
close(),
457489
open("", { layout: { padding: { left: 1, right: 1 } }, bg: mouseBg }),
@@ -552,7 +584,7 @@ await main(function* () {
552584

553585
let pointer = {
554586
events: createChannel<PointerEvent, void>(),
555-
state: undefined as { x: number; y: number; down: boolean} | undefined,
587+
state: undefined as { x: number; y: number; down: boolean } | undefined,
556588
};
557589

558590
for (let event of yield* each(merge(input, pointer.events))) {
@@ -578,18 +610,24 @@ await main(function* () {
578610

579611
if (context["Capture mouse events"]) {
580612
if ("x" in event) {
581-
pointer.state = { x: event.x, y: event.y, down: event.type === "mousedown" };
613+
pointer.state = {
614+
x: event.x,
615+
y: event.y,
616+
down: event.type === "mousedown",
617+
};
582618
}
583619
} else {
584620
pointer.state = undefined;
585621
}
586622

587-
let { output, events } = term.render(keyboard(context), { pointer: pointer.state });
623+
let { output, events } = term.render(keyboard(context), {
624+
pointer: pointer.state,
625+
});
588626

589627
for (let event of events) {
590628
yield* pointer.events.send(event);
591629
}
592-
630+
593631
Deno.stdout.writeSync(output);
594632

595633
yield* each.next();
@@ -623,7 +661,7 @@ function* recognizer(): Iterator<AppContext, never, InputEvent | PointerEvent> {
623661
logged: null,
624662
};
625663

626-
let event= yield current;
664+
let event = yield current;
627665

628666
let mode = inputmode({ ...current, event });
629667

@@ -670,59 +708,63 @@ function* configmode(context: AppContext): Mode {
670708
let event = yield context;
671709
while (true) {
672710
if (event.type === "keydown" && event.key === "Enter") {
673-
return inputmode({...context, event: null });
711+
return inputmode({ ...context, event: null });
674712
}
675713
if (event.type === "keydown") {
676714
let k = (event as KeyEvent).key;
677715
let entry = logEntries.find((e) => e.key === k);
678716
if (entry) {
679-
context = { ...context, log: { ...context.log, [entry.name]: !context.log[entry.name] } };
717+
context = {
718+
...context,
719+
log: { ...context.log, [entry.name]: !context.log[entry.name] },
720+
};
680721
}
681722
if ("012345".indexOf(event.key) >= 0) {
682-
context = { ...context };
683-
context["Report associated text"] = false;
684-
context["Report all keys as escapes"] = false;
685-
context["Report alternate keys"] = false;
686-
context["Report event types"] = false;
687-
context["Disambiguate escape codes"] = false;
688-
switch (event.key) {
689-
case "5":
690-
context["Report associated text"] = true;
691-
case "4":
692-
context["Report all keys as escapes"] = true;
693-
case "3":
694-
context["Report alternate keys"] = true;
695-
case "2":
696-
context["Report event types"] = true;
697-
case "1":
698-
context["Disambiguate escape codes"] = true;
699-
break;
700-
case "0":
701-
break;
702-
}
723+
context = { ...context };
724+
context["Report associated text"] = false;
725+
context["Report all keys as escapes"] = false;
726+
context["Report alternate keys"] = false;
727+
context["Report event types"] = false;
728+
context["Disambiguate escape codes"] = false;
729+
switch (event.key) {
730+
case "5":
731+
context["Report associated text"] = true;
732+
case "4":
733+
context["Report all keys as escapes"] = true;
734+
case "3":
735+
context["Report alternate keys"] = true;
736+
case "2":
737+
context["Report event types"] = true;
738+
case "1":
739+
context["Disambiguate escape codes"] = true;
740+
break;
741+
case "0":
742+
break;
743+
}
703744
}
704745
}
705746
event = yield context;
706747
}
707748
}
708749

709-
710-
function merge<A,B, TClose>(a: Stream<A,TClose>, b: Stream<B,TClose>): Stream<A|B, TClose> {
711-
return resource(function*(provide) {
750+
function merge<A, B, TClose>(
751+
a: Stream<A, TClose>,
752+
b: Stream<B, TClose>,
753+
): Stream<A | B, TClose> {
754+
return resource(function* (provide) {
712755
let subscription = {
713756
a: yield* a,
714757
b: yield* b,
715-
}
758+
};
716759

717760
return yield* provide({
718761
*next() {
719-
return yield* race([subscription.a.next(),subscription.b.next()]);
762+
return yield* race([subscription.a.next(), subscription.b.next()]);
720763
},
721-
})
722-
});
764+
});
765+
});
723766
}
724767

725-
726768
type EventFilter = {
727769
keydown: boolean;
728770
keyrepeat: boolean;
@@ -750,4 +792,3 @@ type AppContext = {
750792
["Report associated text"]: boolean;
751793
["Capture mouse events"]: boolean;
752794
};
753-

input-native.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,15 @@ export const KEY_TAB = 0x09;
7777
export const KEY_BACKSPACE = 0x7F;
7878
export const KEY_SPACE = 0x20;
7979

80-
import { array, int32, offsets, struct, uint8, uint16, uint32 } from "./typedef.ts";
80+
import {
81+
array,
82+
int32,
83+
offsets,
84+
struct,
85+
uint16,
86+
uint32,
87+
uint8,
88+
} from "./typedef.ts";
8189

8290
const MAX_TEXT_CODEPOINTS = 8;
8391

0 commit comments

Comments
 (0)