@valyrianjs/terminal 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +12 -14
- package/dist/ansi.js.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +4 -0
- package/dist/events.js.map +1 -1
- package/dist/frame-style.d.ts +7 -0
- package/dist/frame-style.d.ts.map +1 -0
- package/dist/frame-style.js +27 -0
- package/dist/frame-style.js.map +1 -0
- package/dist/layout.d.ts +5 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +53 -23
- package/dist/layout.js.map +1 -1
- package/dist/mouse.d.ts.map +1 -1
- package/dist/mouse.js +8 -1
- package/dist/mouse.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +87 -48
- package/dist/render.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +2 -0
- package/dist/session.js.map +1 -1
- package/dist/text.d.ts +7 -0
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +114 -0
- package/dist/text.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/api-reference.md +6 -3
- package/docs/cookbook.md +1 -1
- package/docs/interaction-model.md +5 -5
- package/docs/primitive-gallery.md +4 -4
- package/examples/basic.tsx +22 -0
- package/examples/cli.tsx +55 -0
- package/examples/demo.tsx +98 -0
- package/examples/docs/background-fill.tsx +107 -0
- package/examples/docs/component-composition.tsx +140 -0
- package/examples/docs/cursor.tsx +121 -0
- package/examples/docs/employees-list.tsx +138 -0
- package/examples/docs/hello.tsx +98 -0
- package/examples/docs/interactive-note.tsx +111 -0
- package/examples/docs/module-api-dashboard.tsx +307 -0
- package/examples/docs/module-flux-store.tsx +181 -0
- package/examples/docs/module-form-workflow.tsx +339 -0
- package/examples/docs/module-forms.tsx +218 -0
- package/examples/docs/module-money.tsx +175 -0
- package/examples/docs/module-native-store.tsx +188 -0
- package/examples/docs/module-pulses.tsx +142 -0
- package/examples/docs/module-query.tsx +209 -0
- package/examples/docs/module-request.tsx +194 -0
- package/examples/docs/module-state-workbench.tsx +283 -0
- package/examples/docs/module-tasks.tsx +223 -0
- package/examples/docs/module-translate.tsx +194 -0
- package/examples/docs/module-utils.tsx +168 -0
- package/examples/docs/module-valyrian-core.tsx +159 -0
- package/examples/docs/pizza-builder.tsx +463 -0
- package/examples/docs/primitive-activity-console.tsx +113 -0
- package/examples/docs/primitive-command-panel.tsx +186 -0
- package/examples/docs/primitive-data-explorer.tsx +155 -0
- package/examples/docs/primitive-input-workbench.tsx +128 -0
- package/examples/docs/primitive-layout-shell.tsx +115 -0
- package/examples/docs/responsive-split.tsx +186 -0
- package/examples/docs/style-system.tsx +209 -0
- package/examples/docs/theme-colors.tsx +225 -0
- package/examples/docs/virtualized-list-workbench.tsx +232 -0
- package/examples/opencode-dogfood-app.tsx +215 -0
- package/examples/opencode-dogfood-lifecycle.tsx +194 -0
- package/examples/opencode-dogfood.tsx +11 -0
- package/llms-full.txt +16 -13
- package/package.json +3 -2
- package/src/ansi.ts +12 -14
- package/src/events.ts +2 -0
- package/src/frame-style.ts +36 -0
- package/src/layout.ts +57 -24
- package/src/mouse.ts +10 -1
- package/src/render.ts +92 -48
- package/src/session.ts +2 -0
- package/src/text.ts +148 -0
- package/src/types.ts +3 -0
package/docs/cookbook.md
CHANGED
|
@@ -84,7 +84,7 @@ console.log(session.output());
|
|
|
84
84
|
session.destroy();
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
For large lists, add `virtualized` and place the list in a bounded region such as a filled pane or split. The virtualized workbench
|
|
87
|
+
For large lists, add `virtualized` and place the list in a bounded region such as a filled pane or split. The virtualized workbench demonstrates the uncontrolled List API with `itemKey`, the `children` callback, event payloads, active row, selected row, and viewport values: [`examples/docs/virtualized-list-workbench.tsx`](../examples/docs/virtualized-list-workbench.tsx). Shift+Up and Shift+Down are available for custom app actions. The List does not reorder items by default. Run it with `bun examples/docs/virtualized-list-workbench.tsx`, move active rows with `J/K` or `Up`/`Down`, jump with `PageUp`, `PageDown`, `Home`, or `End`, select with `Enter`, and quit with `Ctrl+C`.
|
|
88
88
|
|
|
89
89
|
## Local state to Valyrian state module bridge
|
|
90
90
|
|
|
@@ -12,7 +12,7 @@ This guide explains how interactive primitives receive input and report events.
|
|
|
12
12
|
|
|
13
13
|
## Input events
|
|
14
14
|
|
|
15
|
-
Keyboard dispatch uses normalized key names such as `ENTER`, `TAB`, `SHIFT_TAB`, `LEFT`, `RIGHT`, `CTRL_V`, and single-character strings. When a session is connected to `stdin`, terminal input is parsed and routed to the focused primitive.
|
|
15
|
+
Keyboard dispatch uses normalized key names such as `ENTER`, `TAB`, `SHIFT_TAB`, `LEFT`, `RIGHT`, `SHIFT_UP`, `SHIFT_DOWN`, `CTRL_V`, and single-character strings. When a session is connected to `stdin`, terminal input is parsed and routed to the focused primitive.
|
|
16
16
|
|
|
17
17
|
Pasted text is treated as text input for the focused primitive. Do not treat pasted strings or key sequences as terminal control instructions inside rendered content.
|
|
18
18
|
|
|
@@ -52,7 +52,7 @@ Main handlers:
|
|
|
52
52
|
|
|
53
53
|
### `Editor`
|
|
54
54
|
|
|
55
|
-
`Editor` is a multiline editing primitive.
|
|
55
|
+
`Editor` is a multiline editing primitive. Use `height` for a bounded multiline viewport, `width` for a fixed frame width, and `fill` when the editor should behave like a textarea surface that consumes available width and height. Explicit dimensions win over `fill`; without those props, existing content-sized rendering stays intact.
|
|
56
56
|
|
|
57
57
|
Supported behavior includes:
|
|
58
58
|
|
|
@@ -93,13 +93,13 @@ A quick second primary mouse press keeps the normal activation behavior and also
|
|
|
93
93
|
|
|
94
94
|
### `List`
|
|
95
95
|
|
|
96
|
-
`List` models active row movement, optional visible selection, viewport scroll, and activation. In virtualized mode
|
|
96
|
+
`List` models active row movement, optional visible selection, viewport scroll, and activation. In virtualized mode, pass the full `items` array; the list shows the visible viewport, wheel input scrolls it, and keyboard list commands move the active row and emit `change`. A JSX `children` callback receives `(item, ctx)` with `ctx.active`, `ctx.selected`, `ctx.index`, `ctx.key`, and `ctx.viewportIndex`; it takes precedence over `renderItem`. Use `itemKey` when rows have stable identities across reorder or filtering.
|
|
97
97
|
|
|
98
98
|
Supported behavior includes:
|
|
99
99
|
|
|
100
100
|
- `UP` and `DOWN` move the active row and emit `onchange`
|
|
101
101
|
- `PageUp`, `PageDown`, `Home`, and `End` jump the active row and keep it visible
|
|
102
|
-
- `LEFT` and `
|
|
102
|
+
- `LEFT`, `RIGHT`, `SHIFT_UP`, and `SHIFT_DOWN` have no default list command, so applications can bind them
|
|
103
103
|
- `ENTER` selects and activates the active row
|
|
104
104
|
- mouse click selects and activates the target row
|
|
105
105
|
- mouse hover reports row details when mouse input is connected
|
|
@@ -118,7 +118,7 @@ Main handlers:
|
|
|
118
118
|
- `oncapturestart`
|
|
119
119
|
- `oncaptureend`
|
|
120
120
|
|
|
121
|
-
`pointerCapture` lets a list keep drag interaction even when the pointer leaves the initial hitbox.
|
|
121
|
+
`pointerCapture` lets a list keep drag interaction even when the pointer leaves the initial hitbox. If your app needs reordering, priority changes, or other modified-arrow behavior, bind `SHIFT_UP` or `SHIFT_DOWN` through `keymap.bindings` and handle the custom command in `keymap.onCommand`. The list will not mutate item order by default.
|
|
122
122
|
|
|
123
123
|
### `ScrollView`
|
|
124
124
|
|
|
@@ -176,12 +176,12 @@ Use `Editor` for notes, descriptions, and longer messages.
|
|
|
176
176
|
|
|
177
177
|
**What it is:** A multi-line editable field.
|
|
178
178
|
**Use it for:** notes, descriptions, and longer messages.
|
|
179
|
-
**Core props:** `id`, `value`, `placeholder`, `height`, `onchange`, `oninput`, `onsubmit`, `oncancel`.
|
|
179
|
+
**Core props:** `id`, `value`, `placeholder`, `width`, `height`, `fill`, `onchange`, `oninput`, `onsubmit`, `oncancel`.
|
|
180
180
|
|
|
181
181
|
**Minimal example:**
|
|
182
182
|
|
|
183
183
|
```tsx
|
|
184
|
-
<Editor id="details" height={4} value={details} onchange={(event) => { details = event.value; }} />
|
|
184
|
+
<Editor id="details" fill height={4} value={details} onchange={(event) => { details = event.value; }} />
|
|
185
185
|
```
|
|
186
186
|
|
|
187
187
|
**Complete demo:** [`examples/docs/primitive-input-workbench.tsx`](../examples/docs/primitive-input-workbench.tsx). Run it with `bun examples/docs/primitive-input-workbench.tsx` and quit with `Ctrl+C`.
|
|
@@ -228,7 +228,7 @@ Use `List` for menus, choosers, command lists, and item browsers.
|
|
|
228
228
|
|
|
229
229
|
**What it is:** A focusable collection control with selection and press events.
|
|
230
230
|
**Use it for:** menus, choosers, command lists, and item browsers.
|
|
231
|
-
**Core props:** `id`, `items`, `children` callback, `renderItem`, `itemKey`, `virtualized`, `showActive`, `onchange`, `onviewportchange`, `onpress`, `ondoublepress`, `oncontextpress`. `List`
|
|
231
|
+
**Core props:** `id`, `items`, `children` callback, `renderItem`, `itemKey`, `virtualized`, `showActive`, `onchange`, `onviewportchange`, `onpress`, `ondoublepress`, `oncontextpress`. `List` manages its active row, selected row, and viewport state. Read those values from event payloads.
|
|
232
232
|
|
|
233
233
|
**Minimal example:**
|
|
234
234
|
|
|
@@ -240,7 +240,7 @@ Use `List` for menus, choosers, command lists, and item browsers.
|
|
|
240
240
|
|
|
241
241
|
**Complete demo:** [`examples/docs/primitive-data-explorer.tsx`](../examples/docs/primitive-data-explorer.tsx). Run it with `bun examples/docs/primitive-data-explorer.tsx` and quit with `Ctrl+C`.
|
|
242
242
|
|
|
243
|
-
**Virtualized demo:** [`examples/docs/virtualized-list-workbench.tsx`](../examples/docs/virtualized-list-workbench.tsx). Run it with `bun examples/docs/virtualized-list-workbench.tsx`, move active rows with `J/K` or `Up`/`Down`, jump with `PageUp`, `PageDown`, `Home`, or `End`, select with `Enter`, and quit with `Ctrl+C`.
|
|
243
|
+
**Virtualized demo:** [`examples/docs/virtualized-list-workbench.tsx`](../examples/docs/virtualized-list-workbench.tsx). Run it with `bun examples/docs/virtualized-list-workbench.tsx`, move active rows with `J/K` or `Up`/`Down`, jump with `PageUp`, `PageDown`, `Home`, or `End`, select with `Enter`, and quit with `Ctrl+C`. `Shift+Up` and `Shift+Down` are shown as app-defined commands, not built-in reorder behavior.
|
|
244
244
|
|
|
245
245
|
### `Table`
|
|
246
246
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { mountTerminal } from "../dist/index.js";
|
|
2
|
+
import { createDemoState, DemoApp, renderDemoSnapshot } from "./demo.js";
|
|
3
|
+
|
|
4
|
+
const state = createDemoState();
|
|
5
|
+
|
|
6
|
+
if (process.argv.includes("--snapshot")) {
|
|
7
|
+
console.log(renderDemoSnapshot(state));
|
|
8
|
+
} else {
|
|
9
|
+
const session = mountTerminal(() => <DemoApp state={state} />, {
|
|
10
|
+
stdin: process.stdin,
|
|
11
|
+
stdout: process.stdout
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
session.focus("name");
|
|
15
|
+
|
|
16
|
+
const shutdown = () => {
|
|
17
|
+
session.destroy();
|
|
18
|
+
process.exit(0);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
process.on("SIGINT", shutdown);
|
|
22
|
+
}
|
package/examples/cli.tsx
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { mountTerminal } from "../dist/index.js";
|
|
3
|
+
import { createDemoState, DemoApp, renderDemoSnapshot } from "./demo.js";
|
|
4
|
+
|
|
5
|
+
export function shouldExitCliChunk(chunk: string | Uint8Array) {
|
|
6
|
+
const value = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
7
|
+
if (value.length !== 1) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
const code = value.charCodeAt(0);
|
|
11
|
+
return code === 27 || code === 3;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function printHelp() {
|
|
15
|
+
console.log("valyrianjs-terminal local demo");
|
|
16
|
+
console.log("");
|
|
17
|
+
console.log("Usage:");
|
|
18
|
+
console.log(" bun run examples/cli.tsx [--snapshot]");
|
|
19
|
+
console.log(" bun run examples/cli.tsx --help");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const args = new Set(process.argv.slice(2));
|
|
23
|
+
|
|
24
|
+
if (args.has("--help") || args.has("-h")) {
|
|
25
|
+
printHelp();
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const state = createDemoState();
|
|
30
|
+
|
|
31
|
+
if (args.has("--snapshot")) {
|
|
32
|
+
console.log(renderDemoSnapshot(state));
|
|
33
|
+
} else {
|
|
34
|
+
const session = mountTerminal(() => <DemoApp state={state} />, {
|
|
35
|
+
stdin: process.stdin,
|
|
36
|
+
stdout: process.stdout
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
session.focus("menu");
|
|
40
|
+
|
|
41
|
+
const shutdown = () => {
|
|
42
|
+
process.stdin.off?.("data", onGlobalData);
|
|
43
|
+
session.destroy();
|
|
44
|
+
process.exit(0);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const onGlobalData = (chunk: string | Uint8Array) => {
|
|
48
|
+
if (shouldExitCliChunk(chunk)) {
|
|
49
|
+
shutdown();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
process.stdin.on("data", onGlobalData);
|
|
54
|
+
process.on("SIGINT", shutdown);
|
|
55
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TerminalInputChangeEventPayload,
|
|
3
|
+
TerminalInputSubmitEventPayload,
|
|
4
|
+
TerminalListChangeEventPayload,
|
|
5
|
+
TerminalListPressEventPayload
|
|
6
|
+
} from "../dist/index.js";
|
|
7
|
+
import { Button, Fixed, Input, List, mountTerminal, Pane, Row, Screen, ScrollView, Split, Table, Td, Text } from "../dist/index.js";
|
|
8
|
+
|
|
9
|
+
export function createDemoState() {
|
|
10
|
+
return {
|
|
11
|
+
name: "",
|
|
12
|
+
selected: "Inbox",
|
|
13
|
+
saved: "",
|
|
14
|
+
items: ["Inbox", "Today", "Done"],
|
|
15
|
+
shortcuts: [
|
|
16
|
+
["Tab", "Move focus"],
|
|
17
|
+
["Arrows", "Browse queue"],
|
|
18
|
+
["Enter", "Activate/save"],
|
|
19
|
+
["Esc", "Quit demo"]
|
|
20
|
+
]
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function DemoApp(props: { state: ReturnType<typeof createDemoState> }) {
|
|
25
|
+
const { state } = props;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Screen>
|
|
29
|
+
<Fixed position="top" size={4}>
|
|
30
|
+
<Pane fill style="pane.header" padding={1}>
|
|
31
|
+
<Text>Valyrian Terminal Demo</Text>
|
|
32
|
+
<Text>Queue workbench for triage and quick entry</Text>
|
|
33
|
+
</Pane>
|
|
34
|
+
</Fixed>
|
|
35
|
+
|
|
36
|
+
<Split fill direction="row" gap={1}>
|
|
37
|
+
<Pane fill style="pane.transcript" padding={1}>
|
|
38
|
+
<Text>Queues</Text>
|
|
39
|
+
<Text>Arrow keys to browse</Text>
|
|
40
|
+
<List
|
|
41
|
+
id="menu"
|
|
42
|
+
items={state.items}
|
|
43
|
+
onchange={(event: TerminalListChangeEventPayload<string>) => (state.selected = event.value)}
|
|
44
|
+
onpress={(event: TerminalListPressEventPayload<string>) => (state.saved = `Opened:${event.value}`)}
|
|
45
|
+
/>
|
|
46
|
+
</Pane>
|
|
47
|
+
|
|
48
|
+
<Pane fill style="pane.tools" padding={1}>
|
|
49
|
+
<Text>Quick Entry</Text>
|
|
50
|
+
<Text>{`Draft for ${state.selected}`}</Text>
|
|
51
|
+
<Input
|
|
52
|
+
id="name"
|
|
53
|
+
value={state.name}
|
|
54
|
+
placeholder="Write a quick note"
|
|
55
|
+
onchange={(event: TerminalInputChangeEventPayload) => (state.name = event.value)}
|
|
56
|
+
onsubmit={(event: TerminalInputSubmitEventPayload) => (state.saved = `Submitted:${event.value || state.selected}`)}
|
|
57
|
+
/>
|
|
58
|
+
<Button id="save" onpress={() => (state.saved = `Saved:${state.name || state.selected}`)}>Save Current</Button>
|
|
59
|
+
|
|
60
|
+
<Text>Activity</Text>
|
|
61
|
+
<ScrollView id="details" height={4}>
|
|
62
|
+
<Text>{`Selected : ${state.selected}`}</Text>
|
|
63
|
+
<Text>{`Draft : ${state.name || "-"}`}</Text>
|
|
64
|
+
<Text>{`Saved : ${state.saved || "-"}`}</Text>
|
|
65
|
+
<Text>Tip : Press Tab to switch panels</Text>
|
|
66
|
+
<Text>Tip : Enter saves current value</Text>
|
|
67
|
+
</ScrollView>
|
|
68
|
+
|
|
69
|
+
<Text>Shortcuts</Text>
|
|
70
|
+
<Table v-for={state.shortcuts}>
|
|
71
|
+
{(row: string[]) => (
|
|
72
|
+
<Row>
|
|
73
|
+
<Td>{row[0]}</Td>
|
|
74
|
+
<Td>{row[1]}</Td>
|
|
75
|
+
</Row>
|
|
76
|
+
)}
|
|
77
|
+
</Table>
|
|
78
|
+
</Pane>
|
|
79
|
+
</Split>
|
|
80
|
+
|
|
81
|
+
<Fixed position="bottom" size={3}>
|
|
82
|
+
<Pane fill style="pane.prompt" padding={1}>
|
|
83
|
+
<Text>Tab move focus | Enter activate | Ctrl+C quit</Text>
|
|
84
|
+
</Pane>
|
|
85
|
+
</Fixed>
|
|
86
|
+
</Screen>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function renderDemoSnapshot(state = createDemoState()) {
|
|
91
|
+
const session = mountTerminal(<DemoApp state={state} />, { cols: 80, rows: 24, clipboard: false });
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
return session.output();
|
|
95
|
+
} finally {
|
|
96
|
+
session.destroy();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Pane, Screen, Text, mountTerminal } from "@valyrianjs/terminal";
|
|
2
|
+
import type { TerminalMountOptions, TerminalSession } from "@valyrianjs/terminal";
|
|
3
|
+
|
|
4
|
+
interface BackgroundFillState {
|
|
5
|
+
surface: "panel" | "card";
|
|
6
|
+
running: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface BackgroundFillDemo {
|
|
10
|
+
session: TerminalSession;
|
|
11
|
+
dispatchKey(key: string): string;
|
|
12
|
+
output(): string;
|
|
13
|
+
ansiOutput(): string;
|
|
14
|
+
isRunning(): boolean;
|
|
15
|
+
destroy(): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const theme = { styles: { pane: { main: { background: "#111111" }, card: { background: "#1f2937" } } } };
|
|
19
|
+
|
|
20
|
+
function createInitialState(): BackgroundFillState {
|
|
21
|
+
return { surface: "panel", running: true };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function shouldRunSnapshot() {
|
|
25
|
+
return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function App({ state }: { state: BackgroundFillState }) {
|
|
29
|
+
const isCard = state.surface === "card";
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Screen title="Filled panel">
|
|
33
|
+
<Text>{isCard ? "Surface: card background" : "Surface: panel background"}</Text>
|
|
34
|
+
<Text>Look for the filled 34-column panel behind the content.</Text>
|
|
35
|
+
<Pane fill width={34} height={5} style={isCard ? "pane.card" : "pane.main"}>
|
|
36
|
+
<Text>{isCard ? "Card content" : "Panel content"}</Text>
|
|
37
|
+
</Pane>
|
|
38
|
+
<Text>B: switch surface Ctrl+C: quit</Text>
|
|
39
|
+
</Screen>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createBackgroundFillDemo(options: TerminalMountOptions = {}): BackgroundFillDemo {
|
|
44
|
+
const state = createInitialState();
|
|
45
|
+
let session: TerminalSession;
|
|
46
|
+
|
|
47
|
+
function quit() {
|
|
48
|
+
state.running = false;
|
|
49
|
+
session.destroy();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
session = mountTerminal(<App state={state} />, {
|
|
53
|
+
theme,
|
|
54
|
+
...options,
|
|
55
|
+
keymap: {
|
|
56
|
+
...options.keymap,
|
|
57
|
+
bindings: [
|
|
58
|
+
...(options.keymap?.bindings || []),
|
|
59
|
+
{ key: "b", command: { id: "surface.switch" }, scope: "global" },
|
|
60
|
+
{ key: "B", command: { id: "surface.switch" }, scope: "global" },
|
|
61
|
+
{ key: "CTRL_C", command: { id: "quit" }, scope: "global" }
|
|
62
|
+
],
|
|
63
|
+
onCommand(command, context) {
|
|
64
|
+
if (command.id === "surface.switch") {
|
|
65
|
+
state.surface = state.surface === "panel" ? "card" : "panel";
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (command.id === "quit") {
|
|
69
|
+
quit();
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return options.keymap?.onCommand?.(command, context);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
session,
|
|
79
|
+
dispatchKey(key: string) {
|
|
80
|
+
return session.dispatchKey(key);
|
|
81
|
+
},
|
|
82
|
+
output() {
|
|
83
|
+
return session.output();
|
|
84
|
+
},
|
|
85
|
+
ansiOutput() {
|
|
86
|
+
return session.ansiOutput();
|
|
87
|
+
},
|
|
88
|
+
isRunning() {
|
|
89
|
+
return state.running;
|
|
90
|
+
},
|
|
91
|
+
destroy() {
|
|
92
|
+
state.running = false;
|
|
93
|
+
session.destroy();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (import.meta.main) {
|
|
99
|
+
if (shouldRunSnapshot()) {
|
|
100
|
+
const demo = createBackgroundFillDemo({ runtime: "headless", cols: 42, rows: 9 });
|
|
101
|
+
process.stdout.write(demo.output());
|
|
102
|
+
process.stdout.write("\n");
|
|
103
|
+
demo.destroy();
|
|
104
|
+
} else {
|
|
105
|
+
createBackgroundFillDemo();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Screen, Text, mountTerminal } from "@valyrianjs/terminal";
|
|
2
|
+
import type { TerminalMountOptions, TerminalSession } from "@valyrianjs/terminal";
|
|
3
|
+
|
|
4
|
+
interface Profile {
|
|
5
|
+
name: string;
|
|
6
|
+
role: string;
|
|
7
|
+
status: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ComponentCompositionState {
|
|
11
|
+
selectedIndex: number;
|
|
12
|
+
running: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ComponentCompositionDemo {
|
|
16
|
+
session: TerminalSession;
|
|
17
|
+
dispatchKey(key: string): string;
|
|
18
|
+
output(): string;
|
|
19
|
+
ansiOutput(): string;
|
|
20
|
+
isRunning(): boolean;
|
|
21
|
+
destroy(): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const profiles: Profile[] = [
|
|
25
|
+
{ name: "Maya", role: "Engineering", status: "Ready" },
|
|
26
|
+
{ name: "Ana", role: "Design", status: "Reviewing" },
|
|
27
|
+
{ name: "Luis", role: "Support", status: "Helping customers" }
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function createInitialState(): ComponentCompositionState {
|
|
31
|
+
return { selectedIndex: 0, running: true };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function shouldRunSnapshot() {
|
|
35
|
+
return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function StatusBadge({ status }: { status: string }) {
|
|
39
|
+
return <Text>Status: {status}</Text>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function Greeting({ name }: { name: string }) {
|
|
43
|
+
return <Text>Hello, {name}</Text>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ProfileCard({ profile }: { profile: Profile }) {
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<Greeting name={profile.name} />
|
|
50
|
+
<Text>Role: {profile.role}</Text>
|
|
51
|
+
<StatusBadge status={profile.status} />
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function App({ state }: { state: ComponentCompositionState }) {
|
|
57
|
+
return (
|
|
58
|
+
<Screen title="Components">
|
|
59
|
+
<ProfileCard profile={profiles[state.selectedIndex]} />
|
|
60
|
+
<Text>N/P: switch card Ctrl+C: quit</Text>
|
|
61
|
+
</Screen>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createComponentCompositionDemo(options: TerminalMountOptions = {}): ComponentCompositionDemo {
|
|
66
|
+
const state = createInitialState();
|
|
67
|
+
let session: TerminalSession;
|
|
68
|
+
|
|
69
|
+
function quit() {
|
|
70
|
+
state.running = false;
|
|
71
|
+
session.destroy();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function move(delta: number) {
|
|
75
|
+
state.selectedIndex = (state.selectedIndex + delta + profiles.length) % profiles.length;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
session = mountTerminal(<App state={state} />, {
|
|
79
|
+
...options,
|
|
80
|
+
keymap: {
|
|
81
|
+
...options.keymap,
|
|
82
|
+
bindings: [
|
|
83
|
+
...(options.keymap?.bindings || []),
|
|
84
|
+
{ key: "n", command: { id: "card.next" }, scope: "global" },
|
|
85
|
+
{ key: "N", command: { id: "card.next" }, scope: "global" },
|
|
86
|
+
{ key: "RIGHT", command: { id: "card.next" }, scope: "global" },
|
|
87
|
+
{ key: "p", command: { id: "card.prev" }, scope: "global" },
|
|
88
|
+
{ key: "P", command: { id: "card.prev" }, scope: "global" },
|
|
89
|
+
{ key: "LEFT", command: { id: "card.prev" }, scope: "global" },
|
|
90
|
+
{ key: "CTRL_C", command: { id: "quit" }, scope: "global" }
|
|
91
|
+
],
|
|
92
|
+
onCommand(command, context) {
|
|
93
|
+
if (command.id === "card.next") {
|
|
94
|
+
move(1);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (command.id === "card.prev") {
|
|
98
|
+
move(-1);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (command.id === "quit") {
|
|
102
|
+
quit();
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return options.keymap?.onCommand?.(command, context);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
session,
|
|
112
|
+
dispatchKey(key: string) {
|
|
113
|
+
return session.dispatchKey(key);
|
|
114
|
+
},
|
|
115
|
+
output() {
|
|
116
|
+
return session.output();
|
|
117
|
+
},
|
|
118
|
+
ansiOutput() {
|
|
119
|
+
return session.ansiOutput();
|
|
120
|
+
},
|
|
121
|
+
isRunning() {
|
|
122
|
+
return state.running;
|
|
123
|
+
},
|
|
124
|
+
destroy() {
|
|
125
|
+
state.running = false;
|
|
126
|
+
session.destroy();
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (import.meta.main) {
|
|
132
|
+
if (shouldRunSnapshot()) {
|
|
133
|
+
const demo = createComponentCompositionDemo({ runtime: "headless", cols: 44, rows: 8 });
|
|
134
|
+
process.stdout.write(demo.output());
|
|
135
|
+
process.stdout.write("\n");
|
|
136
|
+
demo.destroy();
|
|
137
|
+
} else {
|
|
138
|
+
createComponentCompositionDemo();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Input, Screen, Text, mountTerminal } from "@valyrianjs/terminal";
|
|
2
|
+
import type { TerminalMountOptions, TerminalSession } from "@valyrianjs/terminal";
|
|
3
|
+
|
|
4
|
+
interface CursorState {
|
|
5
|
+
value: string;
|
|
6
|
+
cursorNote: string;
|
|
7
|
+
running: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CursorDemo {
|
|
11
|
+
session: TerminalSession;
|
|
12
|
+
dispatchKey(key: string): string;
|
|
13
|
+
output(): string;
|
|
14
|
+
ansiOutput(): string;
|
|
15
|
+
isRunning(): boolean;
|
|
16
|
+
destroy(): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createInitialState(): CursorState {
|
|
20
|
+
return {
|
|
21
|
+
value: "ready",
|
|
22
|
+
cursorNote: "Terminal cursor hidden while this app owns the screen",
|
|
23
|
+
running: true
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function shouldRunSnapshot() {
|
|
28
|
+
return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function App({ state }: { state: CursorState }) {
|
|
32
|
+
return (
|
|
33
|
+
<Screen title="Cursor demo">
|
|
34
|
+
<Text>Cursor demo</Text>
|
|
35
|
+
<Text>App hides the terminal cursor while it owns the screen.</Text>
|
|
36
|
+
<Text>Focused input keeps its editing cursor visible.</Text>
|
|
37
|
+
<Input
|
|
38
|
+
id="search"
|
|
39
|
+
value={state.value}
|
|
40
|
+
placeholder="Type here"
|
|
41
|
+
onchange={(event) => {
|
|
42
|
+
state.value = event.value;
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
<Text>{`Input value: ${state.value}`}</Text>
|
|
46
|
+
<Text>{state.cursorNote}</Text>
|
|
47
|
+
<Text>Type: edit field | C: cursor note | Ctrl+C: quit</Text>
|
|
48
|
+
</Screen>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createCursorDemo(options: TerminalMountOptions = {}): CursorDemo {
|
|
53
|
+
const state = createInitialState();
|
|
54
|
+
let session: TerminalSession;
|
|
55
|
+
|
|
56
|
+
function quit() {
|
|
57
|
+
state.running = false;
|
|
58
|
+
session.destroy();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
session = mountTerminal(<App state={state} />, {
|
|
62
|
+
hideCursor: true,
|
|
63
|
+
restoreOnDestroy: true,
|
|
64
|
+
...options,
|
|
65
|
+
keymap: {
|
|
66
|
+
...options.keymap,
|
|
67
|
+
bindings: [
|
|
68
|
+
...(options.keymap?.bindings || []),
|
|
69
|
+
{ key: "c", command: { id: "cursor.note" }, scope: "global" },
|
|
70
|
+
{ key: "C", command: { id: "cursor.note" }, scope: "global" },
|
|
71
|
+
{ key: "CTRL_C", command: { id: "quit" }, scope: "global" }
|
|
72
|
+
],
|
|
73
|
+
onCommand(command, context) {
|
|
74
|
+
if (command.id === "cursor.note") {
|
|
75
|
+
state.cursorNote = state.cursorNote.startsWith("Terminal cursor hidden")
|
|
76
|
+
? "Terminal restores the cursor when the app closes"
|
|
77
|
+
: "Terminal cursor hidden while this app owns the screen";
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
if (command.id === "quit") {
|
|
81
|
+
quit();
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return options.keymap?.onCommand?.(command, context);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
session.focus("search");
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
session,
|
|
93
|
+
dispatchKey(key: string) {
|
|
94
|
+
return session.dispatchKey(key);
|
|
95
|
+
},
|
|
96
|
+
output() {
|
|
97
|
+
return session.output();
|
|
98
|
+
},
|
|
99
|
+
ansiOutput() {
|
|
100
|
+
return session.ansiOutput();
|
|
101
|
+
},
|
|
102
|
+
isRunning() {
|
|
103
|
+
return state.running;
|
|
104
|
+
},
|
|
105
|
+
destroy() {
|
|
106
|
+
state.running = false;
|
|
107
|
+
session.destroy();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (import.meta.main) {
|
|
113
|
+
if (shouldRunSnapshot()) {
|
|
114
|
+
const demo = createCursorDemo({ runtime: "headless", cols: 56, rows: 7 });
|
|
115
|
+
process.stdout.write(demo.output());
|
|
116
|
+
process.stdout.write("\n");
|
|
117
|
+
demo.destroy();
|
|
118
|
+
} else {
|
|
119
|
+
createCursorDemo();
|
|
120
|
+
}
|
|
121
|
+
}
|