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
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generate sample PNG images for the Kitty protocol examples.
|
|
3
|
-
*
|
|
4
|
-
* Creates 3 sample images in examples/kitty/samples/:
|
|
5
|
-
* - gradient.png — Rainbow gradient (256x192)
|
|
6
|
-
* - checker.png — Colorful checkerboard pattern (256x192)
|
|
7
|
-
* - circles.png — Overlapping colored circles on dark background (256x192)
|
|
8
|
-
*
|
|
9
|
-
* Uses raw PNG encoding with Node's zlib — no external dependencies.
|
|
10
|
-
*
|
|
11
|
-
* Run: bun vendor/silvery/examples/kitty/generate-samples.ts
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { deflateSync } from "node:zlib"
|
|
15
|
-
import { writeFileSync, mkdirSync } from "node:fs"
|
|
16
|
-
import { resolve, dirname } from "node:path"
|
|
17
|
-
import { fileURLToPath } from "node:url"
|
|
18
|
-
|
|
19
|
-
const WIDTH = 256
|
|
20
|
-
const HEIGHT = 192
|
|
21
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// HSV to RGB
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
function hsvToRgb(h: number, s: number, v: number): [number, number, number] {
|
|
27
|
-
const c = v * s
|
|
28
|
-
const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
|
|
29
|
-
const m = v - c
|
|
30
|
-
|
|
31
|
-
let r = 0,
|
|
32
|
-
g = 0,
|
|
33
|
-
b = 0
|
|
34
|
-
if (h < 60) {
|
|
35
|
-
r = c
|
|
36
|
-
g = x
|
|
37
|
-
} else if (h < 120) {
|
|
38
|
-
r = x
|
|
39
|
-
g = c
|
|
40
|
-
} else if (h < 180) {
|
|
41
|
-
g = c
|
|
42
|
-
b = x
|
|
43
|
-
} else if (h < 240) {
|
|
44
|
-
g = x
|
|
45
|
-
b = c
|
|
46
|
-
} else if (h < 300) {
|
|
47
|
-
r = x
|
|
48
|
-
b = c
|
|
49
|
-
} else {
|
|
50
|
-
r = c
|
|
51
|
-
b = x
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)]
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
// Raw PNG encoder (no dependencies)
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
|
|
61
|
-
function crc32(buf: Buffer): number {
|
|
62
|
-
// CRC-32 lookup table
|
|
63
|
-
const table: number[] = []
|
|
64
|
-
for (let n = 0; n < 256; n++) {
|
|
65
|
-
let c = n
|
|
66
|
-
for (let k = 0; k < 8; k++) {
|
|
67
|
-
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
|
|
68
|
-
}
|
|
69
|
-
table[n] = c
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
let crc = 0xffffffff
|
|
73
|
-
for (let i = 0; i < buf.length; i++) {
|
|
74
|
-
crc = table[(crc ^ buf[i]!) & 0xff]! ^ (crc >>> 8)
|
|
75
|
-
}
|
|
76
|
-
return (crc ^ 0xffffffff) >>> 0
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function makePngChunk(type: string, data: Buffer): Buffer {
|
|
80
|
-
const length = Buffer.alloc(4)
|
|
81
|
-
length.writeUInt32BE(data.length, 0)
|
|
82
|
-
|
|
83
|
-
const typeAndData = Buffer.concat([Buffer.from(type, "ascii"), data])
|
|
84
|
-
const crc = Buffer.alloc(4)
|
|
85
|
-
crc.writeUInt32BE(crc32(typeAndData), 0)
|
|
86
|
-
|
|
87
|
-
return Buffer.concat([length, typeAndData, crc])
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function rgbaToPng(rgba: Buffer, width: number, height: number): Buffer {
|
|
91
|
-
// PNG signature
|
|
92
|
-
const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
|
|
93
|
-
|
|
94
|
-
// IHDR chunk
|
|
95
|
-
const ihdr = Buffer.alloc(13)
|
|
96
|
-
ihdr.writeUInt32BE(width, 0)
|
|
97
|
-
ihdr.writeUInt32BE(height, 4)
|
|
98
|
-
ihdr[8] = 8 // bit depth
|
|
99
|
-
ihdr[9] = 6 // color type: RGBA
|
|
100
|
-
ihdr[10] = 0 // compression
|
|
101
|
-
ihdr[11] = 0 // filter
|
|
102
|
-
ihdr[12] = 0 // interlace
|
|
103
|
-
const ihdrChunk = makePngChunk("IHDR", ihdr)
|
|
104
|
-
|
|
105
|
-
// IDAT chunk: filter each row with filter type 0 (None), then deflate
|
|
106
|
-
const rawRows = Buffer.alloc(height * (1 + width * 4))
|
|
107
|
-
for (let y = 0; y < height; y++) {
|
|
108
|
-
const rowOffset = y * (1 + width * 4)
|
|
109
|
-
rawRows[rowOffset] = 0 // filter type: None
|
|
110
|
-
rgba.copy(rawRows, rowOffset + 1, y * width * 4, (y + 1) * width * 4)
|
|
111
|
-
}
|
|
112
|
-
const compressed = deflateSync(rawRows)
|
|
113
|
-
const idatChunk = makePngChunk("IDAT", compressed)
|
|
114
|
-
|
|
115
|
-
// IEND chunk
|
|
116
|
-
const iendChunk = makePngChunk("IEND", Buffer.alloc(0))
|
|
117
|
-
|
|
118
|
-
return Buffer.concat([signature, ihdrChunk, idatChunk, iendChunk])
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ---------------------------------------------------------------------------
|
|
122
|
-
// Image generators
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
|
|
125
|
-
function generateGradient(): Buffer {
|
|
126
|
-
const rgba = Buffer.alloc(WIDTH * HEIGHT * 4)
|
|
127
|
-
|
|
128
|
-
for (let y = 0; y < HEIGHT; y++) {
|
|
129
|
-
for (let x = 0; x < WIDTH; x++) {
|
|
130
|
-
const offset = (y * WIDTH + x) * 4
|
|
131
|
-
const hue = (x / WIDTH) * 360
|
|
132
|
-
const brightness = 0.3 + 0.7 * (1 - y / HEIGHT)
|
|
133
|
-
const [r, g, b] = hsvToRgb(hue, 1.0, brightness)
|
|
134
|
-
rgba[offset] = r
|
|
135
|
-
rgba[offset + 1] = g
|
|
136
|
-
rgba[offset + 2] = b
|
|
137
|
-
rgba[offset + 3] = 255
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return rgbaToPng(rgba, WIDTH, HEIGHT)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function generateChecker(): Buffer {
|
|
145
|
-
const rgba = Buffer.alloc(WIDTH * HEIGHT * 4)
|
|
146
|
-
const checkerSize = 24
|
|
147
|
-
|
|
148
|
-
for (let y = 0; y < HEIGHT; y++) {
|
|
149
|
-
for (let x = 0; x < WIDTH; x++) {
|
|
150
|
-
const offset = (y * WIDTH + x) * 4
|
|
151
|
-
const isLight = (Math.floor(x / checkerSize) + Math.floor(y / checkerSize)) % 2 === 0
|
|
152
|
-
const hue = (x / WIDTH) * 360
|
|
153
|
-
const [hr, hg, hb] = hsvToRgb(hue, 0.6, 1.0)
|
|
154
|
-
|
|
155
|
-
if (isLight) {
|
|
156
|
-
rgba[offset] = Math.min(255, hr + 60)
|
|
157
|
-
rgba[offset + 1] = Math.min(255, hg + 60)
|
|
158
|
-
rgba[offset + 2] = Math.min(255, hb + 60)
|
|
159
|
-
} else {
|
|
160
|
-
rgba[offset] = Math.max(0, hr - 100)
|
|
161
|
-
rgba[offset + 1] = Math.max(0, hg - 100)
|
|
162
|
-
rgba[offset + 2] = Math.max(0, hb - 100)
|
|
163
|
-
}
|
|
164
|
-
rgba[offset + 3] = 255
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return rgbaToPng(rgba, WIDTH, HEIGHT)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function generateCircles(): Buffer {
|
|
172
|
-
const rgba = Buffer.alloc(WIDTH * HEIGHT * 4)
|
|
173
|
-
|
|
174
|
-
// Dark background
|
|
175
|
-
for (let i = 0; i < WIDTH * HEIGHT * 4; i += 4) {
|
|
176
|
-
rgba[i] = 20
|
|
177
|
-
rgba[i + 1] = 20
|
|
178
|
-
rgba[i + 2] = 30
|
|
179
|
-
rgba[i + 3] = 255
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Draw overlapping circles with additive blending
|
|
183
|
-
const circles: { cx: number; cy: number; r: number; color: [number, number, number] }[] = [
|
|
184
|
-
{ cx: 90, cy: 80, r: 70, color: [200, 40, 40] },
|
|
185
|
-
{ cx: 166, cy: 80, r: 70, color: [40, 180, 40] },
|
|
186
|
-
{ cx: 128, cy: 140, r: 70, color: [40, 80, 220] },
|
|
187
|
-
{ cx: 50, cy: 140, r: 50, color: [220, 180, 30] },
|
|
188
|
-
{ cx: 206, cy: 140, r: 50, color: [180, 40, 220] },
|
|
189
|
-
{ cx: 128, cy: 60, r: 45, color: [40, 200, 200] },
|
|
190
|
-
]
|
|
191
|
-
|
|
192
|
-
for (const circle of circles) {
|
|
193
|
-
for (let y = 0; y < HEIGHT; y++) {
|
|
194
|
-
for (let x = 0; x < WIDTH; x++) {
|
|
195
|
-
const dx = x - circle.cx
|
|
196
|
-
const dy = y - circle.cy
|
|
197
|
-
const dist = Math.sqrt(dx * dx + dy * dy)
|
|
198
|
-
|
|
199
|
-
if (dist < circle.r) {
|
|
200
|
-
const offset = (y * WIDTH + x) * 4
|
|
201
|
-
// Soft edge with smooth falloff
|
|
202
|
-
const alpha = dist > circle.r - 8 ? (circle.r - dist) / 8 : 1.0
|
|
203
|
-
|
|
204
|
-
// Additive blending
|
|
205
|
-
rgba[offset] = Math.min(255, rgba[offset]! + Math.round(circle.color[0] * alpha))
|
|
206
|
-
rgba[offset + 1] = Math.min(255, rgba[offset + 1]! + Math.round(circle.color[1] * alpha))
|
|
207
|
-
rgba[offset + 2] = Math.min(255, rgba[offset + 2]! + Math.round(circle.color[2] * alpha))
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return rgbaToPng(rgba, WIDTH, HEIGHT)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ---------------------------------------------------------------------------
|
|
217
|
-
// Main
|
|
218
|
-
// ---------------------------------------------------------------------------
|
|
219
|
-
|
|
220
|
-
const samplesDir = resolve(dirname(fileURLToPath(import.meta.url)), "samples")
|
|
221
|
-
mkdirSync(samplesDir, { recursive: true })
|
|
222
|
-
|
|
223
|
-
const samples = [
|
|
224
|
-
{ name: "gradient.png", generate: generateGradient },
|
|
225
|
-
{ name: "checker.png", generate: generateChecker },
|
|
226
|
-
{ name: "circles.png", generate: generateCircles },
|
|
227
|
-
]
|
|
228
|
-
|
|
229
|
-
for (const sample of samples) {
|
|
230
|
-
const path = resolve(samplesDir, sample.name)
|
|
231
|
-
const png = sample.generate()
|
|
232
|
-
writeFileSync(path, png)
|
|
233
|
-
console.log(` ${sample.name} (${png.length} bytes)`)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
console.log(`\nGenerated ${samples.length} samples in ${samplesDir}`)
|
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Image Component Demo
|
|
3
|
-
*
|
|
4
|
-
* Demonstrates the `<Image>` component — a high-level React component for
|
|
5
|
-
* displaying images in the terminal. Unlike the raw Kitty graphics protocol
|
|
6
|
-
* example (images.tsx), this uses the declarative component API with automatic
|
|
7
|
-
* protocol detection and fallback support.
|
|
8
|
-
*
|
|
9
|
-
* Features:
|
|
10
|
-
* - Uses the <Image> component from silvery
|
|
11
|
-
* - Auto-generated rainbow test pattern (no external files needed)
|
|
12
|
-
* - Fallback text when graphics protocol is not supported
|
|
13
|
-
* - Protocol auto-detection status display
|
|
14
|
-
* - Adjustable image dimensions with +/- keys
|
|
15
|
-
*
|
|
16
|
-
* Run: bun vendor/silvery/examples/kitty/image-component.tsx
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { deflateSync } from "node:zlib"
|
|
20
|
-
import React, { useState, useMemo } from "react"
|
|
21
|
-
import { render, Box, Text, Image, useInput, useApp, createTerm, type Key } from "../../src/index.js"
|
|
22
|
-
import { isKittyGraphicsSupported } from "../../src/image/kitty-graphics.js"
|
|
23
|
-
import { isSixelSupported } from "../../src/image/sixel-encoder.js"
|
|
24
|
-
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
25
|
-
|
|
26
|
-
export const meta: ExampleMeta = {
|
|
27
|
-
name: "Image Component",
|
|
28
|
-
description: "Declarative <Image> component with protocol auto-detection",
|
|
29
|
-
features: ["Image", "Kitty graphics", "Sixel", "fallback text", "protocol detection"],
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// ============================================================================
|
|
33
|
-
// PNG Generation
|
|
34
|
-
// ============================================================================
|
|
35
|
-
|
|
36
|
-
/** Convert HSV (h: 0-360, s/v: 0-1) to RGB bytes */
|
|
37
|
-
function hsvToRgb(h: number, s: number, v: number): [number, number, number] {
|
|
38
|
-
const c = v * s
|
|
39
|
-
const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
|
|
40
|
-
const m = v - c
|
|
41
|
-
|
|
42
|
-
let r = 0,
|
|
43
|
-
g = 0,
|
|
44
|
-
b = 0
|
|
45
|
-
if (h < 60) {
|
|
46
|
-
r = c
|
|
47
|
-
g = x
|
|
48
|
-
} else if (h < 120) {
|
|
49
|
-
r = x
|
|
50
|
-
g = c
|
|
51
|
-
} else if (h < 180) {
|
|
52
|
-
g = c
|
|
53
|
-
b = x
|
|
54
|
-
} else if (h < 240) {
|
|
55
|
-
g = x
|
|
56
|
-
b = c
|
|
57
|
-
} else if (h < 300) {
|
|
58
|
-
r = x
|
|
59
|
-
b = c
|
|
60
|
-
} else {
|
|
61
|
-
r = c
|
|
62
|
-
b = x
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)]
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Generate a rainbow gradient PNG buffer (no external dependencies). */
|
|
69
|
-
function generateTestPatternPng(width: number, height: number): Buffer {
|
|
70
|
-
// Build raw RGBA pixel data with filter bytes
|
|
71
|
-
const rawData = Buffer.alloc(height * (1 + width * 4))
|
|
72
|
-
|
|
73
|
-
for (let y = 0; y < height; y++) {
|
|
74
|
-
const rowOffset = y * (1 + width * 4)
|
|
75
|
-
rawData[rowOffset] = 0 // PNG filter: None
|
|
76
|
-
|
|
77
|
-
for (let x = 0; x < width; x++) {
|
|
78
|
-
const hue = (x / width) * 360
|
|
79
|
-
const saturation = 0.7 + 0.3 * Math.sin((y / height) * Math.PI)
|
|
80
|
-
const value = 0.5 + 0.5 * Math.cos((y / height) * Math.PI * 2)
|
|
81
|
-
const [r, g, b] = hsvToRgb(hue, saturation, value)
|
|
82
|
-
|
|
83
|
-
const pixelOffset = rowOffset + 1 + x * 4
|
|
84
|
-
rawData[pixelOffset] = r!
|
|
85
|
-
rawData[pixelOffset + 1] = g!
|
|
86
|
-
rawData[pixelOffset + 2] = b!
|
|
87
|
-
rawData[pixelOffset + 3] = 255
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Encode as minimal PNG
|
|
92
|
-
const compressed = deflateSync(rawData)
|
|
93
|
-
|
|
94
|
-
const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
|
|
95
|
-
|
|
96
|
-
function makeChunk(type: string, data: Buffer): Buffer {
|
|
97
|
-
const len = Buffer.alloc(4)
|
|
98
|
-
len.writeUInt32BE(data.length)
|
|
99
|
-
const typeBytes = Buffer.from(type, "ascii")
|
|
100
|
-
const payload = Buffer.concat([typeBytes, data])
|
|
101
|
-
const crc = crc32(payload)
|
|
102
|
-
const crcBuf = Buffer.alloc(4)
|
|
103
|
-
crcBuf.writeUInt32BE(crc >>> 0)
|
|
104
|
-
return Buffer.concat([len, payload, crcBuf])
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// IHDR
|
|
108
|
-
const ihdr = Buffer.alloc(13)
|
|
109
|
-
ihdr.writeUInt32BE(width, 0)
|
|
110
|
-
ihdr.writeUInt32BE(height, 4)
|
|
111
|
-
ihdr[8] = 8 // bit depth
|
|
112
|
-
ihdr[9] = 6 // color type: RGBA
|
|
113
|
-
ihdr[10] = 0 // compression
|
|
114
|
-
ihdr[11] = 0 // filter
|
|
115
|
-
ihdr[12] = 0 // interlace
|
|
116
|
-
|
|
117
|
-
return Buffer.concat([
|
|
118
|
-
signature,
|
|
119
|
-
makeChunk("IHDR", ihdr),
|
|
120
|
-
makeChunk("IDAT", compressed),
|
|
121
|
-
makeChunk("IEND", Buffer.alloc(0)),
|
|
122
|
-
])
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** CRC-32 for PNG chunks */
|
|
126
|
-
function crc32(data: Buffer): number {
|
|
127
|
-
let crc = 0xffffffff
|
|
128
|
-
for (let i = 0; i < data.length; i++) {
|
|
129
|
-
crc ^= data[i]!
|
|
130
|
-
for (let j = 0; j < 8; j++) {
|
|
131
|
-
crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return (crc ^ 0xffffffff) >>> 0
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ============================================================================
|
|
138
|
-
// Components
|
|
139
|
-
// ============================================================================
|
|
140
|
-
|
|
141
|
-
function ProtocolStatus(): JSX.Element {
|
|
142
|
-
const kitty = isKittyGraphicsSupported()
|
|
143
|
-
const sixel = isSixelSupported()
|
|
144
|
-
|
|
145
|
-
const detected = kitty ? "Kitty" : sixel ? "Sixel" : "None"
|
|
146
|
-
const color = kitty || sixel ? "green" : "yellow"
|
|
147
|
-
|
|
148
|
-
return (
|
|
149
|
-
<Box gap={1} paddingX={1}>
|
|
150
|
-
<Text dim>Protocol:</Text>
|
|
151
|
-
<Text color={color} bold>
|
|
152
|
-
{detected}
|
|
153
|
-
</Text>
|
|
154
|
-
<Text dim>
|
|
155
|
-
(Kitty: {kitty ? "yes" : "no"}, Sixel: {sixel ? "yes" : "no"})
|
|
156
|
-
</Text>
|
|
157
|
-
</Box>
|
|
158
|
-
)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function ImageComponentDemo(): JSX.Element {
|
|
162
|
-
const { exit } = useApp()
|
|
163
|
-
const [imageWidth, setImageWidth] = useState(40)
|
|
164
|
-
const [imageHeight, setImageHeight] = useState(15)
|
|
165
|
-
|
|
166
|
-
// Generate a test pattern PNG
|
|
167
|
-
const pngBuffer = useMemo(() => generateTestPatternPng(256, 192), [])
|
|
168
|
-
|
|
169
|
-
useInput((input: string, key: Key) => {
|
|
170
|
-
if (input === "q" || key.escape) {
|
|
171
|
-
exit()
|
|
172
|
-
return
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Adjust width
|
|
176
|
-
if (key.rightArrow || input === "l") {
|
|
177
|
-
setImageWidth((prev) => Math.min(80, prev + 5))
|
|
178
|
-
}
|
|
179
|
-
if (key.leftArrow || input === "h") {
|
|
180
|
-
setImageWidth((prev) => Math.max(10, prev - 5))
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Adjust height
|
|
184
|
-
if (input === "+" || input === "=") {
|
|
185
|
-
setImageHeight((prev) => Math.min(30, prev + 2))
|
|
186
|
-
}
|
|
187
|
-
if (input === "-") {
|
|
188
|
-
setImageHeight((prev) => Math.max(5, prev - 2))
|
|
189
|
-
}
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
return (
|
|
193
|
-
<Box flexDirection="column" padding={1} gap={1}>
|
|
194
|
-
<ProtocolStatus />
|
|
195
|
-
|
|
196
|
-
<Box flexDirection="column" borderStyle="round" borderColor="cyan" paddingX={1}>
|
|
197
|
-
<Box marginBottom={1} gap={1}>
|
|
198
|
-
<Text bold color="cyan">
|
|
199
|
-
{"<Image>"}
|
|
200
|
-
</Text>
|
|
201
|
-
<Text dim>
|
|
202
|
-
{imageWidth}x{imageHeight} cols/rows
|
|
203
|
-
</Text>
|
|
204
|
-
</Box>
|
|
205
|
-
|
|
206
|
-
<Image
|
|
207
|
-
src={pngBuffer}
|
|
208
|
-
width={imageWidth}
|
|
209
|
-
height={imageHeight}
|
|
210
|
-
fallback="[Rainbow gradient — graphics protocol not available]"
|
|
211
|
-
/>
|
|
212
|
-
</Box>
|
|
213
|
-
|
|
214
|
-
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={1}>
|
|
215
|
-
<Box marginBottom={1}>
|
|
216
|
-
<Text bold>Component Props</Text>
|
|
217
|
-
</Box>
|
|
218
|
-
<Box gap={2}>
|
|
219
|
-
<Text>
|
|
220
|
-
width={"{"}
|
|
221
|
-
<Text color="green">{imageWidth}</Text>
|
|
222
|
-
{"}"}
|
|
223
|
-
</Text>
|
|
224
|
-
<Text>
|
|
225
|
-
height={"{"}
|
|
226
|
-
<Text color="green">{imageHeight}</Text>
|
|
227
|
-
{"}"}
|
|
228
|
-
</Text>
|
|
229
|
-
<Text>
|
|
230
|
-
protocol=
|
|
231
|
-
<Text color="yellow">"auto"</Text>
|
|
232
|
-
</Text>
|
|
233
|
-
</Box>
|
|
234
|
-
<Text dim>fallback="[Rainbow gradient — graphics protocol not available]"</Text>
|
|
235
|
-
</Box>
|
|
236
|
-
|
|
237
|
-
<Text dim>
|
|
238
|
-
{" "}
|
|
239
|
-
<Text bold dim>
|
|
240
|
-
h/l
|
|
241
|
-
</Text>{" "}
|
|
242
|
-
width{" "}
|
|
243
|
-
<Text bold dim>
|
|
244
|
-
+/-
|
|
245
|
-
</Text>{" "}
|
|
246
|
-
height{" "}
|
|
247
|
-
<Text bold dim>
|
|
248
|
-
Esc/q
|
|
249
|
-
</Text>{" "}
|
|
250
|
-
quit
|
|
251
|
-
</Text>
|
|
252
|
-
</Box>
|
|
253
|
-
)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// ============================================================================
|
|
257
|
-
// Main
|
|
258
|
-
// ============================================================================
|
|
259
|
-
|
|
260
|
-
async function main() {
|
|
261
|
-
using term = createTerm()
|
|
262
|
-
const { waitUntilExit } = await render(
|
|
263
|
-
<ExampleBanner meta={meta} controls="h/l width +/- height Esc/q quit">
|
|
264
|
-
<ImageComponentDemo />
|
|
265
|
-
</ExampleBanner>,
|
|
266
|
-
term,
|
|
267
|
-
)
|
|
268
|
-
await waitUntilExit()
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (import.meta.main) {
|
|
272
|
-
main().catch(console.error)
|
|
273
|
-
}
|