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.
Files changed (120) hide show
  1. package/README.md +41 -145
  2. package/dist/chalk.js +3 -0
  3. package/dist/chalk.js.map +11 -0
  4. package/dist/index.js +340 -0
  5. package/dist/index.js.map +282 -0
  6. package/dist/ink.js +129 -0
  7. package/dist/ink.js.map +140 -0
  8. package/dist/runtime.js +394 -0
  9. package/dist/runtime.js.map +286 -0
  10. package/dist/theme.js +343 -0
  11. package/dist/theme.js.map +286 -0
  12. package/dist/ui/animation.js +3 -0
  13. package/dist/ui/animation.js.map +15 -0
  14. package/dist/ui/ansi.js +3 -0
  15. package/dist/ui/ansi.js.map +10 -0
  16. package/dist/ui/cli.js +8 -0
  17. package/dist/ui/cli.js.map +14 -0
  18. package/dist/ui/display.js +4 -0
  19. package/dist/ui/display.js.map +10 -0
  20. package/dist/ui/image.js +4 -0
  21. package/dist/ui/image.js.map +15 -0
  22. package/dist/ui/input.js +3 -0
  23. package/dist/ui/input.js.map +11 -0
  24. package/dist/ui/progress.js +8 -0
  25. package/dist/ui/progress.js.map +20 -0
  26. package/dist/ui/react.js +3 -0
  27. package/dist/ui/react.js.map +15 -0
  28. package/dist/ui/utils.js +3 -0
  29. package/dist/ui/utils.js.map +10 -0
  30. package/dist/ui/wrappers.js +14 -0
  31. package/dist/ui/wrappers.js.map +19 -0
  32. package/dist/ui.js +17 -0
  33. package/dist/ui.js.map +20 -0
  34. package/package.json +67 -15
  35. package/src/index.ts +67 -1
  36. package/src/runtime.ts +4 -0
  37. package/src/theme.ts +4 -0
  38. package/src/ui/animation.ts +2 -0
  39. package/src/ui/ansi.ts +2 -0
  40. package/src/ui/cli.ts +2 -0
  41. package/src/ui/display.ts +2 -0
  42. package/src/ui/image.ts +2 -0
  43. package/src/ui/input.ts +2 -0
  44. package/src/ui/progress.ts +2 -0
  45. package/src/ui/react.ts +2 -0
  46. package/src/ui/utils.ts +2 -0
  47. package/src/ui/wrappers.ts +2 -0
  48. package/src/ui.ts +4 -0
  49. package/examples/CLAUDE.md +0 -75
  50. package/examples/_banner.tsx +0 -60
  51. package/examples/cli.ts +0 -228
  52. package/examples/index.md +0 -101
  53. package/examples/inline/inline-nontty.tsx +0 -98
  54. package/examples/inline/inline-progress.tsx +0 -79
  55. package/examples/inline/inline-simple.tsx +0 -63
  56. package/examples/inline/scrollback.tsx +0 -185
  57. package/examples/interactive/_input-debug.tsx +0 -110
  58. package/examples/interactive/_stdin-test.ts +0 -71
  59. package/examples/interactive/_textarea-bare.tsx +0 -45
  60. package/examples/interactive/aichat/components.tsx +0 -468
  61. package/examples/interactive/aichat/index.tsx +0 -207
  62. package/examples/interactive/aichat/script.ts +0 -460
  63. package/examples/interactive/aichat/state.ts +0 -326
  64. package/examples/interactive/aichat/types.ts +0 -19
  65. package/examples/interactive/app-todo.tsx +0 -198
  66. package/examples/interactive/async-data.tsx +0 -208
  67. package/examples/interactive/cli-wizard.tsx +0 -332
  68. package/examples/interactive/clipboard.tsx +0 -183
  69. package/examples/interactive/components.tsx +0 -463
  70. package/examples/interactive/data-explorer.tsx +0 -506
  71. package/examples/interactive/dev-tools.tsx +0 -379
  72. package/examples/interactive/explorer.tsx +0 -747
  73. package/examples/interactive/gallery.tsx +0 -652
  74. package/examples/interactive/inline-bench.tsx +0 -136
  75. package/examples/interactive/kanban.tsx +0 -267
  76. package/examples/interactive/layout-ref.tsx +0 -185
  77. package/examples/interactive/outline.tsx +0 -171
  78. package/examples/interactive/paste-demo.tsx +0 -198
  79. package/examples/interactive/scroll.tsx +0 -77
  80. package/examples/interactive/search-filter.tsx +0 -240
  81. package/examples/interactive/task-list.tsx +0 -279
  82. package/examples/interactive/terminal.tsx +0 -798
  83. package/examples/interactive/textarea.tsx +0 -103
  84. package/examples/interactive/theme.tsx +0 -336
  85. package/examples/interactive/transform.tsx +0 -256
  86. package/examples/interactive/virtual-10k.tsx +0 -413
  87. package/examples/kitty/canvas.tsx +0 -519
  88. package/examples/kitty/generate-samples.ts +0 -236
  89. package/examples/kitty/image-component.tsx +0 -273
  90. package/examples/kitty/images.tsx +0 -604
  91. package/examples/kitty/input.tsx +0 -371
  92. package/examples/kitty/keys.tsx +0 -378
  93. package/examples/kitty/paint.tsx +0 -1017
  94. package/examples/layout/dashboard.tsx +0 -551
  95. package/examples/layout/live-resize.tsx +0 -290
  96. package/examples/layout/overflow.tsx +0 -51
  97. package/examples/playground/README.md +0 -69
  98. package/examples/playground/build.ts +0 -61
  99. package/examples/playground/index.html +0 -420
  100. package/examples/playground/playground-app.tsx +0 -416
  101. package/examples/runtime/elm-counter.tsx +0 -206
  102. package/examples/runtime/hello-runtime.tsx +0 -73
  103. package/examples/runtime/pipe-composition.tsx +0 -184
  104. package/examples/runtime/run-counter.tsx +0 -78
  105. package/examples/runtime/runtime-counter.tsx +0 -197
  106. package/examples/screenshots/generate.tsx +0 -563
  107. package/examples/scrollback-perf.tsx +0 -230
  108. package/examples/viewer.tsx +0 -654
  109. package/examples/web/build.ts +0 -365
  110. package/examples/web/canvas-app.tsx +0 -80
  111. package/examples/web/canvas.html +0 -89
  112. package/examples/web/dom-app.tsx +0 -81
  113. package/examples/web/dom.html +0 -113
  114. package/examples/web/showcase-app.tsx +0 -107
  115. package/examples/web/showcase.html +0 -34
  116. package/examples/web/showcases/index.tsx +0 -56
  117. package/examples/web/viewer-app.tsx +0 -555
  118. package/examples/web/viewer.html +0 -30
  119. package/examples/web/xterm-app.tsx +0 -105
  120. package/examples/web/xterm.html +0 -118
