@amritk/nish-x86_64-linux 0.20.0 → 0.21.0
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/INSTALL.md +22 -10
- package/bin/nish +0 -0
- package/docs/AI.md +2759 -0
- package/package.json +2 -1
- package/runtime/browser.d.ts +14 -0
- package/runtime/browser.mjs +88 -0
- package/runtime/core/arrays.d.ts +18 -0
- package/runtime/core/arrays.mjs +125 -0
- package/runtime/core/checks.mjs +20 -0
- package/runtime/core/host.d.ts +70 -0
- package/runtime/core/host.mjs +311 -0
- package/runtime/core/numbers.d.ts +53 -0
- package/runtime/core/numbers.mjs +316 -0
- package/runtime/core/result.d.ts +41 -0
- package/runtime/core/result.mjs +127 -0
- package/runtime/core/strings.d.ts +14 -0
- package/runtime/core/strings.mjs +169 -0
- package/runtime/core.d.ts +9 -0
- package/runtime/core.mjs +55 -0
- package/runtime/host.d.ts +6 -0
- package/runtime/nish.d.ts +44 -0
- package/runtime/nish.h +75 -2
- package/runtime/nish.mjs +23 -3
- package/runtime/node.mjs +383 -0
- package/runtime/runtime-bytes.c +401 -0
- package/runtime/runtime-cross.c +1 -1
- package/runtime/runtime-host.c +1 -1
- package/runtime/runtime-os.c +1 -1
- package/runtime/runtime-parallel.c +1 -1
- package/runtime/runtime-simd.c +231 -7
- package/runtime/runtime-term.c +504 -0
- package/runtime/runtime-wasm.c +0 -11
- package/runtime/runtime.c +190 -6
- package/runtime/shim.mjs +6 -985
- package/runtime/sqlite/LICENSE.md +85 -0
- package/runtime/sqlite/README.md +45 -0
- package/runtime/sqlite/nish-sqlite.c +359 -0
- package/runtime/sqlite/sqlite3.c +271131 -0
- package/runtime/sqlite/sqlite3.h +14447 -0
- package/scripts/build.sh +261 -53
- package/std/LICENSE-fast-float +23 -0
- package/std/LICENSE-fdlibm +8 -0
- package/std/README.md +31 -11
- package/std/base64.ts +116 -0
- package/std/bytes.ts +256 -0
- package/std/cli.ts +328 -0
- package/std/collections.ts +6 -5
- package/std/crypto/LICENSE-crypt_blowfish +26 -0
- package/std/crypto/base64url.ts +125 -0
- package/std/crypto/bcrypt.ts +760 -0
- package/std/crypto/hmac.ts +138 -2
- package/std/crypto/x509.ts +11 -122
- package/std/json-tokenizer.ts +2998 -57
- package/std/json-value.ts +22 -3
- package/std/json.ts +83 -1
- package/std/math.ts +608 -0
- package/std/net/http1-server.ts +85 -8
- package/std/net/http3-server.ts +50 -12
- package/std/net/postgres.ts +1875 -0
- package/std/net/quic-listener.ts +20 -4
- package/std/regex.ts +4274 -0
- package/std/sqlite.ts +391 -0
- package/std/term-io.ts +978 -0
- package/std/term-pixels.ts +874 -0
- package/std/term-probe.ts +459 -0
- package/std/term-rate.ts +993 -0
- package/std/term.ts +2453 -0
- package/std/text.ts +90 -7
- package/std/yaml-lines.ts +141 -0
- package/std/yaml.ts +4675 -0
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nish/term-pixels` — a framebuffer of pixels, and the encoders that draw it
|
|
3
|
+
* to a terminal with the best backend the terminal has: kitty graphics, Sixel,
|
|
4
|
+
* half-block cells or ASCII.
|
|
5
|
+
*
|
|
6
|
+
* A program draws `0xRRGGBB` pixels into a `Framebuffer` and never thinks about
|
|
7
|
+
* the terminal; `std/term-io` picks the backend and calls one encoder per frame.
|
|
8
|
+
* Everything here is pure: no capability, no I/O, and the bytes are the same
|
|
9
|
+
* natively and under Node, because the kitty and Sixel output is ASCII and the
|
|
10
|
+
* cell drawers go through `Screen`, which already is.
|
|
11
|
+
*
|
|
12
|
+
* - `Framebuffer(width, height)` holds the pixels, with `set`, `get`, `fill`,
|
|
13
|
+
* `clear` and `resize`. `scaleFramebuffer(src, divisor, dst)` shrinks one by
|
|
14
|
+
* an integer divisor, nearest-neighbour, for the refresh controller's
|
|
15
|
+
* resolution downgrade.
|
|
16
|
+
* - `GRAPHICS_ASCII`, `GRAPHICS_HALFBLOCK`, `GRAPHICS_SIXEL` and
|
|
17
|
+
* `GRAPHICS_KITTY` name the backends, numbered from worst to best so that a
|
|
18
|
+
* capability and a cap combine with `Math.min`.
|
|
19
|
+
* - `fitFramebuffer(fbW, fbH, cols, rows, cellW, cellH, backend)` answers the
|
|
20
|
+
* `PixelRect` the image is drawn in: the cells it covers, centred in a
|
|
21
|
+
* `cols × rows` area (letterboxed), and its size in the backend's pixels,
|
|
22
|
+
* keeping the framebuffer's aspect ratio.
|
|
23
|
+
* - `kittyFrame(fb, rect, imageId, out)` writes the kitty graphics commands for
|
|
24
|
+
* one frame into a `PixelBytes`; `KITTY_TRANSFER_DIRECT` is its transfer.
|
|
25
|
+
* Every frame retransmits the same `imageId`, which the terminal swaps in
|
|
26
|
+
* atomically, so there is no per-frame delete; `kittyDelete(imageId)` is the
|
|
27
|
+
* command that frees the image when the program closes.
|
|
28
|
+
* - `sixelFrame(fb, rect, dither, out)` writes one Sixel image, in the fixed
|
|
29
|
+
* palette `SIXEL_REGISTERS` counts, optionally ordered-dithered.
|
|
30
|
+
* - `drawHalfBlocks(fb, canvas, rect)` and `drawAscii(fb, screen, rect, colors)`
|
|
31
|
+
* draw onto the cells of a `Canvas` or a `Screen` from `nish/term`, whose own
|
|
32
|
+
* diff makes an unchanged cell cost nothing at `flush`.
|
|
33
|
+
*
|
|
34
|
+
* Every encoder samples the framebuffer nearest-neighbour onto the rect's
|
|
35
|
+
* pixels; none of them clears the letterbox around the rect, which is the
|
|
36
|
+
* caller's.
|
|
37
|
+
*
|
|
38
|
+
* `PixelBytes` holds the kitty and Sixel encoders' working arrays as well as
|
|
39
|
+
* their output, grown once to the largest frame, so a program that keeps one
|
|
40
|
+
* `PixelBytes` encodes frame after frame without allocating.
|
|
41
|
+
*
|
|
42
|
+
* Private helpers share the importing program's flat symbol namespace
|
|
43
|
+
* (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
|
|
44
|
+
*/
|
|
45
|
+
import { base64EncodeInto } from "nish/base64"
|
|
46
|
+
import { COLOR_DEFAULT, ATTR_NONE, Canvas, Screen } from "nish/term"
|
|
47
|
+
|
|
48
|
+
/** Text cells, one luminance character each: the Linux console's backend. */
|
|
49
|
+
export const GRAPHICS_ASCII: i32 = 0
|
|
50
|
+
|
|
51
|
+
/** Two pixels a cell, through `Canvas`'s half blocks. */
|
|
52
|
+
export const GRAPHICS_HALFBLOCK: i32 = 1
|
|
53
|
+
|
|
54
|
+
/** DEC Sixel, in the terminal's own pixels. */
|
|
55
|
+
export const GRAPHICS_SIXEL: i32 = 2
|
|
56
|
+
|
|
57
|
+
/** The kitty graphics protocol, in the terminal's own pixels. */
|
|
58
|
+
export const GRAPHICS_KITTY: i32 = 3
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* kitty's direct transfer, `t=d`: the pixels travel base64-encoded inside the
|
|
62
|
+
* escape codes themselves. It is the only transfer built. The protocol's
|
|
63
|
+
* shared-memory transfer (`t=s`, a POSIX shm object named in the payload) and
|
|
64
|
+
* zlib compression (`o=z`) are the seam for a local terminal and a slow link;
|
|
65
|
+
* a future `KITTY_TRANSFER_SHM` would take its own value here and its own
|
|
66
|
+
* branch in `kittyFrame`, which today has none to choose.
|
|
67
|
+
*/
|
|
68
|
+
export const KITTY_TRANSFER_DIRECT: i32 = 0
|
|
69
|
+
|
|
70
|
+
/** The Sixel registers the fixed palette uses: 6 reds × 7 greens × 6 blues. */
|
|
71
|
+
export const SIXEL_REGISTERS: i32 = 252
|
|
72
|
+
|
|
73
|
+
/** The pixel size of a cell `fitFramebuffer` assumes when the terminal did not say: VGA's 8×16. */
|
|
74
|
+
const PIXELS_CELL_W: i32 = 8
|
|
75
|
+
const PIXELS_CELL_H: i32 = 16
|
|
76
|
+
|
|
77
|
+
/** The base64 characters in one kitty chunk, the most the protocol allows. */
|
|
78
|
+
const PIXELS_KITTY_CHUNK: i32 = 4096
|
|
79
|
+
|
|
80
|
+
/** The pixel bytes one chunk carries: 4096 characters are 1024 groups of three. */
|
|
81
|
+
const PIXELS_KITTY_RAW: i32 = 3072
|
|
82
|
+
|
|
83
|
+
/** ESC, which opens every escape this module writes. */
|
|
84
|
+
const PIXELS_ESC: i32 = 27
|
|
85
|
+
|
|
86
|
+
// The rest of the bytes this module writes, named because a bare literal is an
|
|
87
|
+
// f64 under `--number-mode f64` and cannot be passed where an i32 is wanted.
|
|
88
|
+
|
|
89
|
+
/** One, as the i32 a byte count and a bit start from. */
|
|
90
|
+
const PIXELS_ONE: i32 = 1
|
|
91
|
+
|
|
92
|
+
/** `\`, which follows ESC to end an APC or a DCS (ECMA-48 ST). */
|
|
93
|
+
const PIXELS_BACKSLASH: i32 = 92
|
|
94
|
+
|
|
95
|
+
/** `[`, which follows ESC to open a control sequence (ECMA-48 CSI). */
|
|
96
|
+
const PIXELS_BRACKET: i32 = 91
|
|
97
|
+
|
|
98
|
+
/** `H`, the final byte of cursor position (ECMA-48 CUP). */
|
|
99
|
+
const PIXELS_CUP: i32 = 72
|
|
100
|
+
|
|
101
|
+
/** `0`, the first decimal digit. */
|
|
102
|
+
const PIXELS_DIGIT_0: i32 = 48
|
|
103
|
+
|
|
104
|
+
/** `;`, between the numbers of raster attributes and colour introducers. */
|
|
105
|
+
const PIXELS_SEMICOLON: i32 = 59
|
|
106
|
+
|
|
107
|
+
/** `!`, the Sixel repeat introducer (DECGRI). */
|
|
108
|
+
const SIXEL_REPEAT: i32 = 33
|
|
109
|
+
|
|
110
|
+
/** `?`, the sixel with no pixels set; a sixel is this plus its six bits. */
|
|
111
|
+
const SIXEL_BLANK: i32 = 63
|
|
112
|
+
|
|
113
|
+
/** `#`, the Sixel colour introducer (DECGCI). */
|
|
114
|
+
const SIXEL_COLOUR: i32 = 35
|
|
115
|
+
|
|
116
|
+
/** `$`, the Sixel carriage return (DECGCR): back to the band's first column. */
|
|
117
|
+
const SIXEL_RETURN: i32 = 36
|
|
118
|
+
|
|
119
|
+
/** `-`, the Sixel next line (DECGNL): down to the next six-pixel band. */
|
|
120
|
+
const SIXEL_NEXT_LINE: i32 = 45
|
|
121
|
+
|
|
122
|
+
/** The 4×4 Bayer matrix, row by row, one hex digit a threshold: the order of ordered dithering. */
|
|
123
|
+
const PIXELS_BAYER: string = "082ac4e63b19f7d5"
|
|
124
|
+
|
|
125
|
+
/** The luminance ramp `drawAscii` draws with, darkest first. */
|
|
126
|
+
const PIXELS_RAMP: string = " .:-=+*#%@"
|
|
127
|
+
|
|
128
|
+
/** xterm's 16 standard colours in their SGR order, six hex digits each. */
|
|
129
|
+
const PIXELS_ANSI16: string =
|
|
130
|
+
"000000cd000000cd00cdcd000000eecd00cd00cdcde5e5e57f7f7fff000000ff00ffff005c5cffff00ff00ffffffffff"
|
|
131
|
+
|
|
132
|
+
/** The value of the hex digits `text[at..at+count)`. */
|
|
133
|
+
const pixelsHex = (text: string, at: i32, count: i32): i32 => {
|
|
134
|
+
let v: i32 = 0
|
|
135
|
+
const length: i32 = toI32(text.length)
|
|
136
|
+
for (let i: i32 = at; i < at + count && i >= 0 && i < length; i++) {
|
|
137
|
+
const c: i32 = toI32(text.charCodeAt(i))
|
|
138
|
+
v = (v << 4) | (c >= 97 ? c - 87 : c - 48)
|
|
139
|
+
}
|
|
140
|
+
return v
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Panic, as `who`, unless `width × height` is a size an array of pixels can have. */
|
|
144
|
+
const pixelsCheckSize = (who: string, width: i32, height: i32): void => {
|
|
145
|
+
if (width < 0 || height < 0) {
|
|
146
|
+
panic(`${who}: a negative size`)
|
|
147
|
+
}
|
|
148
|
+
if (width > 0 && height > ((2147483647 / width) | 0)) {
|
|
149
|
+
panic(`${who}: more than 2^31 - 1 pixels`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** A `width × height` grid of `0xRRGGBB` pixels, row by row, all black when made. */
|
|
154
|
+
export class Framebuffer {
|
|
155
|
+
/** Pixels across. */
|
|
156
|
+
width: i32
|
|
157
|
+
/** Pixels down. */
|
|
158
|
+
height: i32
|
|
159
|
+
/** One colour per pixel, `y * width + x`. */
|
|
160
|
+
pixels: i32[]
|
|
161
|
+
|
|
162
|
+
/** A black framebuffer of `width × height`; a negative size, or more than 2^31 - 1 pixels, panics. */
|
|
163
|
+
constructor(width: i32, height: i32) {
|
|
164
|
+
pixelsCheckSize("Framebuffer", width, height)
|
|
165
|
+
this.width = width
|
|
166
|
+
this.height = height
|
|
167
|
+
this.pixels = new Array<i32>(width * height)
|
|
168
|
+
this.pixels.fill(0)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Make the framebuffer `width × height` and black; the array is replaced only when the size changes. */
|
|
172
|
+
resize(width: i32, height: i32): void {
|
|
173
|
+
pixelsCheckSize("Framebuffer.resize", width, height)
|
|
174
|
+
if (width !== this.width || height !== this.height) {
|
|
175
|
+
this.width = width
|
|
176
|
+
this.height = height
|
|
177
|
+
this.pixels = new Array<i32>(width * height)
|
|
178
|
+
}
|
|
179
|
+
this.pixels.fill(0)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Set the pixel at `x`, `y` to `colour`'s low 24 bits; a pixel off the framebuffer is not drawn. */
|
|
183
|
+
set(x: i32, y: i32, colour: i32): void {
|
|
184
|
+
if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
this.pixels[y * this.width + x] = colour & 0xffffff
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The colour of the pixel at `x`, `y`, or black off the framebuffer. */
|
|
191
|
+
get(x: i32, y: i32): i32 {
|
|
192
|
+
if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
|
|
193
|
+
return 0
|
|
194
|
+
}
|
|
195
|
+
return this.pixels[y * this.width + x]
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Set every pixel to `colour`'s low 24 bits. */
|
|
199
|
+
fill(colour: i32): void {
|
|
200
|
+
this.pixels.fill(colour & 0xffffff)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Set every pixel to black. */
|
|
204
|
+
clear(): void {
|
|
205
|
+
this.pixels.fill(0)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Shrink `src` by `divisor` into `dst`, which is resized to
|
|
211
|
+
* `ceil(width / divisor) × ceil(height / divisor)`: each pixel of `dst` is the
|
|
212
|
+
* top-left pixel of its `divisor × divisor` block, nearest-neighbour, so a
|
|
213
|
+
* frame costs a quarter or a sixteenth of the bytes at divisor 2 or 4. A
|
|
214
|
+
* divisor below 1 panics; 1 copies. `dst` must be another framebuffer than
|
|
215
|
+
* `src`, which resizing it would blank.
|
|
216
|
+
*/
|
|
217
|
+
export const scaleFramebuffer = (src: Framebuffer, divisor: i32, dst: Framebuffer): void => {
|
|
218
|
+
if (divisor < 1) {
|
|
219
|
+
panic("scaleFramebuffer: a divisor below 1")
|
|
220
|
+
}
|
|
221
|
+
const w: i32 = pixelsCeilDiv(src.width, divisor)
|
|
222
|
+
const h: i32 = pixelsCeilDiv(src.height, divisor)
|
|
223
|
+
dst.resize(w, h)
|
|
224
|
+
for (let y: i32 = 0; y < h; y++) {
|
|
225
|
+
const from: i32 = y * divisor * src.width
|
|
226
|
+
for (let x: i32 = 0; x < w; x++) {
|
|
227
|
+
dst.pixels[y * w + x] = src.pixels[from + x * divisor]
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Where a framebuffer is drawn: the cells it covers and its size in the
|
|
234
|
+
* backend's pixels. `width` and `height` are terminal pixels for kitty and
|
|
235
|
+
* Sixel, half-cell pixels for half-blocks (`cols` across, up to `rows * 2`
|
|
236
|
+
* down) and cells for ASCII (`cols × rows`).
|
|
237
|
+
*/
|
|
238
|
+
export class PixelRect {
|
|
239
|
+
/** The first column covered, from 0. */
|
|
240
|
+
col: i32
|
|
241
|
+
/** The first row covered, from 0. */
|
|
242
|
+
row: i32
|
|
243
|
+
/** The columns covered. */
|
|
244
|
+
cols: i32
|
|
245
|
+
/** The rows covered. */
|
|
246
|
+
rows: i32
|
|
247
|
+
/** The image's width in the backend's pixels. */
|
|
248
|
+
width: i32
|
|
249
|
+
/** The image's height in the backend's pixels. */
|
|
250
|
+
height: i32
|
|
251
|
+
/** The backend's pixels per framebuffer pixel across; ASCII's rows are half that down. */
|
|
252
|
+
scale: f64
|
|
253
|
+
|
|
254
|
+
/** A rect of the given cells and size; `fitFramebuffer` is how one is usually made. */
|
|
255
|
+
constructor(col: i32, row: i32, cols: i32, rows: i32, width: i32, height: i32, scale: f64) {
|
|
256
|
+
this.col = col
|
|
257
|
+
this.row = row
|
|
258
|
+
this.cols = cols
|
|
259
|
+
this.rows = rows
|
|
260
|
+
this.width = width
|
|
261
|
+
this.height = height
|
|
262
|
+
this.scale = scale
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** `a / b` rounded up, for `a >= 0` and `b > 0`. */
|
|
267
|
+
const pixelsCeilDiv = (a: i32, b: i32): i32 => (a === 0 ? 0 : (((a - 1) / b) | 0) + 1)
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The rect a `fbW × fbH` framebuffer is drawn in on a `cols × rows` area of
|
|
271
|
+
* cells `cellW × cellH` pixels each, by `backend` (a `GRAPHICS_` constant): as
|
|
272
|
+
* large as fits with the framebuffer's aspect ratio kept, and centred, so the
|
|
273
|
+
* spare cells on two sides are the letterbox. The rect starts at column and
|
|
274
|
+
* row 0 of the area; the caller adds the area's own offset.
|
|
275
|
+
*
|
|
276
|
+
* kitty and Sixel measure in the cell's pixels, taken as 8×16 when `cellW` or
|
|
277
|
+
* `cellH` is not positive (the terminal did not answer `CSI 16 t`). Half-blocks
|
|
278
|
+
* and ASCII take a cell as one pixel wide and two tall, the shape of a usual
|
|
279
|
+
* terminal font, so a square framebuffer stays square. An empty framebuffer or
|
|
280
|
+
* area answers an empty rect at 0, 0.
|
|
281
|
+
*/
|
|
282
|
+
export const fitFramebuffer = (
|
|
283
|
+
fbW: i32,
|
|
284
|
+
fbH: i32,
|
|
285
|
+
cols: i32,
|
|
286
|
+
rows: i32,
|
|
287
|
+
cellW: i32,
|
|
288
|
+
cellH: i32,
|
|
289
|
+
backend: i32
|
|
290
|
+
): PixelRect => {
|
|
291
|
+
if (fbW <= 0 || fbH <= 0 || cols <= 0 || rows <= 0) {
|
|
292
|
+
return new PixelRect(0, 0, 0, 0, 0, 0, 0.0)
|
|
293
|
+
}
|
|
294
|
+
const inPixels: boolean = backend === GRAPHICS_SIXEL || backend === GRAPHICS_KITTY
|
|
295
|
+
const unknownCell: boolean = cellW <= 0 || cellH <= 0
|
|
296
|
+
let pw: i32 = 1
|
|
297
|
+
let ph: i32 = 2
|
|
298
|
+
if (inPixels) {
|
|
299
|
+
pw = unknownCell ? PIXELS_CELL_W : cellW
|
|
300
|
+
ph = unknownCell ? PIXELS_CELL_H : cellH
|
|
301
|
+
}
|
|
302
|
+
// The products are f64, exact below 2^53, so that a large area cannot
|
|
303
|
+
// overflow i32 while the two aspect ratios are compared.
|
|
304
|
+
const availW: f64 = toF64(cols) * toF64(pw)
|
|
305
|
+
const availH: f64 = toF64(rows) * toF64(ph)
|
|
306
|
+
let w: i32 = 0
|
|
307
|
+
let h: i32 = 0
|
|
308
|
+
if (toF64(fbW) * availH >= toF64(fbH) * availW) {
|
|
309
|
+
w = toI32(availW)
|
|
310
|
+
h = toI32(Math.floor((toF64(fbH) * availW) / toF64(fbW)))
|
|
311
|
+
} else {
|
|
312
|
+
h = toI32(availH)
|
|
313
|
+
w = toI32(Math.floor((toF64(fbW) * availH) / toF64(fbH)))
|
|
314
|
+
}
|
|
315
|
+
w = w < 1 ? 1 : w
|
|
316
|
+
h = h < 1 ? 1 : h
|
|
317
|
+
const usedCols: i32 = pixelsCeilDiv(w, pw)
|
|
318
|
+
const usedRows: i32 = pixelsCeilDiv(h, ph)
|
|
319
|
+
const scale: f64 = toF64(w) / toF64(fbW)
|
|
320
|
+
const col: i32 = (cols - usedCols) >> 1
|
|
321
|
+
const row: i32 = (rows - usedRows) >> 1
|
|
322
|
+
if (backend === GRAPHICS_ASCII) {
|
|
323
|
+
return new PixelRect(col, row, usedCols, usedRows, usedCols, usedRows, scale)
|
|
324
|
+
}
|
|
325
|
+
return new PixelRect(col, row, usedCols, usedRows, w, h, scale)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* A growable buffer of the bytes an encoder writes. `text()` is them as a
|
|
330
|
+
* string, ready for one write; `reset()` empties it and keeps its capacity, so
|
|
331
|
+
* one buffer serves every frame.
|
|
332
|
+
*
|
|
333
|
+
* It also holds the working arrays of `kittyFrame` and `sixelFrame`, grown to
|
|
334
|
+
* the largest frame and then reused, so that encoding a frame allocates
|
|
335
|
+
* nothing once the buffer has seen one as large: a frame loop calling an
|
|
336
|
+
* encoder 60 times a second runs in flat memory. An encoder's arrays could
|
|
337
|
+
* not be its own locals, because the arena cannot take a call's allocations
|
|
338
|
+
* back while that call grows a buffer it was handed (`bytes` above).
|
|
339
|
+
*/
|
|
340
|
+
export class PixelBytes {
|
|
341
|
+
/** The bytes, of which the first `length` are written. */
|
|
342
|
+
bytes: u8[]
|
|
343
|
+
/** How many bytes are written. */
|
|
344
|
+
length: i32
|
|
345
|
+
/** One kitty chunk's RGB bytes. */
|
|
346
|
+
kittyRaw: u8[]
|
|
347
|
+
/** The framebuffer column and row each destination pixel samples. */
|
|
348
|
+
sampleXs: i32[]
|
|
349
|
+
sampleYs: i32[]
|
|
350
|
+
/** Every destination pixel's Sixel register, row by row. */
|
|
351
|
+
sixelReg: i32[]
|
|
352
|
+
/** Which registers a Sixel frame uses, 1 or 0. */
|
|
353
|
+
sixelUsed: u8[]
|
|
354
|
+
/** The first and last column of a band each register covers, -1 for none. */
|
|
355
|
+
sixelFirst: i32[]
|
|
356
|
+
sixelLast: i32[]
|
|
357
|
+
/** Each register's sixels across a band, all zero between bands. */
|
|
358
|
+
sixelBits: i32[]
|
|
359
|
+
|
|
360
|
+
/** An empty buffer with room for `capacity` bytes before it grows. */
|
|
361
|
+
constructor(capacity: i32) {
|
|
362
|
+
this.bytes = new Array<u8>(Math.max(capacity, 16))
|
|
363
|
+
this.bytes.fill(0)
|
|
364
|
+
this.length = 0
|
|
365
|
+
this.kittyRaw = new Array<u8>(PIXELS_KITTY_RAW)
|
|
366
|
+
this.kittyRaw.fill(0)
|
|
367
|
+
this.sampleXs = []
|
|
368
|
+
this.sampleYs = []
|
|
369
|
+
this.sixelReg = []
|
|
370
|
+
this.sixelUsed = new Array<u8>(SIXEL_REGISTERS)
|
|
371
|
+
this.sixelUsed.fill(0)
|
|
372
|
+
this.sixelFirst = new Array<i32>(SIXEL_REGISTERS)
|
|
373
|
+
this.sixelFirst.fill(-1)
|
|
374
|
+
this.sixelLast = new Array<i32>(SIXEL_REGISTERS)
|
|
375
|
+
this.sixelLast.fill(-1)
|
|
376
|
+
this.sixelBits = []
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Forget what was written. */
|
|
380
|
+
reset(): void {
|
|
381
|
+
this.length = 0
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Make room for `n` more bytes, doubling the array as often as that takes. */
|
|
385
|
+
reserve(n: i32): void {
|
|
386
|
+
const need: i32 = this.length + n
|
|
387
|
+
if (need <= toI32(this.bytes.length)) {
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
// Doubled in f64, which cannot overflow, and capped at the longest array.
|
|
391
|
+
let size: f64 = toF64(this.bytes.length)
|
|
392
|
+
while (size < toF64(need)) {
|
|
393
|
+
size = size * 2.0
|
|
394
|
+
}
|
|
395
|
+
const grown: u8[] = new Array<u8>(toI32(Math.min(size, 2147483647.0)))
|
|
396
|
+
grown.fill(0)
|
|
397
|
+
grown.set(this.bytes, 0)
|
|
398
|
+
this.bytes = grown
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Append the byte `b`. */
|
|
402
|
+
put(b: i32): void {
|
|
403
|
+
this.reserve(PIXELS_ONE)
|
|
404
|
+
this.bytes[this.length] = toU8(b)
|
|
405
|
+
this.length = this.length + 1
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Append `s`, which is ASCII. */
|
|
409
|
+
putText(s: string): void {
|
|
410
|
+
const n: i32 = toI32(s.length)
|
|
411
|
+
this.reserve(n)
|
|
412
|
+
for (let i: i32 = 0; i < n; i++) {
|
|
413
|
+
this.bytes[this.length + i] = toU8(s.charCodeAt(i))
|
|
414
|
+
}
|
|
415
|
+
this.length = this.length + n
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Append `v`, at least 0, in decimal. */
|
|
419
|
+
putDecimal(v: i32): void {
|
|
420
|
+
if (v >= 10) {
|
|
421
|
+
this.putDecimal((v / 10) | 0)
|
|
422
|
+
}
|
|
423
|
+
this.put(PIXELS_DIGIT_0 + (v % 10))
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** What was written, as one string. */
|
|
427
|
+
text(): string {
|
|
428
|
+
return String.fromBytes(this.bytes, 0, this.length)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* For each of `n` destination pixels along one axis, the framebuffer pixel it
|
|
434
|
+
* samples: the one under the destination pixel's centre, nearest-neighbour,
|
|
435
|
+
* computed once per frame rather than once per pixel.
|
|
436
|
+
*/
|
|
437
|
+
const pixelsSampleMap = (n: i32, source: i32): i32[] => {
|
|
438
|
+
const map: i32[] = new Array<i32>(n)
|
|
439
|
+
pixelsSampleMapInto(map, n, source)
|
|
440
|
+
return map
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** `pixelsSampleMap` written into the first `n` entries of `map`, which is at least `n` long. */
|
|
444
|
+
const pixelsSampleMapInto = (map: i32[], n: i32, source: i32): void => {
|
|
445
|
+
for (let i: i32 = 0; i < n && i < toI32(map.length); i++) {
|
|
446
|
+
const s: i32 = toI32(Math.floor(((toF64(i) + 0.5) * toF64(source)) / toF64(n)))
|
|
447
|
+
map[i] = s >= source ? source - 1 : s
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* `a` when it is at least `n` long, else a new zeroed array for a `PixelBytes`
|
|
453
|
+
* working array: twice `a`'s length, or `n` if that is more, so frames that
|
|
454
|
+
* grow a little at a time reallocate a logarithmic number of times.
|
|
455
|
+
*/
|
|
456
|
+
const pixelsAtLeast = (a: i32[], n: i32): i32[] => {
|
|
457
|
+
const had: i32 = toI32(a.length)
|
|
458
|
+
if (had >= n) {
|
|
459
|
+
return a
|
|
460
|
+
}
|
|
461
|
+
const twice: f64 = Math.min(toF64(had) * 2.0, 2147483647.0)
|
|
462
|
+
const grown: i32[] = new Array<i32>(twice > toF64(n) ? toI32(twice) : n)
|
|
463
|
+
grown.fill(0)
|
|
464
|
+
return grown
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Append `CSI row ; col H`, the cursor to the 0-based cell (`col`, `row`), as `moveCursor` writes it. */
|
|
468
|
+
const pixelsMoveTo = (out: PixelBytes, col: i32, row: i32): void => {
|
|
469
|
+
out.put(PIXELS_ESC)
|
|
470
|
+
out.put(PIXELS_BRACKET)
|
|
471
|
+
out.putDecimal(row + 1)
|
|
472
|
+
out.put(PIXELS_SEMICOLON)
|
|
473
|
+
out.putDecimal(col + 1)
|
|
474
|
+
out.put(PIXELS_CUP)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Append `ESC _ G`, which opens a kitty graphics command (an APC). */
|
|
478
|
+
const pixelsKittyOpen = (out: PixelBytes): void => {
|
|
479
|
+
out.put(PIXELS_ESC)
|
|
480
|
+
out.putText("_G")
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Append `ESC \`, the string terminator that closes an APC or a DCS. */
|
|
484
|
+
const pixelsStringEnd = (out: PixelBytes): void => {
|
|
485
|
+
out.put(PIXELS_ESC)
|
|
486
|
+
out.put(PIXELS_BACKSLASH)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Append one frame of `fb` in the kitty graphics protocol
|
|
491
|
+
* (https://sw.kovidgoyal.net/kitty/graphics-protocol/), placed over `rect`.
|
|
492
|
+
*
|
|
493
|
+
* 1. The cursor moves to the rect's first cell, where kitty places an image.
|
|
494
|
+
* 2. `a=T` transmits and displays at once: `f=24` is packed RGB, `t=d` direct
|
|
495
|
+
* transfer (`KITTY_TRANSFER_DIRECT`), `i` the image and `p=1` its one
|
|
496
|
+
* placement, `s` and `v` the framebuffer's own size, `c` and `r` the rect's
|
|
497
|
+
* cells, which kitty scales the image to — so a downgraded framebuffer
|
|
498
|
+
* costs fewer bytes and still fills the rect — `C=1` leaves the cursor
|
|
499
|
+
* where it was, and `q=2` suppresses every reply.
|
|
500
|
+
* 3. The RGB bytes go as base64 (RFC 4648 §4) in chunks of 4096 characters,
|
|
501
|
+
* the protocol's limit: each but the last carries `m=1`, the last `m=0`.
|
|
502
|
+
* Only the first chunk carries the keys; the rest carry `q=2` and `m`,
|
|
503
|
+
* since a terminal may answer a chunk that does not repeat `q`.
|
|
504
|
+
*
|
|
505
|
+
* Every frame reuses `imageId` and nothing is deleted first. Transmitting an
|
|
506
|
+
* id that exists replaces that image and frees the old one's data, and the
|
|
507
|
+
* same image and placement id replace the placement, so the terminal holds
|
|
508
|
+
* one frame however many are sent. The new frame takes over only when its
|
|
509
|
+
* last chunk (`m=0`) has arrived, so the old one stays on screen while a
|
|
510
|
+
* frame of hundreds of chunks streams in. Deleting first would blank the
|
|
511
|
+
* image for that whole time. `kittyDelete` removes the image when the program
|
|
512
|
+
* is done with it.
|
|
513
|
+
*
|
|
514
|
+
* `imageId` is 1 to 2^31 - 1, since 0 is the protocol's "no id"; anything
|
|
515
|
+
* else panics. An empty framebuffer or rect writes nothing.
|
|
516
|
+
*/
|
|
517
|
+
export const kittyFrame = (fb: Framebuffer, rect: PixelRect, imageId: i32, out: PixelBytes): void => {
|
|
518
|
+
pixelsCheckImageId("kittyFrame", imageId)
|
|
519
|
+
const count: i32 = fb.width * fb.height
|
|
520
|
+
if (count === 0 || rect.cols <= 0 || rect.rows <= 0) {
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
pixelsMoveTo(out, rect.col, rect.row)
|
|
524
|
+
const raw: u8[] = out.kittyRaw
|
|
525
|
+
const total: i32 = count * 3
|
|
526
|
+
let sent: i32 = 0
|
|
527
|
+
let pixel: i32 = 0
|
|
528
|
+
while (sent < total) {
|
|
529
|
+
// The next chunk's pixels as RGB bytes, 1024 pixels to a full chunk.
|
|
530
|
+
const n: i32 = total - sent < PIXELS_KITTY_RAW ? total - sent : PIXELS_KITTY_RAW
|
|
531
|
+
// `k + 2 < raw.length` always holds below `n`; it proves the stores in range.
|
|
532
|
+
for (let k: i32 = 0; k < n && k + 2 < toI32(raw.length); k = k + 3) {
|
|
533
|
+
const c: i32 = fb.pixels[pixel]
|
|
534
|
+
raw[k] = toU8((c >> 16) & 255)
|
|
535
|
+
raw[k + 1] = toU8((c >> 8) & 255)
|
|
536
|
+
raw[k + 2] = toU8(c & 255)
|
|
537
|
+
pixel = pixel + 1
|
|
538
|
+
}
|
|
539
|
+
pixelsKittyOpen(out)
|
|
540
|
+
if (sent === 0) {
|
|
541
|
+
out.putText("a=T,f=24,t=d,i=")
|
|
542
|
+
out.putDecimal(imageId)
|
|
543
|
+
out.putText(",p=1,s=")
|
|
544
|
+
out.putDecimal(fb.width)
|
|
545
|
+
out.putText(",v=")
|
|
546
|
+
out.putDecimal(fb.height)
|
|
547
|
+
out.putText(",c=")
|
|
548
|
+
out.putDecimal(rect.cols)
|
|
549
|
+
out.putText(",r=")
|
|
550
|
+
out.putDecimal(rect.rows)
|
|
551
|
+
out.putText(",C=1,")
|
|
552
|
+
}
|
|
553
|
+
out.putText("q=2,")
|
|
554
|
+
sent = sent + n
|
|
555
|
+
out.putText(sent < total ? "m=1;" : "m=0;")
|
|
556
|
+
out.reserve(PIXELS_KITTY_CHUNK)
|
|
557
|
+
out.length = base64EncodeInto(raw, 0, n, out.bytes, out.length)
|
|
558
|
+
pixelsStringEnd(out)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** Panic, as `who`, unless `imageId` is a kitty image id: 1 or more, 0 being "no id". */
|
|
563
|
+
const pixelsCheckImageId = (who: string, imageId: i32): void => {
|
|
564
|
+
if (imageId < 1) {
|
|
565
|
+
panic(`${who}: an image id below 1`)
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* The kitty command that deletes the image `imageId`, every placement of it
|
|
571
|
+
* and its data (`a=d,d=I`, the uppercase freeing the data), with `q=2` so the
|
|
572
|
+
* terminal does not answer. A program writes it when it stops drawing, and
|
|
573
|
+
* puts it in its terminal restore string, so the last frame does not stay
|
|
574
|
+
* behind. Panics, like `kittyFrame`, on an id below 1.
|
|
575
|
+
*/
|
|
576
|
+
export const kittyDelete = (imageId: i32): string => {
|
|
577
|
+
pixelsCheckImageId("kittyDelete", imageId)
|
|
578
|
+
return `\x1b_Ga=d,d=I,i=${imageId},q=2\x1b\\`
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** The Sixel register of the palette colour with red level `r` (0–5), green `g` (0–6) and blue `b` (0–5). */
|
|
582
|
+
const pixelsSixelRegister = (r: i32, g: i32, b: i32): i32 => r * 42 + g * 6 + b
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* The level of one 0–255 component among `levels`, nearest, after `bias` (a
|
|
586
|
+
* dither offset, 0 without dithering) is added and the sum clamped to 0–255.
|
|
587
|
+
*/
|
|
588
|
+
const pixelsLevel = (v: i32, bias: i32, levels: i32): i32 => {
|
|
589
|
+
const x: i32 = Math.min(Math.max(v + bias, 0), 255)
|
|
590
|
+
return ((x * (levels - 1) * 2 + 255) / 510) | 0
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* The dither offset for one component quantised to `levels`, at Bayer
|
|
595
|
+
* threshold `t` (0–15): spread evenly over one quantisation step, centred on
|
|
596
|
+
* zero, so a flat colour between two levels becomes a fixed pattern of both.
|
|
597
|
+
*/
|
|
598
|
+
const pixelsDitherBias = (t: i32, levels: i32): i32 => (((t * 2 - 15) * 255) / ((levels - 1) * 32)) | 0
|
|
599
|
+
|
|
600
|
+
/** Append the sixel character for the six-pixel column `bits`, `count` times, run-length encoded. */
|
|
601
|
+
const pixelsSixelRun = (out: PixelBytes, bits: i32, count: i32): void => {
|
|
602
|
+
// `!n` followed by the character repeats it n times (DECGRI); three or fewer
|
|
603
|
+
// are as short written out.
|
|
604
|
+
if (count > 3) {
|
|
605
|
+
out.put(SIXEL_REPEAT)
|
|
606
|
+
out.putDecimal(count)
|
|
607
|
+
out.put(SIXEL_BLANK + bits)
|
|
608
|
+
} else {
|
|
609
|
+
for (let k: i32 = 0; k < count; k++) {
|
|
610
|
+
out.put(SIXEL_BLANK + bits)
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Append one frame of `fb` as DEC Sixel graphics, drawn over `rect`'s
|
|
617
|
+
* `width × height` terminal pixels from its first cell. The format is DEC's
|
|
618
|
+
* (VT330/VT340 Programmer Reference, "Sixel Graphics"; DECSIXEL):
|
|
619
|
+
*
|
|
620
|
+
* - `ESC P q` opens the device control string with the default parameters, and
|
|
621
|
+
* `ESC \` closes it.
|
|
622
|
+
* - `"1;1;w;h` is the raster attributes (DECGRA): square pixels and the
|
|
623
|
+
* image's size, so the terminal can reserve it before the data arrives.
|
|
624
|
+
* - `#n;2;r;g;b` sets colour register `n` to an RGB colour in percent
|
|
625
|
+
* (DECGCI). The palette is fixed: 6 levels of red, 7 of green and 6 of blue,
|
|
626
|
+
* 252 registers (`SIXEL_REGISTERS`), the most that fits the 256 a
|
|
627
|
+
* terminal must offer. A register always holds the same colour, so frames
|
|
628
|
+
* never fight over the palette and nothing flickers; each frame declares the
|
|
629
|
+
* registers it uses, in ascending order.
|
|
630
|
+
* - The image goes in bands six pixels tall. Within a band, `#n` selects a
|
|
631
|
+
* register and each character `?`–`~` is a column of six pixels, one bit
|
|
632
|
+
* each, painted in it; `!n` repeats a character, `$` returns to the band's
|
|
633
|
+
* start for the next colour, and `-` moves to the next band.
|
|
634
|
+
*
|
|
635
|
+
* With `dither`, each pixel's components are offset by the 4×4 Bayer
|
|
636
|
+
* matrix's threshold before they are quantised (ordered dithering), so a
|
|
637
|
+
* gradient becomes a fine, stable pattern rather than steps. An empty rect
|
|
638
|
+
* writes nothing.
|
|
639
|
+
*/
|
|
640
|
+
export const sixelFrame = (fb: Framebuffer, rect: PixelRect, dither: boolean, out: PixelBytes): void => {
|
|
641
|
+
const w: i32 = rect.width
|
|
642
|
+
const h: i32 = rect.height
|
|
643
|
+
if (w <= 0 || h <= 0 || fb.width <= 0 || fb.height <= 0) {
|
|
644
|
+
return
|
|
645
|
+
}
|
|
646
|
+
pixelsCheckSize("sixelFrame", w, h)
|
|
647
|
+
pixelsCheckSize("sixelFrame", w, SIXEL_REGISTERS)
|
|
648
|
+
// The working arrays live on `out`, grown once to the largest frame.
|
|
649
|
+
out.sampleXs = pixelsAtLeast(out.sampleXs, w)
|
|
650
|
+
out.sampleYs = pixelsAtLeast(out.sampleYs, h)
|
|
651
|
+
out.sixelReg = pixelsAtLeast(out.sixelReg, w * h)
|
|
652
|
+
out.sixelBits = pixelsAtLeast(out.sixelBits, SIXEL_REGISTERS * w)
|
|
653
|
+
const xs: i32[] = out.sampleXs
|
|
654
|
+
const ys: i32[] = out.sampleYs
|
|
655
|
+
pixelsSampleMapInto(xs, w, fb.width)
|
|
656
|
+
pixelsSampleMapInto(ys, h, fb.height)
|
|
657
|
+
// Every destination pixel's register, and which registers the frame uses.
|
|
658
|
+
const reg: i32[] = out.sixelReg
|
|
659
|
+
const used: u8[] = out.sixelUsed
|
|
660
|
+
used.fill(0)
|
|
661
|
+
// `ys` is at least `h` long and `xs` `w`: bounding the loops by their lengths
|
|
662
|
+
// too is what proves the reads in range.
|
|
663
|
+
for (let y: i32 = 0; y < h && y < toI32(ys.length); y++) {
|
|
664
|
+
const from: i32 = ys[y] * fb.width
|
|
665
|
+
for (let x: i32 = 0; x < w && x < toI32(xs.length); x++) {
|
|
666
|
+
const c: i32 = fb.pixels[from + xs[x]]
|
|
667
|
+
let br: i32 = 0
|
|
668
|
+
let bg: i32 = 0
|
|
669
|
+
let bb: i32 = 0
|
|
670
|
+
if (dither) {
|
|
671
|
+
const t: i32 = pixelsHex(PIXELS_BAYER, ((y & 3) << 2) | (x & 3), 1)
|
|
672
|
+
br = pixelsDitherBias(t, 6)
|
|
673
|
+
bg = pixelsDitherBias(t, 7)
|
|
674
|
+
bb = pixelsDitherBias(t, 6)
|
|
675
|
+
}
|
|
676
|
+
const n: i32 = pixelsSixelRegister(
|
|
677
|
+
pixelsLevel((c >> 16) & 255, br, 6),
|
|
678
|
+
pixelsLevel((c >> 8) & 255, bg, 7),
|
|
679
|
+
pixelsLevel(c & 255, bb, 6)
|
|
680
|
+
)
|
|
681
|
+
reg[y * w + x] = n
|
|
682
|
+
// Always true (a register is 0 to 251); it proves the store in range.
|
|
683
|
+
if (n >= 0 && n < toI32(used.length)) {
|
|
684
|
+
used[n] = 1
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
pixelsMoveTo(out, rect.col, rect.row)
|
|
689
|
+
out.put(PIXELS_ESC)
|
|
690
|
+
out.putText('Pq"1;1;')
|
|
691
|
+
out.putDecimal(w)
|
|
692
|
+
out.put(PIXELS_SEMICOLON)
|
|
693
|
+
out.putDecimal(h)
|
|
694
|
+
for (let n: i32 = 0; n < toI32(used.length); n++) {
|
|
695
|
+
if (used[n] !== 0) {
|
|
696
|
+
const r: i32 = (n / 42) | 0
|
|
697
|
+
const g: i32 = ((n / 6) | 0) % 7
|
|
698
|
+
const b: i32 = n % 6
|
|
699
|
+
out.put(SIXEL_COLOUR)
|
|
700
|
+
out.putDecimal(n)
|
|
701
|
+
out.putText(";2;")
|
|
702
|
+
out.putDecimal(((r * 100) / 5) | 0)
|
|
703
|
+
out.put(PIXELS_SEMICOLON)
|
|
704
|
+
out.putDecimal(((g * 100) / 6) | 0)
|
|
705
|
+
out.put(PIXELS_SEMICOLON)
|
|
706
|
+
out.putDecimal(((b * 100) / 5) | 0)
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// Per band: the first and last column each register appears in, so a colour
|
|
710
|
+
// is only walked over the span it covers, and each register's sixels across
|
|
711
|
+
// the band, `n * w + x`, built in one pass over its pixels. Emitting a span
|
|
712
|
+
// zeroes it, which leaves `bits` all zero for the next band and the next
|
|
713
|
+
// frame.
|
|
714
|
+
const first: i32[] = out.sixelFirst
|
|
715
|
+
const last: i32[] = out.sixelLast
|
|
716
|
+
const bits: i32[] = out.sixelBits
|
|
717
|
+
for (let top: i32 = 0; top < h; top = top + 6) {
|
|
718
|
+
const bottom: i32 = top + 6 < h ? top + 6 : h
|
|
719
|
+
first.fill(-1)
|
|
720
|
+
for (let y: i32 = top; y < bottom; y++) {
|
|
721
|
+
const bit: i32 = PIXELS_ONE << (y - top)
|
|
722
|
+
for (let x: i32 = 0; x < w; x++) {
|
|
723
|
+
const n: i32 = reg[y * w + x]
|
|
724
|
+
// Never taken (a register is 0 to 251); it proves `first[n]` and
|
|
725
|
+
// `last[n]` in range.
|
|
726
|
+
if (n < 0 || n >= toI32(first.length) || n >= toI32(last.length)) {
|
|
727
|
+
continue
|
|
728
|
+
}
|
|
729
|
+
bits[n * w + x] = bits[n * w + x] | bit
|
|
730
|
+
if (first[n] < 0) {
|
|
731
|
+
first[n] = x
|
|
732
|
+
last[n] = x
|
|
733
|
+
} else if (x < first[n]) {
|
|
734
|
+
first[n] = x
|
|
735
|
+
} else if (x > last[n]) {
|
|
736
|
+
last[n] = x
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (top > 0) {
|
|
741
|
+
out.put(SIXEL_NEXT_LINE)
|
|
742
|
+
}
|
|
743
|
+
let firstColour: boolean = true
|
|
744
|
+
for (let n: i32 = 0; n < toI32(first.length) && n < toI32(last.length); n++) {
|
|
745
|
+
// Read once: the calls below drop what proves `n` in range.
|
|
746
|
+
const start: i32 = first[n]
|
|
747
|
+
const end: i32 = last[n]
|
|
748
|
+
if (start < 0) {
|
|
749
|
+
continue
|
|
750
|
+
}
|
|
751
|
+
if (!firstColour) {
|
|
752
|
+
out.put(SIXEL_RETURN)
|
|
753
|
+
}
|
|
754
|
+
firstColour = false
|
|
755
|
+
out.put(SIXEL_COLOUR)
|
|
756
|
+
out.putDecimal(n)
|
|
757
|
+
// Blank columns before the colour's first one, then its runs to its last.
|
|
758
|
+
pixelsSixelRun(out, 0, start)
|
|
759
|
+
let runBits: i32 = -1
|
|
760
|
+
let runCount: i32 = 0
|
|
761
|
+
const row: i32 = n * w
|
|
762
|
+
for (let x: i32 = start; x <= end; x++) {
|
|
763
|
+
const sixel: i32 = bits[row + x]
|
|
764
|
+
bits[row + x] = 0
|
|
765
|
+
if (sixel === runBits) {
|
|
766
|
+
runCount = runCount + 1
|
|
767
|
+
} else {
|
|
768
|
+
if (runCount > 0) {
|
|
769
|
+
pixelsSixelRun(out, runBits, runCount)
|
|
770
|
+
}
|
|
771
|
+
runBits = sixel
|
|
772
|
+
runCount = 1
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
pixelsSixelRun(out, runBits, runCount)
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
pixelsStringEnd(out)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Draw `fb` onto `canvas`'s pixels over `rect` (from `fitFramebuffer` with
|
|
783
|
+
* `GRAPHICS_HALFBLOCK`): two pixels a cell, the upper and lower half block.
|
|
784
|
+
* The image's own pixels are set; a rect row's lower half that the image does
|
|
785
|
+
* not reach is set to `COLOR_DEFAULT`. Each covered cell is repainted once,
|
|
786
|
+
* and `Screen.flush` sends only the cells whose glyph or colours changed, so
|
|
787
|
+
* a frame that barely moves costs almost nothing.
|
|
788
|
+
*/
|
|
789
|
+
export const drawHalfBlocks = (fb: Framebuffer, canvas: Canvas, rect: PixelRect): void => {
|
|
790
|
+
if (rect.width <= 0 || rect.height <= 0 || fb.width <= 0 || fb.height <= 0) {
|
|
791
|
+
return
|
|
792
|
+
}
|
|
793
|
+
const xs: i32[] = pixelsSampleMap(rect.width, fb.width)
|
|
794
|
+
const ys: i32[] = pixelsSampleMap(rect.height, fb.height)
|
|
795
|
+
const left: i32 = rect.col
|
|
796
|
+
const top: i32 = rect.row * 2
|
|
797
|
+
for (let y: i32 = 0; y < rect.rows * 2; y++) {
|
|
798
|
+
const py: i32 = top + y
|
|
799
|
+
if (py < 0 || py >= canvas.height) {
|
|
800
|
+
continue
|
|
801
|
+
}
|
|
802
|
+
// `xs` is `rect.width` long and `ys` `rect.height`; their lengths bound the
|
|
803
|
+
// reads so the prover sees them in range.
|
|
804
|
+
for (let x: i32 = 0; x < toI32(xs.length); x++) {
|
|
805
|
+
const px: i32 = left + x
|
|
806
|
+
if (px < 0 || px >= canvas.width) {
|
|
807
|
+
continue
|
|
808
|
+
}
|
|
809
|
+
canvas.pixels[py * canvas.width + px] =
|
|
810
|
+
y < toI32(ys.length) ? fb.pixels[ys[y] * fb.width + xs[x]] : COLOR_DEFAULT
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
for (let row: i32 = rect.row; row < rect.row + rect.rows; row++) {
|
|
814
|
+
for (let col: i32 = left; col < left + rect.width; col++) {
|
|
815
|
+
if (row >= 0 && col >= 0 && row * 2 < canvas.height && col < canvas.width) {
|
|
816
|
+
canvas.paintCell(col, row)
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/** The one of xterm's 16 standard colours nearest `colour` in RGB; a tie goes to the lower number. */
|
|
823
|
+
const pixelsNearestAnsi16 = (colour: i32): i32 => {
|
|
824
|
+
const r: i32 = (colour >> 16) & 255
|
|
825
|
+
const g: i32 = (colour >> 8) & 255
|
|
826
|
+
const b: i32 = colour & 255
|
|
827
|
+
let best: i32 = 0
|
|
828
|
+
let bestDistance: i32 = 2147483647
|
|
829
|
+
for (let i: i32 = 0; i < 16; i++) {
|
|
830
|
+
const c: i32 = pixelsHex(PIXELS_ANSI16, i * 6, 6)
|
|
831
|
+
const dr: i32 = r - ((c >> 16) & 255)
|
|
832
|
+
const dg: i32 = g - ((c >> 8) & 255)
|
|
833
|
+
const db: i32 = b - (c & 255)
|
|
834
|
+
const d: i32 = dr * dr + dg * dg + db * db
|
|
835
|
+
if (d < bestDistance) {
|
|
836
|
+
best = c
|
|
837
|
+
bestDistance = d
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return best
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Draw `fb` onto `screen`'s cells over `rect` (from `fitFramebuffer` with
|
|
845
|
+
* `GRAPHICS_ASCII`), one character a cell for terminals with no graphics: the
|
|
846
|
+
* framebuffer pixel under the cell's centre picks a character from the ramp
|
|
847
|
+
* ` .:-=+*#%@` by its luminance (Rec. 601 weights, darkest first) and is the
|
|
848
|
+
* character's colour, on the terminal's own background.
|
|
849
|
+
*
|
|
850
|
+
* When `colors` is 16 — the Linux console — the colour is first moved to the
|
|
851
|
+
* nearest of xterm's 16 standard colours. `Screen` sends colours as 24-bit or
|
|
852
|
+
* 256-palette SGR only, and a console that reduces those to its 16 itself then
|
|
853
|
+
* lands on the colour chosen here, not on a neighbour of it.
|
|
854
|
+
*/
|
|
855
|
+
export const drawAscii = (fb: Framebuffer, screen: Screen, rect: PixelRect, colors: i32): void => {
|
|
856
|
+
if (rect.width <= 0 || rect.height <= 0 || fb.width <= 0 || fb.height <= 0) {
|
|
857
|
+
return
|
|
858
|
+
}
|
|
859
|
+
const xs: i32[] = pixelsSampleMap(rect.width, fb.width)
|
|
860
|
+
const ys: i32[] = pixelsSampleMap(rect.height, fb.height)
|
|
861
|
+
const ramp: i32 = toI32(PIXELS_RAMP.length)
|
|
862
|
+
// `ys` is `rect.height` long and `xs` `rect.width`; bounding the loops by
|
|
863
|
+
// their lengths proves the reads in range.
|
|
864
|
+
for (let y: i32 = 0; y < toI32(ys.length); y++) {
|
|
865
|
+
const from: i32 = ys[y] * fb.width
|
|
866
|
+
for (let x: i32 = 0; x < toI32(xs.length); x++) {
|
|
867
|
+
const c: i32 = fb.pixels[from + xs[x]]
|
|
868
|
+
const luma: i32 = ((((c >> 16) & 255) * 299 + ((c >> 8) & 255) * 587 + (c & 255) * 114) / 1000) | 0
|
|
869
|
+
const glyph: i32 = toI32(PIXELS_RAMP.charCodeAt(((luma * ramp) / 256) | 0))
|
|
870
|
+
const fg: i32 = colors === 16 ? pixelsNearestAnsi16(c) : c
|
|
871
|
+
screen.set(rect.col + x, rect.row + y, glyph, fg, COLOR_DEFAULT, ATTR_NONE)
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|