silvery 0.3.0 → 0.4.1
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/README.md +41 -145
- package/dist/chalk.js +3 -0
- package/dist/chalk.js.map +11 -0
- package/dist/index.js +340 -0
- package/dist/index.js.map +282 -0
- package/dist/ink.js +129 -0
- package/dist/ink.js.map +140 -0
- package/dist/runtime.js +394 -0
- package/dist/runtime.js.map +286 -0
- package/dist/theme.js +343 -0
- package/dist/theme.js.map +286 -0
- package/dist/ui/animation.js +3 -0
- package/dist/ui/animation.js.map +15 -0
- package/dist/ui/ansi.js +3 -0
- package/dist/ui/ansi.js.map +10 -0
- package/dist/ui/cli.js +8 -0
- package/dist/ui/cli.js.map +14 -0
- package/dist/ui/display.js +4 -0
- package/dist/ui/display.js.map +10 -0
- package/dist/ui/image.js +4 -0
- package/dist/ui/image.js.map +15 -0
- package/dist/ui/input.js +3 -0
- package/dist/ui/input.js.map +11 -0
- package/dist/ui/progress.js +8 -0
- package/dist/ui/progress.js.map +20 -0
- package/dist/ui/react.js +3 -0
- package/dist/ui/react.js.map +15 -0
- package/dist/ui/utils.js +3 -0
- package/dist/ui/utils.js.map +10 -0
- package/dist/ui/wrappers.js +14 -0
- package/dist/ui/wrappers.js.map +19 -0
- package/dist/ui.js +17 -0
- package/dist/ui.js.map +20 -0
- package/package.json +67 -15
- package/src/index.ts +67 -1
- package/src/runtime.ts +4 -0
- package/src/theme.ts +4 -0
- package/src/ui/animation.ts +2 -0
- package/src/ui/ansi.ts +2 -0
- package/src/ui/cli.ts +2 -0
- package/src/ui/display.ts +2 -0
- package/src/ui/image.ts +2 -0
- package/src/ui/input.ts +2 -0
- package/src/ui/progress.ts +2 -0
- package/src/ui/react.ts +2 -0
- package/src/ui/utils.ts +2 -0
- package/src/ui/wrappers.ts +2 -0
- package/src/ui.ts +4 -0
- package/examples/CLAUDE.md +0 -75
- package/examples/_banner.tsx +0 -60
- package/examples/cli.ts +0 -228
- package/examples/index.md +0 -101
- package/examples/inline/inline-nontty.tsx +0 -98
- package/examples/inline/inline-progress.tsx +0 -79
- package/examples/inline/inline-simple.tsx +0 -63
- package/examples/inline/scrollback.tsx +0 -185
- package/examples/interactive/_input-debug.tsx +0 -110
- package/examples/interactive/_stdin-test.ts +0 -71
- package/examples/interactive/_textarea-bare.tsx +0 -45
- package/examples/interactive/aichat/components.tsx +0 -468
- package/examples/interactive/aichat/index.tsx +0 -207
- package/examples/interactive/aichat/script.ts +0 -460
- package/examples/interactive/aichat/state.ts +0 -326
- package/examples/interactive/aichat/types.ts +0 -19
- package/examples/interactive/app-todo.tsx +0 -198
- package/examples/interactive/async-data.tsx +0 -208
- package/examples/interactive/cli-wizard.tsx +0 -332
- package/examples/interactive/clipboard.tsx +0 -183
- package/examples/interactive/components.tsx +0 -463
- package/examples/interactive/data-explorer.tsx +0 -506
- package/examples/interactive/dev-tools.tsx +0 -379
- package/examples/interactive/explorer.tsx +0 -747
- package/examples/interactive/gallery.tsx +0 -652
- package/examples/interactive/inline-bench.tsx +0 -136
- package/examples/interactive/kanban.tsx +0 -267
- package/examples/interactive/layout-ref.tsx +0 -185
- package/examples/interactive/outline.tsx +0 -171
- package/examples/interactive/paste-demo.tsx +0 -198
- package/examples/interactive/scroll.tsx +0 -77
- package/examples/interactive/search-filter.tsx +0 -240
- package/examples/interactive/task-list.tsx +0 -279
- package/examples/interactive/terminal.tsx +0 -798
- package/examples/interactive/textarea.tsx +0 -103
- package/examples/interactive/theme.tsx +0 -336
- package/examples/interactive/transform.tsx +0 -256
- package/examples/interactive/virtual-10k.tsx +0 -413
- package/examples/kitty/canvas.tsx +0 -519
- package/examples/kitty/generate-samples.ts +0 -236
- package/examples/kitty/image-component.tsx +0 -273
- package/examples/kitty/images.tsx +0 -604
- package/examples/kitty/input.tsx +0 -371
- package/examples/kitty/keys.tsx +0 -378
- package/examples/kitty/paint.tsx +0 -1017
- package/examples/layout/dashboard.tsx +0 -551
- package/examples/layout/live-resize.tsx +0 -290
- package/examples/layout/overflow.tsx +0 -51
- package/examples/playground/README.md +0 -69
- package/examples/playground/build.ts +0 -61
- package/examples/playground/index.html +0 -420
- package/examples/playground/playground-app.tsx +0 -416
- package/examples/runtime/elm-counter.tsx +0 -206
- package/examples/runtime/hello-runtime.tsx +0 -73
- package/examples/runtime/pipe-composition.tsx +0 -184
- package/examples/runtime/run-counter.tsx +0 -78
- package/examples/runtime/runtime-counter.tsx +0 -197
- package/examples/screenshots/generate.tsx +0 -563
- package/examples/scrollback-perf.tsx +0 -230
- package/examples/viewer.tsx +0 -654
- package/examples/web/build.ts +0 -365
- package/examples/web/canvas-app.tsx +0 -80
- package/examples/web/canvas.html +0 -89
- package/examples/web/dom-app.tsx +0 -81
- package/examples/web/dom.html +0 -113
- package/examples/web/showcase-app.tsx +0 -107
- package/examples/web/showcase.html +0 -34
- package/examples/web/showcases/index.tsx +0 -56
- package/examples/web/viewer-app.tsx +0 -555
- package/examples/web/viewer.html +0 -30
- package/examples/web/xterm-app.tsx +0 -105
- package/examples/web/xterm.html +0 -118
package/examples/index.md
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# Silvery Examples
|
|
2
|
-
|
|
3
|
-
Interactive examples demonstrating Silvery features. Organized by category.
|
|
4
|
-
|
|
5
|
-
## Running Examples
|
|
6
|
-
|
|
7
|
-
Browse all examples in the interactive viewer:
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
bun examples
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Run any example standalone:
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
bun examples/<category>/<name>.tsx
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Structure
|
|
20
|
-
|
|
21
|
-
Examples are organized into category directories. Each exports a `meta` object
|
|
22
|
-
with `name` and `description`. The viewer auto-discovers all examples — no
|
|
23
|
-
registry to maintain.
|
|
24
|
-
|
|
25
|
-
```
|
|
26
|
-
examples/
|
|
27
|
-
_banner.tsx # Shared banner component (not an example)
|
|
28
|
-
viewer.tsx # Interactive example browser
|
|
29
|
-
layout/ # Layout and responsive design
|
|
30
|
-
interactive/ # Keyboard-driven interactive apps
|
|
31
|
-
kitty/ # Kitty protocol features (graphics, keyboard)
|
|
32
|
-
runtime/ # Runtime layer demos (Layer 1-3)
|
|
33
|
-
inline/ # Inline mode and scrollback
|
|
34
|
-
playground/ # Web playground (not an example)
|
|
35
|
-
screenshots/ # Screenshot generation tool
|
|
36
|
-
web/ # Web render targets (canvas, DOM)
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Layout
|
|
40
|
-
|
|
41
|
-
| Example | File | Description |
|
|
42
|
-
| ----------- | ------------------------ | --------------------------------------------- |
|
|
43
|
-
| Dashboard | `layout/dashboard.tsx` | Multi-pane dashboard with keyboard navigation |
|
|
44
|
-
| Live Resize | `layout/live-resize.tsx` | Responsive columns via `useContentRect()` |
|
|
45
|
-
| Overflow | `layout/overflow.tsx` | `overflow="hidden"` content clipping |
|
|
46
|
-
|
|
47
|
-
## Interactive
|
|
48
|
-
|
|
49
|
-
| Example | File | Description |
|
|
50
|
-
| --------------- | ------------------------------- | ---------------------------------------------- |
|
|
51
|
-
| AI Coding Agent | `interactive/aichat/` | Coding agent with streaming, tool calls |
|
|
52
|
-
| Todo App | `interactive/app-todo.tsx` | Layer 3: `createApp()` with Zustand store |
|
|
53
|
-
| Async Data | `interactive/async-data.tsx` | Suspense boundaries with `use()` hook |
|
|
54
|
-
| Kanban | `interactive/kanban.tsx` | Multi-column kanban with card movement |
|
|
55
|
-
| Layout Ref | `interactive/layout-ref.tsx` | `forwardRef` + `onLayout` callbacks |
|
|
56
|
-
| Scroll | `interactive/scroll.tsx` | Basic scrollable list |
|
|
57
|
-
| Search Filter | `interactive/search-filter.tsx` | React concurrent features (`useDeferredValue`) |
|
|
58
|
-
| Task List | `interactive/task-list.tsx` | VirtualList with variable-height items |
|
|
59
|
-
| TextArea | `interactive/textarea.tsx` | Multi-line text input component |
|
|
60
|
-
| Virtual 10K | `interactive/virtual-10k.tsx` | VirtualList with 10,000 items |
|
|
61
|
-
| Clipboard | `interactive/clipboard.tsx` | OSC 52 clipboard copy/paste across sessions |
|
|
62
|
-
| Paste Demo | `interactive/paste-demo.tsx` | Bracketed paste mode — paste as single event |
|
|
63
|
-
| Outline | `interactive/outline.tsx` | Outline vs border side-by-side comparison |
|
|
64
|
-
| Transform | `interactive/transform.tsx` | Text post-processing with Transform component |
|
|
65
|
-
|
|
66
|
-
## Kitty Protocol
|
|
67
|
-
|
|
68
|
-
| Example | File | Description |
|
|
69
|
-
| --------------- | --------------------------- | ------------------------------------------------ |
|
|
70
|
-
| Image Viewer | `kitty/images.tsx` | Raw Kitty graphics protocol image display |
|
|
71
|
-
| Image Component | `kitty/image-component.tsx` | Declarative `<Image>` with protocol auto-detect |
|
|
72
|
-
| Key Events | `kitty/keys.tsx` | Interactive key chord tester with Kitty protocol |
|
|
73
|
-
| Input | `kitty/input.tsx` | Kitty keyboard input demonstration |
|
|
74
|
-
| Canvas | `kitty/canvas.tsx` | Canvas rendering via Kitty graphics |
|
|
75
|
-
| Paint | `kitty/paint.tsx` | Terminal paint app using Kitty graphics |
|
|
76
|
-
|
|
77
|
-
## Runtime
|
|
78
|
-
|
|
79
|
-
| Example | File | Description |
|
|
80
|
-
| --------------- | ----------------------------- | ------------------------------------------------ |
|
|
81
|
-
| Elm Counter | `runtime/elm-counter.tsx` | Layer 1: `createRuntime()` with Elm architecture |
|
|
82
|
-
| Hello Runtime | `runtime/hello-runtime.tsx` | Layer 1: minimal static render |
|
|
83
|
-
| Run Counter | `runtime/run-counter.tsx` | Layer 2: `run()` with React hooks |
|
|
84
|
-
| Runtime Counter | `runtime/runtime-counter.tsx` | Layer 1: `createRuntime()` with event loop |
|
|
85
|
-
|
|
86
|
-
## Inline
|
|
87
|
-
|
|
88
|
-
| Example | File | Description |
|
|
89
|
-
| --------------- | ---------------------------- | --------------------------------------------------- |
|
|
90
|
-
| Inline Simple | `inline/inline-simple.tsx` | Basic inline rendering |
|
|
91
|
-
| Inline Progress | `inline/inline-progress.tsx` | Inline progress bar |
|
|
92
|
-
| Inline Non-TTY | `inline/inline-nontty.tsx` | Inline output for piped/non-TTY |
|
|
93
|
-
| Scrollback | `inline/scrollback.tsx` | REPL with `useScrollback` + VirtualList virtualized |
|
|
94
|
-
|
|
95
|
-
## Creating New Examples
|
|
96
|
-
|
|
97
|
-
1. Add a `.tsx` file in the appropriate category directory
|
|
98
|
-
2. Export a `meta` object: `export const meta: ExampleMeta = { name: "...", description: "..." }`
|
|
99
|
-
3. Export your main component as a named function
|
|
100
|
-
4. Wrap with `ExampleBanner` in the `import.meta.main` block for standalone mode
|
|
101
|
-
5. The viewer discovers it automatically — no registry to update
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env tsx
|
|
2
|
-
/**
|
|
3
|
-
* Example: Non-TTY Mode Support (km-silvery-nontty)
|
|
4
|
-
*
|
|
5
|
-
* Demonstrates silvery's non-TTY mode support for rendering in environments
|
|
6
|
-
* without a terminal (pipes, CI, TERM=dumb).
|
|
7
|
-
*
|
|
8
|
-
* Run this example:
|
|
9
|
-
* # Normal TTY mode
|
|
10
|
-
* bun examples/inline-nontty.tsx
|
|
11
|
-
*
|
|
12
|
-
* # Piped output (auto-detects non-TTY)
|
|
13
|
-
* bun examples/inline-nontty.tsx | cat
|
|
14
|
-
*
|
|
15
|
-
* # Force plain text mode
|
|
16
|
-
* SILVERY_NONTTY=plain bun examples/inline-nontty.tsx
|
|
17
|
-
*
|
|
18
|
-
* # Force line-by-line mode
|
|
19
|
-
* SILVERY_NONTTY=line-by-line bun examples/inline-nontty.tsx
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import React, { useEffect, useState } from "react"
|
|
23
|
-
import { Box, render, Text, useApp, createTerm, type NonTTYMode } from "../../src/index.js"
|
|
24
|
-
import type { ExampleMeta } from "../_banner.js"
|
|
25
|
-
|
|
26
|
-
export const meta: ExampleMeta = {
|
|
27
|
-
name: "Non-TTY Mode",
|
|
28
|
-
description: "Graceful degradation for pipes, CI, and TERM=dumb",
|
|
29
|
-
features: ["renderString()", "non-TTY output", "pipe-safe"],
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function ProgressExample() {
|
|
33
|
-
const { exit } = useApp()
|
|
34
|
-
const [progress, setProgress] = useState(0)
|
|
35
|
-
const [done, setDone] = useState(false)
|
|
36
|
-
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
const timer = setInterval(() => {
|
|
39
|
-
setProgress((prev) => {
|
|
40
|
-
const next = prev + 20
|
|
41
|
-
if (next >= 100) {
|
|
42
|
-
setDone(true)
|
|
43
|
-
clearInterval(timer)
|
|
44
|
-
return 100
|
|
45
|
-
}
|
|
46
|
-
return next
|
|
47
|
-
})
|
|
48
|
-
}, 300)
|
|
49
|
-
|
|
50
|
-
return () => clearInterval(timer)
|
|
51
|
-
}, [])
|
|
52
|
-
|
|
53
|
-
// Exit cleanly after showing "Complete!" for a moment
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
if (!done) return
|
|
56
|
-
const timeout = setTimeout(() => exit(), 300)
|
|
57
|
-
return () => clearTimeout(timeout)
|
|
58
|
-
}, [done, exit])
|
|
59
|
-
|
|
60
|
-
const barWidth = 30
|
|
61
|
-
const filled = Math.floor((progress / 100) * barWidth)
|
|
62
|
-
const bar = "#".repeat(filled) + "-".repeat(barWidth - filled)
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<Box flexDirection="column">
|
|
66
|
-
<Text>Processing files...</Text>
|
|
67
|
-
<Text>
|
|
68
|
-
[{bar}] {progress}%
|
|
69
|
-
</Text>
|
|
70
|
-
{done && <Text color="green">Complete!</Text>}
|
|
71
|
-
</Box>
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function main() {
|
|
76
|
-
// Determine non-TTY mode from environment
|
|
77
|
-
const envMode = process.env.SILVERY_NONTTY as NonTTYMode | undefined
|
|
78
|
-
const nonTTYMode = envMode || "auto"
|
|
79
|
-
|
|
80
|
-
console.log(`Non-TTY mode: ${nonTTYMode}`)
|
|
81
|
-
console.log(`stdout.isTTY: ${process.stdout.isTTY}`)
|
|
82
|
-
console.log("---\n")
|
|
83
|
-
|
|
84
|
-
using term = createTerm()
|
|
85
|
-
const { waitUntilExit } = await render(<ProgressExample />, term, {
|
|
86
|
-
mode: "inline",
|
|
87
|
-
nonTTYMode,
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
await waitUntilExit()
|
|
91
|
-
|
|
92
|
-
console.log("\n---")
|
|
93
|
-
console.log("Done!")
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (import.meta.main) {
|
|
97
|
-
main().catch(console.error)
|
|
98
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env tsx
|
|
2
|
-
/**
|
|
3
|
-
* Example: Inline Progress Indicator
|
|
4
|
-
*
|
|
5
|
-
* Demonstrates the new inline mode for progress bars and status indicators.
|
|
6
|
-
* Unlike fullscreen mode, inline mode renders from the current cursor position
|
|
7
|
-
* and updates in place using relative cursor positioning.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import React, { useState, useEffect } from "react"
|
|
11
|
-
import { render, Box, Text, useApp, createTerm } from "../../src/index.js"
|
|
12
|
-
import type { ExampleMeta } from "../_banner.js"
|
|
13
|
-
|
|
14
|
-
export const meta: ExampleMeta = {
|
|
15
|
-
name: "Inline Progress",
|
|
16
|
-
description: "Inline progress bar updating in place",
|
|
17
|
-
features: ["render() inline mode", "setInterval updates"],
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function InlineProgress() {
|
|
21
|
-
const { exit } = useApp()
|
|
22
|
-
const [progress, setProgress] = useState(0)
|
|
23
|
-
const [status, setStatus] = useState("Starting...")
|
|
24
|
-
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
const timer = setInterval(() => {
|
|
27
|
-
setProgress((prev) => {
|
|
28
|
-
const next = prev + 10
|
|
29
|
-
if (next >= 100) {
|
|
30
|
-
setStatus("Complete!")
|
|
31
|
-
clearInterval(timer)
|
|
32
|
-
return 100
|
|
33
|
-
}
|
|
34
|
-
setStatus(`Processing... ${next}%`)
|
|
35
|
-
return next
|
|
36
|
-
})
|
|
37
|
-
}, 500)
|
|
38
|
-
|
|
39
|
-
return () => clearInterval(timer)
|
|
40
|
-
}, [])
|
|
41
|
-
|
|
42
|
-
// Exit cleanly after showing "Complete!" for a moment
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
if (progress < 100) return
|
|
45
|
-
const timeout = setTimeout(() => exit(), 300)
|
|
46
|
-
return () => clearTimeout(timeout)
|
|
47
|
-
}, [progress, exit])
|
|
48
|
-
|
|
49
|
-
const barWidth = 40
|
|
50
|
-
const filled = Math.floor((progress / 100) * barWidth)
|
|
51
|
-
const bar = "█".repeat(filled) + "░".repeat(barWidth - filled)
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<Box flexDirection="column">
|
|
55
|
-
<Text>{status}</Text>
|
|
56
|
-
<Text>
|
|
57
|
-
[{bar}] {progress}%
|
|
58
|
-
</Text>
|
|
59
|
-
</Box>
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function main() {
|
|
64
|
-
console.log("This is regular console output before the progress bar.\n")
|
|
65
|
-
|
|
66
|
-
using term = createTerm()
|
|
67
|
-
const { waitUntilExit } = await render(<InlineProgress />, term, {
|
|
68
|
-
mode: "inline",
|
|
69
|
-
exitOnCtrlC: true,
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
await waitUntilExit()
|
|
73
|
-
|
|
74
|
-
console.log("\nProgress complete! This is output after the progress bar.")
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (import.meta.main) {
|
|
78
|
-
main().catch(console.error)
|
|
79
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* Simple inline mode test
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React, { useState, useEffect } from "react"
|
|
7
|
-
import { render, Box, Text, useApp, createTerm } from "../../src/index.js"
|
|
8
|
-
import type { ExampleMeta } from "../_banner.js"
|
|
9
|
-
|
|
10
|
-
export const meta: ExampleMeta = {
|
|
11
|
-
name: "Inline Simple",
|
|
12
|
-
description: "Inline rendering from current cursor position",
|
|
13
|
-
features: ["render() inline mode", "one-shot output"],
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function Counter() {
|
|
17
|
-
const { exit } = useApp()
|
|
18
|
-
const [count, setCount] = useState(0)
|
|
19
|
-
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
const timer = setInterval(() => {
|
|
22
|
-
setCount((c) => {
|
|
23
|
-
if (c >= 5) {
|
|
24
|
-
clearInterval(timer)
|
|
25
|
-
return c
|
|
26
|
-
}
|
|
27
|
-
return c + 1
|
|
28
|
-
})
|
|
29
|
-
}, 500)
|
|
30
|
-
|
|
31
|
-
return () => clearInterval(timer)
|
|
32
|
-
}, [])
|
|
33
|
-
|
|
34
|
-
// Exit cleanly after count reaches 5
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
if (count < 5) return
|
|
37
|
-
const timeout = setTimeout(() => exit(), 300)
|
|
38
|
-
return () => clearTimeout(timeout)
|
|
39
|
-
}, [count, exit])
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<Box>
|
|
43
|
-
<Text>Count: {count}</Text>
|
|
44
|
-
</Box>
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function main() {
|
|
49
|
-
console.log("Before\n")
|
|
50
|
-
|
|
51
|
-
using term = createTerm()
|
|
52
|
-
const { waitUntilExit } = await render(<Counter />, term, {
|
|
53
|
-
mode: "inline",
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
await waitUntilExit()
|
|
57
|
-
|
|
58
|
-
console.log("\nAfter")
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (import.meta.main) {
|
|
62
|
-
main().catch(console.error)
|
|
63
|
-
}
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scrollback Mode — REPL
|
|
3
|
-
*
|
|
4
|
-
* Interactive expression evaluator demonstrating useScrollback + VirtualList virtualized.
|
|
5
|
-
* Completed results freeze into terminal scrollback; the active prompt stays at bottom.
|
|
6
|
-
*
|
|
7
|
-
* Controls:
|
|
8
|
-
* Type expression + Enter - Evaluate
|
|
9
|
-
* q (when input empty) - Quit
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import React, { useState, useCallback } from "react"
|
|
13
|
-
import { Box, Text, Divider, VirtualList, useInput, type Key } from "../../src/index.js"
|
|
14
|
-
import { run, useExit } from "@silvery/term/runtime"
|
|
15
|
-
import { useScrollback } from "../../src/hooks/useScrollback.js"
|
|
16
|
-
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
17
|
-
|
|
18
|
-
export const meta: ExampleMeta = {
|
|
19
|
-
name: "Scrollback",
|
|
20
|
-
description: "REPL with useScrollback + VirtualList virtualized for terminal scrollback",
|
|
21
|
-
features: ["useScrollback()", "VirtualList virtualized", "inline mode"],
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// =============================================================================
|
|
25
|
-
// Data
|
|
26
|
-
// =============================================================================
|
|
27
|
-
|
|
28
|
-
interface Result {
|
|
29
|
-
id: number
|
|
30
|
-
expr: string
|
|
31
|
-
value: string
|
|
32
|
-
frozen: boolean
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let nextId = 0
|
|
36
|
-
|
|
37
|
-
function evaluate(expr: string): string {
|
|
38
|
-
try {
|
|
39
|
-
// eslint-disable-next-line no-eval
|
|
40
|
-
return String(eval(expr))
|
|
41
|
-
} catch (e: unknown) {
|
|
42
|
-
return `Error: ${e instanceof Error ? e.message : String(e)}`
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// =============================================================================
|
|
47
|
-
// Component
|
|
48
|
-
// =============================================================================
|
|
49
|
-
|
|
50
|
-
export function Repl() {
|
|
51
|
-
const exit = useExit()
|
|
52
|
-
const [results, setResults] = useState<Result[]>([])
|
|
53
|
-
const [input, setInput] = useState("")
|
|
54
|
-
const [cursor, setCursor] = useState(0)
|
|
55
|
-
|
|
56
|
-
// Push frozen results to terminal scrollback
|
|
57
|
-
const frozenCount = useScrollback(results, {
|
|
58
|
-
frozen: (r) => r.frozen,
|
|
59
|
-
render: (r) => `$ ${r.expr}\n→ ${r.value}`,
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
const submit = useCallback(() => {
|
|
63
|
-
const expr = input.trim()
|
|
64
|
-
if (!expr) return
|
|
65
|
-
|
|
66
|
-
const value = evaluate(expr)
|
|
67
|
-
const id = nextId++
|
|
68
|
-
|
|
69
|
-
// Mark all existing results as frozen, add new one unfrozen
|
|
70
|
-
setResults((prev) => [...prev.map((r) => ({ ...r, frozen: true })), { id, expr, value, frozen: false }])
|
|
71
|
-
setInput("")
|
|
72
|
-
setCursor(0)
|
|
73
|
-
}, [input])
|
|
74
|
-
|
|
75
|
-
useInput((ch: string, key: Key) => {
|
|
76
|
-
if (key.return) {
|
|
77
|
-
submit()
|
|
78
|
-
return
|
|
79
|
-
}
|
|
80
|
-
if (key.escape || (ch === "q" && input === "")) {
|
|
81
|
-
exit()
|
|
82
|
-
return
|
|
83
|
-
}
|
|
84
|
-
if (key.backspace) {
|
|
85
|
-
if (cursor > 0) {
|
|
86
|
-
setInput((v) => v.slice(0, cursor - 1) + v.slice(cursor))
|
|
87
|
-
setCursor((c) => c - 1)
|
|
88
|
-
}
|
|
89
|
-
return
|
|
90
|
-
}
|
|
91
|
-
if (key.leftArrow) {
|
|
92
|
-
setCursor((c) => Math.max(0, c - 1))
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
if (key.rightArrow) {
|
|
96
|
-
setCursor((c) => Math.min(input.length, c + 1))
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
// Ctrl+A: beginning of line
|
|
100
|
-
if (key.ctrl && ch === "a") {
|
|
101
|
-
setCursor(0)
|
|
102
|
-
return
|
|
103
|
-
}
|
|
104
|
-
// Ctrl+E: end of line
|
|
105
|
-
if (key.ctrl && ch === "e") {
|
|
106
|
-
setCursor(input.length)
|
|
107
|
-
return
|
|
108
|
-
}
|
|
109
|
-
// Ctrl+U: clear line
|
|
110
|
-
if (key.ctrl && ch === "u") {
|
|
111
|
-
setInput("")
|
|
112
|
-
setCursor(0)
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
if (ch >= " ") {
|
|
116
|
-
setInput((v) => v.slice(0, cursor) + ch + v.slice(cursor))
|
|
117
|
-
setCursor((c) => c + 1)
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
const activeCount = results.length - frozenCount
|
|
122
|
-
const beforeCursor = input.slice(0, cursor)
|
|
123
|
-
const atCursor = input[cursor] ?? " "
|
|
124
|
-
const afterCursor = input.slice(cursor + 1)
|
|
125
|
-
|
|
126
|
-
return (
|
|
127
|
-
<Box flexDirection="column">
|
|
128
|
-
{/* Active (non-virtualized) results via VirtualList */}
|
|
129
|
-
{activeCount > 0 && (
|
|
130
|
-
<VirtualList
|
|
131
|
-
items={results}
|
|
132
|
-
virtualized={(r) => r.frozen}
|
|
133
|
-
height={activeCount * 2}
|
|
134
|
-
itemHeight={2}
|
|
135
|
-
scrollTo={0}
|
|
136
|
-
renderItem={(r) => (
|
|
137
|
-
<Box key={r.id} flexDirection="column">
|
|
138
|
-
<Text>
|
|
139
|
-
<Text color="gray">{"$ "}</Text>
|
|
140
|
-
<Text>{r.expr}</Text>
|
|
141
|
-
</Text>
|
|
142
|
-
<Text>
|
|
143
|
-
<Text color="cyan">{"→ "}</Text>
|
|
144
|
-
<Text>{r.value}</Text>
|
|
145
|
-
</Text>
|
|
146
|
-
</Box>
|
|
147
|
-
)}
|
|
148
|
-
/>
|
|
149
|
-
)}
|
|
150
|
-
|
|
151
|
-
{/* Separator */}
|
|
152
|
-
<Divider />
|
|
153
|
-
|
|
154
|
-
{/* Input prompt */}
|
|
155
|
-
<Text>
|
|
156
|
-
<Text color="yellow">{"› "}</Text>
|
|
157
|
-
<Text>{beforeCursor}</Text>
|
|
158
|
-
<Text inverse>{atCursor}</Text>
|
|
159
|
-
<Text>{afterCursor}</Text>
|
|
160
|
-
</Text>
|
|
161
|
-
|
|
162
|
-
{/* Status */}
|
|
163
|
-
<Text dim>
|
|
164
|
-
{results.length} result{results.length !== 1 ? "s" : ""} | Esc/q to quit
|
|
165
|
-
</Text>
|
|
166
|
-
</Box>
|
|
167
|
-
)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// =============================================================================
|
|
171
|
-
// Main
|
|
172
|
-
// =============================================================================
|
|
173
|
-
|
|
174
|
-
async function main() {
|
|
175
|
-
await run(
|
|
176
|
-
<ExampleBanner meta={meta} controls="Type expr + Enter Esc/q quit">
|
|
177
|
-
<Repl />
|
|
178
|
-
</ExampleBanner>,
|
|
179
|
-
{ mode: "inline" },
|
|
180
|
-
)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (import.meta.main) {
|
|
184
|
-
main().catch(console.error)
|
|
185
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Input Debug Tool
|
|
3
|
-
*
|
|
4
|
-
* Minimal diagnostic to find where keypresses are lost.
|
|
5
|
-
* Shows every event received by useInput + TextArea side by side.
|
|
6
|
-
*
|
|
7
|
-
* Run: bun vendor/silvery/examples/interactive/_input-debug.tsx
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import React, { useState, useRef } from "react"
|
|
11
|
-
import { render, Box, Text, TextArea, useInput, useApp, createTerm, type Key } from "../../src/index.js"
|
|
12
|
-
|
|
13
|
-
function InputDebug(): JSX.Element {
|
|
14
|
-
const { exit } = useApp()
|
|
15
|
-
|
|
16
|
-
// Track raw useInput events
|
|
17
|
-
const [rawEvents, setRawEvents] = useState<string[]>([])
|
|
18
|
-
const rawCountRef = useRef(0)
|
|
19
|
-
|
|
20
|
-
// Track TextArea value
|
|
21
|
-
const [textValue, setTextValue] = useState("")
|
|
22
|
-
const textChangeCountRef = useRef(0)
|
|
23
|
-
|
|
24
|
-
// Raw useInput handler — logs EVERY event
|
|
25
|
-
useInput((input: string, key: Key) => {
|
|
26
|
-
if (key.escape) {
|
|
27
|
-
exit()
|
|
28
|
-
return
|
|
29
|
-
}
|
|
30
|
-
rawCountRef.current++
|
|
31
|
-
const desc = describeKey(input, key)
|
|
32
|
-
setRawEvents((prev) => [...prev.slice(-15), `#${rawCountRef.current} ${desc}`])
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
// TextArea onChange handler
|
|
36
|
-
function handleChange(value: string) {
|
|
37
|
-
textChangeCountRef.current++
|
|
38
|
-
setTextValue(value)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<Box flexDirection="column" padding={1}>
|
|
43
|
-
<Text bold color="cyan">
|
|
44
|
-
Input Pipeline Diagnostic
|
|
45
|
-
</Text>
|
|
46
|
-
<Text dim>Type slowly (1 char/2sec). Compare left (raw events) vs right (TextArea value).</Text>
|
|
47
|
-
<Text dim>Press Esc to quit.</Text>
|
|
48
|
-
<Box height={1} />
|
|
49
|
-
|
|
50
|
-
<Box gap={4}>
|
|
51
|
-
{/* Left: Raw useInput events */}
|
|
52
|
-
<Box flexDirection="column" width={40}>
|
|
53
|
-
<Text bold color="yellow">
|
|
54
|
-
useInput events: {rawCountRef.current}
|
|
55
|
-
</Text>
|
|
56
|
-
{rawEvents.map((e, i) => (
|
|
57
|
-
<Text key={i} dimColor={i < rawEvents.length - 1}>
|
|
58
|
-
{e}
|
|
59
|
-
</Text>
|
|
60
|
-
))}
|
|
61
|
-
</Box>
|
|
62
|
-
|
|
63
|
-
{/* Right: TextArea */}
|
|
64
|
-
<Box flexDirection="column" width={40}>
|
|
65
|
-
<Text bold color="green">
|
|
66
|
-
TextArea value ({textValue.length} chars, {textChangeCountRef.current} changes):
|
|
67
|
-
</Text>
|
|
68
|
-
<Box borderStyle="single" borderColor="green">
|
|
69
|
-
<Box paddingX={1}>
|
|
70
|
-
<TextArea value={textValue} onChange={handleChange} height={4} placeholder="Type here..." />
|
|
71
|
-
</Box>
|
|
72
|
-
</Box>
|
|
73
|
-
<Box marginTop={1}>
|
|
74
|
-
<Text>Value: {JSON.stringify(textValue)}</Text>
|
|
75
|
-
</Box>
|
|
76
|
-
</Box>
|
|
77
|
-
</Box>
|
|
78
|
-
</Box>
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function describeKey(input: string, key: Key): string {
|
|
83
|
-
const parts: string[] = []
|
|
84
|
-
if (key.ctrl) parts.push("Ctrl")
|
|
85
|
-
if (key.meta) parts.push("Meta")
|
|
86
|
-
if (key.shift) parts.push("Shift")
|
|
87
|
-
|
|
88
|
-
if (key.return) parts.push("Enter")
|
|
89
|
-
else if (key.escape) parts.push("Esc")
|
|
90
|
-
else if (key.backspace) parts.push("BS")
|
|
91
|
-
else if (key.delete) parts.push("Del")
|
|
92
|
-
else if (key.upArrow) parts.push("Up")
|
|
93
|
-
else if (key.downArrow) parts.push("Down")
|
|
94
|
-
else if (key.leftArrow) parts.push("Left")
|
|
95
|
-
else if (key.rightArrow) parts.push("Right")
|
|
96
|
-
else if (key.tab) parts.push("Tab")
|
|
97
|
-
else if (input.length === 1 && input >= " ") parts.push(`'${input}'`)
|
|
98
|
-
else if (input) parts.push(`raw:${JSON.stringify(input)}`)
|
|
99
|
-
else parts.push("(empty)")
|
|
100
|
-
|
|
101
|
-
return parts.join("+")
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function main() {
|
|
105
|
-
using term = createTerm()
|
|
106
|
-
const { waitUntilExit } = await render(<InputDebug />, term)
|
|
107
|
-
await waitUntilExit()
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
main().catch(console.error)
|