@@ -1,519 +0,0 @@
1
- /**
2
- * Terminal Canvas
3
- *
4
- * Click-drag to draw pixel art on a terminal canvas using half-block characters.
5
- * Each terminal cell holds 2 vertical pixels using Unicode half-block technique.
6
- *
7
- * Features:
8
- * - Half-block pixel art (2x vertical resolution)
9
- * - Full RGB color picker with HSL gradient
10
- * - Hue bar + saturation bar for intuitive color selection
11
- * - Pen and eraser tools
12
- * - Keyboard shortcuts for color/tool selection
13
- *
14
- * Run: bun vendor/silvery/examples/kitty/terminal-canvas.tsx
15
- */
16
-
17
- import { createTerm, enableMouse, disableMouse, parseMouseSequence, isMouseSequence } from "../../src/index.js"
18
- import type { ExampleMeta } from "../_banner.js"
19
-
20
- export const meta: ExampleMeta = {
21
- name: "Char Draw",
22
- description: "Click-drag to draw with half-block pixel art, RGB color picker",
23
- features: ["parseMouseSequence()", "enableMouse()", "half-block rendering", "drag tracking", "HSL color picker"],
24
- }
25
-
26
- // Half-block characters for 2x vertical resolution
27
- const UPPER_HALF = "\u2580" // ▀ — top filled, bottom empty
28
- const LOWER_HALF = "\u2584" // ▄ — top empty, bottom filled
29
- const FULL_BLOCK = "\u2588" // █ — both filled
30
-
31
- type RGB = [number, number, number]
32
-
33
- // Preset colors accessible via 1-9/0
34
- const PRESETS: { name: string; color: RGB }[] = [
35
- { name: "white", color: [255, 255, 255] },
36
- { name: "red", color: [255, 0, 0] },
37
- { name: "orange", color: [255, 165, 0] },
38
- { name: "yellow", color: [255, 255, 0] },
39
- { name: "green", color: [0, 255, 0] },
40
- { name: "cyan", color: [0, 255, 255] },
41
- { name: "blue", color: [0, 100, 255] },
42
- { name: "magenta", color: [255, 0, 255] },
43
- { name: "pink", color: [255, 128, 200] },
44
- { name: "black", color: [0, 0, 0] },
45
- ]
46
-
47
- // --- HSL <-> RGB conversion ---
48
-
49
- function hslToRgb(h: number, s: number, l: number): RGB {
50
- // h: 0-360, s: 0-1, l: 0-1
51
- const c = (1 - Math.abs(2 * l - 1)) * s
52
- const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
53
- const m = l - c / 2
54
-
55
- let r1: number, g1: number, b1: number
56
- if (h < 60) {
57
- ;[r1, g1, b1] = [c, x, 0]
58
- } else if (h < 120) {
59
- ;[r1, g1, b1] = [x, c, 0]
60
- } else if (h < 180) {
61
- ;[r1, g1, b1] = [0, c, x]
62
- } else if (h < 240) {
63
- ;[r1, g1, b1] = [0, x, c]
64
- } else if (h < 300) {
65
- ;[r1, g1, b1] = [x, 0, c]
66
- } else {
67
- ;[r1, g1, b1] = [c, 0, x]
68
- }
69
-
70
- return [Math.round((r1 + m) * 255), Math.round((g1 + m) * 255), Math.round((b1 + m) * 255)]
71
- }
72
-
73
- function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
74
- r /= 255
75
- g /= 255
76
- b /= 255
77
- const max = Math.max(r, g, b)
78
- const min = Math.min(r, g, b)
79
- const l = (max + min) / 2
80
- if (max === min) return [0, 0, l]
81
- const d = max - min
82
- const s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
83
- let h: number
84
- if (max === r) {
85
- h = ((g - b) / d + (g < b ? 6 : 0)) * 60
86
- } else if (max === g) {
87
- h = ((b - r) / d + 2) * 60
88
- } else {
89
- h = ((r - g) / d + 4) * 60
90
- }
91
- return [h, s, l]
92
- }
93
-
94
- type Tool = "pen" | "eraser"
95
-
96
- interface CanvasState {
97
- /** 2D array of pixel colors as RGB tuples (null = empty). Width x Height where height = rows*2 */
98
- pixels: (RGB | null)[][]
99
- /** Canvas width in terminal columns */
100
- width: number
101
- /** Canvas height in pixel rows (2x terminal rows) */
102
- height: number
103
- /** Currently selected color as RGB */
104
- currentColor: RGB
105
- /** Current HSL values for picker state */
106
- hue: number
107
- saturation: number
108
- lightness: number
109
- /** Current tool */
110
- tool: Tool
111
- /** Mouse position for status bar */
112
- mouseX: number
113
- mouseY: number
114
- /** Whether mouse is currently pressed */
115
- isDrawing: boolean
116
- }
117
-
118
- // Reserve 4 rows: 1 header, 1 hue bar, 1 saturation bar, 1 status
119
- const RESERVED_ROWS = 4
120
-
121
- function createCanvas(cols: number, rows: number): CanvasState {
122
- const canvasRows = rows - RESERVED_ROWS
123
- const width = cols
124
- const height = canvasRows * 2
125
-
126
- const pixels: (RGB | null)[][] = []
127
- for (let y = 0; y < height; y++) {
128
- pixels.push(new Array(width).fill(null))
129
- }
130
-
131
- return {
132
- pixels,
133
- width,
134
- height,
135
- currentColor: [255, 255, 255],
136
- hue: 0,
137
- saturation: 1.0,
138
- lightness: 0.5,
139
- tool: "pen",
140
- mouseX: 0,
141
- mouseY: 0,
142
- isDrawing: false,
143
- }
144
- }
145
-
146
- function setPixel(state: CanvasState, termX: number, termY: number): void {
147
- // termY is relative to terminal row; row 0 is the header
148
- // Canvas starts at row 1
149
- const canvasTermRow = termY - 1
150
- if (canvasTermRow < 0) return
151
-
152
- const pixelY0 = canvasTermRow * 2
153
- const pixelY1 = pixelY0 + 1
154
-
155
- const x = termX
156
- if (x < 0 || x >= state.width) return
157
-
158
- const value: RGB | null = state.tool === "pen" ? [...state.currentColor] : null
159
-
160
- if (pixelY0 >= 0 && pixelY0 < state.height) {
161
- state.pixels[pixelY0]![x] = value
162
- }
163
- if (pixelY1 >= 0 && pixelY1 < state.height) {
164
- state.pixels[pixelY1]![x] = value
165
- }
166
- }
167
-
168
- function renderFrame(state: CanvasState, term: ReturnType<typeof createTerm>): string {
169
- const lines: string[] = []
170
-
171
- // Header
172
- lines.push(
173
- term.dim.yellow("▸ silvery") +
174
- " " +
175
- term.bold("Terminal Canvas") +
176
- " " +
177
- term.dim("— click-drag to draw") +
178
- " " +
179
- term.dim("1-9/0 preset [/] hue -/= sat b bright e eraser c clear q quit"),
180
- )
181
-
182
- // Canvas: convert pixel pairs to half-block characters
183
- const canvasRows = Math.floor(state.height / 2)
184
- for (let row = 0; row < canvasRows; row++) {
185
- let line = ""
186
- for (let col = 0; col < state.width; col++) {
187
- const topPixel = state.pixels[row * 2]![col]
188
- const bottomPixel = state.pixels[row * 2 + 1]![col]
189
-
190
- if (topPixel === null && bottomPixel === null) {
191
- line += " "
192
- } else if (topPixel !== null && bottomPixel === null) {
193
- const [r, g, b] = topPixel
194
- line += term.rgb(r, g, b)(UPPER_HALF)
195
- } else if (topPixel === null && bottomPixel !== null) {
196
- const [r, g, b] = bottomPixel
197
- line += term.rgb(r, g, b)(LOWER_HALF)
198
- } else if (
199
- topPixel !== null &&
200
- topPixel[0] === bottomPixel?.[0] &&
201
- topPixel[1] === bottomPixel[1] &&
202
- topPixel[2] === bottomPixel[2]
203
- ) {
204
- const [r, g, b] = topPixel
205
- line += term.rgb(r, g, b)(FULL_BLOCK)
206
- } else {
207
- const [tr, tg, tb] = topPixel!
208
- const [br, bg, bb] = bottomPixel!
209
- line += term.rgb(tr, tg, tb).bgRgb(br, bg, bb)(UPPER_HALF)
210
- }
211
- }
212
- lines.push(line)
213
- }
214
-
215
- // --- Hue gradient bar ---
216
- // Each column maps to a hue value; full saturation, 0.5 lightness
217
- let hueLine = ""
218
- for (let col = 0; col < state.width; col++) {
219
- const hue = (col / state.width) * 360
220
- const [r, g, b] = hslToRgb(hue, 1.0, 0.5)
221
- // Mark the selected hue column
222
- const isSelected = Math.abs(hue - state.hue) < 360 / state.width / 2 + 0.5
223
- if (isSelected) {
224
- hueLine += term.bgRgb(r, g, b).black("▼")
225
- } else {
226
- hueLine += term.bgRgb(r, g, b)(" ")
227
- }
228
- }
229
- lines.push(hueLine)
230
-
231
- // --- Saturation/brightness bar ---
232
- // Left half: saturation gradient at current hue. Right half: lightness gradient.
233
- const halfWidth = Math.floor(state.width / 2)
234
- let satLine = ""
235
- for (let col = 0; col < halfWidth; col++) {
236
- const sat = col / (halfWidth - 1)
237
- const [r, g, b] = hslToRgb(state.hue, sat, state.lightness)
238
- const isSelected = Math.abs(sat - state.saturation) < 1 / (halfWidth - 1) / 2 + 0.01
239
- if (isSelected) {
240
- satLine += term.bgRgb(r, g, b)(r + g + b > 384 ? term.black("◆") : term.white("◆"))
241
- } else {
242
- satLine += term.bgRgb(r, g, b)(" ")
243
- }
244
- }
245
- // Separator
246
- satLine += term.dim("│")
247
- // Lightness gradient
248
- for (let col = 0; col < state.width - halfWidth - 1; col++) {
249
- const lit = col / (state.width - halfWidth - 2)
250
- const [r, g, b] = hslToRgb(state.hue, state.saturation, lit)
251
- const isSelected = Math.abs(lit - state.lightness) < 1 / (state.width - halfWidth - 2) / 2 + 0.01
252
- if (isSelected) {
253
- satLine += term.bgRgb(r, g, b)(r + g + b > 384 ? term.black("◆") : term.white("◆"))
254
- } else {
255
- satLine += term.bgRgb(r, g, b)(" ")
256
- }
257
- }
258
- lines.push(satLine)
259
-
260
- // --- Status bar with color preview ---
261
- const [cr, cg, cb] = state.currentColor
262
- const colorSwatch = term.bgRgb(cr, cg, cb)(" ") // 4-char swatch
263
- const toolLabel = state.tool === "pen" ? "Pen" : "Eraser"
264
- const pos = `(${state.mouseX}, ${state.mouseY})`
265
- const rgbLabel = `rgb(${cr}, ${cg}, ${cb})`
266
- const hexLabel = `#${cr.toString(16).padStart(2, "0")}${cg.toString(16).padStart(2, "0")}${cb.toString(16).padStart(2, "0")}`
267
-
268
- lines.push(
269
- ` ${colorSwatch} ${term.bold(rgbLabel)} ${term.dim(hexLabel)}` +
270
- ` ${term.dim("Tool:")} ${term.bold(toolLabel)}` +
271
- ` ${term.dim("HSL:")} ${Math.round(state.hue)}° ${Math.round(state.saturation * 100)}% ${Math.round(state.lightness * 100)}%` +
272
- ` ${term.dim("Pos:")} ${pos}`,
273
- )
274
-
275
- return lines.join("\n")
276
- }
277
-
278
- async function main() {
279
- const crashCleanup = () => {
280
- const stdout = process.stdout
281
- stdout.write("\x1b[?1003l\x1b[?1006l") // Disable mouse
282
- stdout.write("\x1b[?25h") // Show cursor
283
- stdout.write("\x1b[?1049l") // Exit alternate screen
284
- stdout.write("\x1b[0m") // Reset colors
285
- if (process.stdin.isTTY && process.stdin.isRaw) {
286
- try {
287
- process.stdin.setRawMode(false)
288
- } catch {}
289
- }
290
- }
291
- process.on("uncaughtException", (err) => {
292
- crashCleanup()
293
- throw err
294
- })
295
-
296
- using term = createTerm()
297
- const cols = term.cols ?? 80
298
- const rows = term.rows ?? 24
299
-
300
- const state = createCanvas(cols, rows)
301
-
302
- const { stdin, stdout } = process
303
-
304
- // Enable raw mode and mouse tracking
305
- if (stdin.isTTY) {
306
- stdin.setRawMode(true)
307
- }
308
- stdin.resume()
309
- stdout.write(enableMouse())
310
-
311
- // Enter alternate screen, hide cursor
312
- stdout.write("\x1b[?1049h")
313
- stdout.write("\x1b[?25l")
314
-
315
- // Clear screen and render initial frame
316
- stdout.write("\x1b[2J\x1b[H")
317
- stdout.write(renderFrame(state, term))
318
-
319
- const redraw = () => {
320
- stdout.write("\x1b[H")
321
- stdout.write(renderFrame(state, term))
322
- }
323
-
324
- /** Update currentColor from current HSL state */
325
- const syncColor = () => {
326
- state.currentColor = hslToRgb(state.hue, state.saturation, state.lightness)
327
- }
328
-
329
- /** Compute terminal row indices for the UI bars */
330
- const canvasTermRows = () => Math.floor(state.height / 2)
331
-
332
- const onData = (data: Buffer) => {
333
- const raw = data.toString()
334
-
335
- // Check for mouse events
336
- if (isMouseSequence(raw)) {
337
- const parsed = parseMouseSequence(raw)
338
- if (!parsed) return
339
-
340
- state.mouseX = parsed.x
341
- state.mouseY = parsed.y
342
-
343
- const hueBarRow = 1 + canvasTermRows() // row after header + canvas
344
- const satBarRow = hueBarRow + 1
345
- const halfWidth = Math.floor(state.width / 2)
346
-
347
- if (parsed.action === "down" && parsed.button === 0) {
348
- if (parsed.y === hueBarRow) {
349
- // Click on hue bar
350
- state.hue = (parsed.x / state.width) * 360
351
- syncColor()
352
- state.tool = "pen"
353
- redraw()
354
- } else if (parsed.y === satBarRow) {
355
- if (parsed.x < halfWidth) {
356
- // Click on saturation section
357
- state.saturation = Math.max(0, Math.min(1, parsed.x / (halfWidth - 1)))
358
- syncColor()
359
- state.tool = "pen"
360
- } else if (parsed.x > halfWidth) {
361
- // Click on lightness section
362
- const litCol = parsed.x - halfWidth - 1
363
- const litWidth = state.width - halfWidth - 2
364
- state.lightness = Math.max(0, Math.min(1, litCol / litWidth))
365
- syncColor()
366
- state.tool = "pen"
367
- }
368
- redraw()
369
- } else if (parsed.y > 0 && parsed.y < hueBarRow) {
370
- // Click on canvas
371
- state.isDrawing = true
372
- setPixel(state, parsed.x, parsed.y)
373
- redraw()
374
- } else {
375
- redraw()
376
- }
377
- } else if (parsed.action === "move" && state.isDrawing) {
378
- if (parsed.y > 0 && parsed.y < hueBarRow) {
379
- setPixel(state, parsed.x, parsed.y)
380
- }
381
- redraw()
382
- } else if (parsed.action === "up") {
383
- state.isDrawing = false
384
- redraw()
385
- } else {
386
- redraw()
387
- }
388
- return
389
- }
390
-
391
- // Keyboard input
392
- for (const ch of raw) {
393
- if (ch === "q" || ch === "\x1b") {
394
- cleanup()
395
- return
396
- }
397
-
398
- if (ch === "e") {
399
- state.tool = state.tool === "eraser" ? "pen" : "eraser"
400
- redraw()
401
- } else if (ch === "c") {
402
- for (let y = 0; y < state.height; y++) {
403
- state.pixels[y]!.fill(null)
404
- }
405
- redraw()
406
- } else if (ch >= "1" && ch <= "9") {
407
- const preset = PRESETS[Number(ch) - 1]
408
- if (preset) {
409
- state.currentColor = [...preset.color]
410
- const [h, s, l] = rgbToHsl(...preset.color)
411
- state.hue = h
412
- state.saturation = s
413
- state.lightness = l
414
- state.tool = "pen"
415
- redraw()
416
- }
417
- } else if (ch === "0") {
418
- const preset = PRESETS[9]
419
- if (preset) {
420
- state.currentColor = [...preset.color]
421
- const [h, s, l] = rgbToHsl(...preset.color)
422
- state.hue = h
423
- state.saturation = s
424
- state.lightness = l
425
- state.tool = "pen"
426
- redraw()
427
- }
428
- } else if (ch === "[") {
429
- // Cycle hue left
430
- state.hue = (state.hue - 5 + 360) % 360
431
- syncColor()
432
- state.tool = "pen"
433
- redraw()
434
- } else if (ch === "]") {
435
- // Cycle hue right
436
- state.hue = (state.hue + 5) % 360
437
- syncColor()
438
- state.tool = "pen"
439
- redraw()
440
- } else if (ch === "-") {
441
- // Decrease saturation
442
- state.saturation = Math.max(0, state.saturation - 0.05)
443
- syncColor()
444
- state.tool = "pen"
445
- redraw()
446
- } else if (ch === "=") {
447
- // Increase saturation
448
- state.saturation = Math.min(1, state.saturation + 0.05)
449
- syncColor()
450
- state.tool = "pen"
451
- redraw()
452
- } else if (ch === "b") {
453
- // Cycle brightness: 0.5 -> 0.75 -> 1.0 -> 0.25 -> 0.5
454
- if (state.lightness < 0.3) {
455
- state.lightness = 0.5
456
- } else if (state.lightness < 0.55) {
457
- state.lightness = 0.75
458
- } else if (state.lightness < 0.8) {
459
- state.lightness = 1.0
460
- } else {
461
- state.lightness = 0.25
462
- }
463
- syncColor()
464
- state.tool = "pen"
465
- redraw()
466
- }
467
- }
468
- }
469
-
470
- const cleanup = () => {
471
- stdout.write(disableMouse())
472
- stdout.write("\x1b[?25h") // Show cursor
473
- stdout.write("\x1b[?1049l") // Exit alternate screen
474
- if (stdin.isTTY) {
475
- stdin.setRawMode(false)
476
- }
477
- stdin.off("data", onData)
478
- stdin.pause()
479
- process.exit(0)
480
- }
481
-
482
- stdin.on("data", onData)
483
-
484
- // Handle terminal resize
485
- stdout.on("resize", () => {
486
- const newCols = stdout.columns ?? cols
487
- const newRows = stdout.rows ?? rows
488
- const newState = createCanvas(newCols, newRows)
489
- // Copy existing pixels that still fit
490
- for (let y = 0; y < Math.min(state.height, newState.height); y++) {
491
- for (let x = 0; x < Math.min(state.width, newState.width); x++) {
492
- newState.pixels[y]![x] = state.pixels[y]![x]!
493
- }
494
- }
495
- state.pixels = newState.pixels
496
- state.width = newState.width
497
- state.height = newState.height
498
- stdout.write("\x1b[2J\x1b[H")
499
- redraw()
500
- })
501
- }
502
-
503
- if (import.meta.main) {
504
- main().catch((err) => {
505
- // Restore terminal on crash
506
- const stdout = process.stdout
507
- stdout.write("\x1b[?1003l\x1b[?1006l") // Disable mouse
508
- stdout.write("\x1b[?25h") // Show cursor
509
- stdout.write("\x1b[?1049l") // Exit alternate screen
510
- stdout.write("\x1b[0m") // Reset colors
511
- if (process.stdin.isTTY && process.stdin.isRaw) {
512
- try {
513
- process.stdin.setRawMode(false)
514
- } catch {}
515
- }
516
- console.error(err)
517
- process.exit(1)
518
- })
519
- }