@amritk/nish-x86_64-linux 0.18.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.
Files changed (104) hide show
  1. package/INSTALL.md +58 -28
  2. package/bin/nish +0 -0
  3. package/docs/AI.md +2759 -0
  4. package/package.json +3 -2
  5. package/runtime/browser.d.ts +14 -0
  6. package/runtime/browser.mjs +88 -0
  7. package/runtime/core/arrays.d.ts +18 -0
  8. package/runtime/core/arrays.mjs +125 -0
  9. package/runtime/core/checks.mjs +20 -0
  10. package/runtime/core/host.d.ts +70 -0
  11. package/runtime/core/host.mjs +311 -0
  12. package/runtime/core/numbers.d.ts +53 -0
  13. package/runtime/core/numbers.mjs +316 -0
  14. package/runtime/core/result.d.ts +41 -0
  15. package/runtime/core/result.mjs +127 -0
  16. package/runtime/core/strings.d.ts +14 -0
  17. package/runtime/core/strings.mjs +169 -0
  18. package/runtime/core.d.ts +9 -0
  19. package/runtime/core.mjs +55 -0
  20. package/runtime/host.d.ts +6 -0
  21. package/runtime/nish.d.ts +99 -5
  22. package/runtime/nish.h +289 -19
  23. package/runtime/nish.mjs +39 -3
  24. package/runtime/node.mjs +383 -0
  25. package/runtime/runtime-aes.c +490 -0
  26. package/runtime/runtime-bytes.c +401 -0
  27. package/runtime/runtime-cross.c +366 -0
  28. package/runtime/runtime-host.c +8 -7
  29. package/runtime/runtime-net.c +93 -34
  30. package/runtime/runtime-os.c +214 -16
  31. package/runtime/runtime-parallel.c +590 -107
  32. package/runtime/runtime-simd.c +235 -11
  33. package/runtime/runtime-term.c +504 -0
  34. package/runtime/runtime-wasm.c +5 -14
  35. package/runtime/runtime.c +257 -17
  36. package/runtime/shim.mjs +6 -915
  37. package/runtime/sqlite/LICENSE.md +85 -0
  38. package/runtime/sqlite/README.md +45 -0
  39. package/runtime/sqlite/nish-sqlite.c +359 -0
  40. package/runtime/sqlite/sqlite3.c +271131 -0
  41. package/runtime/sqlite/sqlite3.h +14447 -0
  42. package/scripts/build.sh +437 -28
  43. package/std/LICENSE-fast-float +23 -0
  44. package/std/LICENSE-fdlibm +8 -0
  45. package/std/README.md +108 -58
  46. package/std/base64.ts +116 -0
  47. package/std/bytes.ts +419 -0
  48. package/std/cli.ts +328 -0
  49. package/std/collections.ts +6 -5
  50. package/std/crypto/LICENSE-crypt_blowfish +26 -0
  51. package/std/crypto/LICENSE-libsecp256k1 +19 -0
  52. package/std/crypto/aes.ts +238 -76
  53. package/std/crypto/base64url.ts +125 -0
  54. package/std/crypto/bcrypt.ts +760 -0
  55. package/std/crypto/chacha20poly1305.ts +43 -9
  56. package/std/crypto/ed25519.ts +840 -0
  57. package/std/crypto/hkdf.ts +304 -12
  58. package/std/crypto/hmac.ts +382 -54
  59. package/std/crypto/p256.ts +932 -61
  60. package/std/crypto/x25519.ts +55 -37
  61. package/std/crypto/x509-random.ts +71 -0
  62. package/std/crypto/x509.ts +20 -131
  63. package/std/json-tokenizer.ts +4237 -0
  64. package/std/json-typed.ts +1014 -0
  65. package/std/json-value.ts +1182 -0
  66. package/std/json.ts +1202 -34
  67. package/std/math.ts +608 -0
  68. package/std/net/buffer-pool.ts +151 -0
  69. package/std/net/hpack.ts +416 -112
  70. package/std/net/http-fields.ts +93 -0
  71. package/std/net/http1-server.ts +501 -62
  72. package/std/net/http1.ts +31 -6
  73. package/std/net/http2-tls.ts +112 -24
  74. package/std/net/http2.ts +333 -75
  75. package/std/net/http3-server.ts +261 -43
  76. package/std/net/http3.ts +187 -28
  77. package/std/net/postgres.ts +1875 -0
  78. package/std/net/prefork.ts +412 -0
  79. package/std/net/quic-listener.ts +108 -11
  80. package/std/net/quic-packet.ts +243 -82
  81. package/std/net/quic-recovery.ts +361 -30
  82. package/std/net/quic-stream.ts +424 -24
  83. package/std/net/quic.ts +519 -167
  84. package/std/net/router.ts +676 -0
  85. package/std/net/tls/codec.ts +426 -27
  86. package/std/net/tls/record-server.ts +478 -132
  87. package/std/net/tls/record.ts +133 -42
  88. package/std/net/tls/schedule.ts +464 -55
  89. package/std/net/tls/ticket.ts +610 -0
  90. package/std/net/tls-tcp.ts +196 -53
  91. package/std/net/tls.ts +1018 -260
  92. package/std/net/url.ts +571 -0
  93. package/std/random.ts +94 -0
  94. package/std/regex.ts +4274 -0
  95. package/std/sqlite.ts +391 -0
  96. package/std/term-io.ts +978 -0
  97. package/std/term-pixels.ts +874 -0
  98. package/std/term-probe.ts +459 -0
  99. package/std/term-rate.ts +993 -0
  100. package/std/term.ts +2453 -0
  101. package/std/text.ts +90 -7
  102. package/std/threads.ts +438 -14
  103. package/std/yaml-lines.ts +141 -0
  104. package/std/yaml.ts +4675 -0
package/std/term-io.ts ADDED
@@ -0,0 +1,978 @@
1
+ /**
2
+ * `std/term-io` — opening a terminal, drawing frames, reading events and
3
+ * leaving it as it was: the one entry point that uses the other term modules
4
+ * and does the I/O.
5
+ *
6
+ * ```ts
7
+ * import { defaultTerminalOptions, runTerminal, TermInput, Frame } from "nish/term-io";
8
+ * ```
9
+ *
10
+ * - `runTerminal(options, state, update, render)` is the whole program loop:
11
+ * it opens the terminal, runs `update(state, input)` at
12
+ * `options.stepsPerSecond` fixed steps a second, calls `render(state, frame)`
13
+ * whenever the refresh controller (`nish/term-rate`) lets a frame go out,
14
+ * presents it and closes the terminal when `update` answers false or the
15
+ * user quits (Ctrl-C under the kitty keyboard protocol; a plain Ctrl-C is
16
+ * SIGINT, after which the runtime writes the restore string). It answers
17
+ * `Err` with one sentence when the terminal cannot be opened or stdin ends.
18
+ * - `openTerminal(options)` answers a `Terminal`, for a program that runs its
19
+ * own loop with `readEvents`, `present`, `stats` and `close`.
20
+ *
21
+ * Opening refuses a pipe on stdin or stdout, enters raw mode and the alternate
22
+ * screen, hides the cursor, probes the terminal (`nish/term-probe`, at most
23
+ * `TERM_PROBE_TIMEOUT_MS`) and turns on what the options ask for and the
24
+ * terminal has: the kitty keyboard protocol (`CSI > 11 u`: key releases and
25
+ * Ctrl-C as a key), mouse reports (1000, 1002 or 1003, in SGR form 1006, with
26
+ * pixel coordinates 1016 when the terminal has them), bracketed paste (2004)
27
+ * and focus reports (1004). Before each change reaches the terminal, the
28
+ * runtime's restore string (`setTerminalRestore`) is set to undo everything —
29
+ * cancel any half-written escape (CAN), end synchronized output,
30
+ * delete the kitty image, pop the kitty keyboard flags, turn mouse, paste and
31
+ * focus reports off, reset the style, show the cursor and leave the alternate
32
+ * screen — so a panic or a signal leaves the terminal as it was.
33
+ *
34
+ * `present()` draws the `Screen` and the `Framebuffer`, if the options gave
35
+ * one, with the best backend the terminal has capped by the controller
36
+ * (`min(caps.graphics, decision.backendCap)`), at the controller's resolution
37
+ * divisor and palette, wrapped in synchronized output when the terminal has
38
+ * it, as one write. It follows the frame with a DSR 5 query (`CSI 5 n`),
39
+ * whose answer (`CSI 0 n`) acknowledges the frame to the controller and marks
40
+ * the input before it as delivered to `KeyState`. No answer from the terminal
41
+ * reaches the program: one that arrives after the probe gave up (a slow link)
42
+ * is taken, and a cell size among them is learned. `close` reads the
43
+ * acknowledgements still in flight before it leaves raw mode, so none is
44
+ * echoed onto the shell's line.
45
+ *
46
+ * A frame's cost to the controller is the program's `update` and `render`
47
+ * since the last frame plus encoding it, not the write: a write that blocks
48
+ * on a slow link is the link's time, which the acknowledgements measure.
49
+ *
50
+ * Steady state allocates nothing per frame that outlives it: the frame's
51
+ * strings live in `Terminal.send`'s scope, the pixel encoders write into one
52
+ * `PixelBytes`, and a read that holds only frame acknowledgements is counted
53
+ * without being decoded.
54
+ *
55
+ * Private helpers share the importing program's flat symbol namespace
56
+ * (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
57
+ */
58
+ import { isTerminal, readStdin, setRawMode, setTerminalRestore, terminalSize, waitStdin } from "nish:term"
59
+ import { monotonicNanos } from "nish:process"
60
+ import {
61
+ EVENT_QUIT,
62
+ EVENT_REPLY,
63
+ EVENT_RESIZE,
64
+ Canvas,
65
+ FixedStep,
66
+ KeyDecoder,
67
+ KeyState,
68
+ Screen,
69
+ TermEvent,
70
+ beginSync,
71
+ endSync,
72
+ enterAltScreen,
73
+ hideCursor,
74
+ leaveAltScreen,
75
+ resetStyle,
76
+ showCursor,
77
+ } from "nish/term"
78
+ import { TermCaps, TermEnv, TermProbe, probeQueries } from "nish/term-probe"
79
+ import { RATE_FIRST, RateController, RateOptions, TermStats, drawStatsOverlay } from "nish/term-rate"
80
+ import {
81
+ GRAPHICS_ASCII,
82
+ GRAPHICS_HALFBLOCK,
83
+ GRAPHICS_KITTY,
84
+ GRAPHICS_SIXEL,
85
+ Framebuffer,
86
+ PixelBytes,
87
+ PixelRect,
88
+ drawAscii,
89
+ drawHalfBlocks,
90
+ fitFramebuffer,
91
+ kittyDelete,
92
+ kittyFrame,
93
+ scaleFramebuffer,
94
+ sixelFrame,
95
+ } from "nish/term-pixels"
96
+
97
+ /** The kitty image id a `Terminal` draws its framebuffer as. */
98
+ const TERM_IO_IMAGE_ID: i32 = 1
99
+
100
+ /** How long the session decoder holds an unfinished escape sequence, in milliseconds. */
101
+ const TERM_IO_ESCAPE_MS: f64 = 40.0
102
+
103
+ /** How long after the last acknowledgement arrived `KeyState` stops measuring silence against it, in milliseconds. */
104
+ const TERM_IO_WATERMARK_MS: f64 = 1000.0
105
+
106
+ /** The frame acknowledgements a `Terminal` remembers the send times of. */
107
+ const TERM_IO_ACKS: i32 = 64
108
+
109
+ /** The least and most `close` waits for the acknowledgements still in flight, in milliseconds. */
110
+ const TERM_IO_DRAIN_MIN_MS: f64 = 50.0
111
+ const TERM_IO_DRAIN_MAX_MS: f64 = 500.0
112
+
113
+ /** The bytes one read takes. */
114
+ const TERM_IO_READ: i32 = 4096
115
+
116
+ /** The size assumed when the terminal will not say. */
117
+ const TERM_IO_COLS: i32 = 80
118
+ const TERM_IO_ROWS: i32 = 24
119
+
120
+ /** What `openTerminal` and `runTerminal` turn on, and how the frame loop runs. */
121
+ export class TerminalOptions {
122
+ /** Fixed `update` steps a second (60). */
123
+ stepsPerSecond: i32
124
+ /** The least frame rate the controller keeps before it cuts detail (10). */
125
+ minFps: i32
126
+ /** The most frames a second (60). */
127
+ maxFps: i32
128
+ /** A fixed frame rate, or 0 to let the controller choose (0). */
129
+ fixedFps: i32
130
+ /** Whether 120 frames a second may be chosen (false). */
131
+ allow120: boolean
132
+ /** `RATE_FIRST` or `DETAIL_FIRST`, from `nish/term-rate` (`RATE_FIRST`). */
133
+ prefer: i32
134
+ /** Mouse reports: 0 (off), 1000 (clicks), 1002 (drags) or 1003 (all motion) (0). */
135
+ mouse: i32
136
+ /** Bracketed paste, so a paste is one `EVENT_PASTE` (true). */
137
+ paste: boolean
138
+ /** Focus reports, `EVENT_FOCUS` (false). */
139
+ focus: boolean
140
+ /** The kitty keyboard protocol where the terminal has it: real releases, Ctrl-C as a key (true). */
141
+ kittyKeyboard: boolean
142
+ /** Whether Ctrl-C, when it arrives as a key, quits (true). */
143
+ quitOnCtrlC: boolean
144
+ /** How long after a held key's last repeat it counts as released, without the kitty protocol (100). */
145
+ releaseGapMs: f64
146
+ /** The framebuffer's size in pixels, or 0 by 0 for none (0 by 0). */
147
+ framebufferWidth: i32
148
+ framebufferHeight: i32
149
+ /** The corner (`CORNER_*` from `nish/term-rate`) to draw the stats overlay in, or -1 for none (-1). */
150
+ overlay: i32
151
+
152
+ constructor() {
153
+ this.stepsPerSecond = 60
154
+ this.minFps = 10
155
+ this.maxFps = 60
156
+ this.fixedFps = 0
157
+ this.allow120 = false
158
+ this.prefer = RATE_FIRST
159
+ this.mouse = 0
160
+ this.paste = true
161
+ this.focus = false
162
+ this.kittyKeyboard = true
163
+ this.quitOnCtrlC = true
164
+ this.releaseGapMs = 100.0
165
+ this.framebufferWidth = 0
166
+ this.framebufferHeight = 0
167
+ this.overlay = -1
168
+ }
169
+ }
170
+
171
+ /** The options every field's comment gives the default of. */
172
+ export const defaultTerminalOptions = (): TerminalOptions => new TerminalOptions()
173
+
174
+ /** The monotonic clock in milliseconds. */
175
+ const termIoNowMs = (): f64 => toF64(monotonicNanos()) / 1000000.0
176
+
177
+ /** The environment variable `name`, or "" when it is unset. */
178
+ const termIoGetenv = (name: string): string => {
179
+ const v: string | null = getenv(name)
180
+ return v === null ? "" : v
181
+ }
182
+
183
+ /** The variables `nish/term-probe` reads. */
184
+ const termIoEnv = (): TermEnv => {
185
+ const env: TermEnv = new TermEnv()
186
+ env.term = termIoGetenv("TERM")
187
+ env.colorterm = termIoGetenv("COLORTERM")
188
+ env.termProgram = termIoGetenv("TERM_PROGRAM")
189
+ env.tmux = termIoGetenv("TMUX")
190
+ env.sty = termIoGetenv("STY")
191
+ env.sshConnection = termIoGetenv("SSH_CONNECTION")
192
+ env.sshClient = termIoGetenv("SSH_CLIENT")
193
+ env.sshTty = termIoGetenv("SSH_TTY")
194
+ env.noColor = termIoGetenv("NO_COLOR")
195
+ env.nishTermGraphics = termIoGetenv("NISH_TERM_GRAPHICS")
196
+ env.nishTermFps = termIoGetenv("NISH_TERM_FPS")
197
+ return env
198
+ }
199
+
200
+ /** The `GRAPHICS_*` a `TermCaps.graphics` name stands for. */
201
+ const termIoGraphics = (name: string): i32 => {
202
+ if (name === "kitty") {
203
+ return GRAPHICS_KITTY
204
+ }
205
+ if (name === "sixel") {
206
+ return GRAPHICS_SIXEL
207
+ }
208
+ return name === "halfblock" ? GRAPHICS_HALFBLOCK : GRAPHICS_ASCII
209
+ }
210
+
211
+ /** A probed cell size clamped to 1–256 pixels, or 0 when the probe did not learn it. */
212
+ const termIoCellPx = (px: i32): i32 => {
213
+ if (px <= 0) {
214
+ return 0
215
+ }
216
+ return px > 256 ? 256 : px
217
+ }
218
+
219
+ /** Whether `e` is the answer to DSR 5, `CSI 0 n`: a frame acknowledgement. */
220
+ const termIoIsAck = (e: TermEvent): boolean =>
221
+ e.type === EVENT_REPLY && e.introducer === 91 && e.prefix === 0 && e.intermediate === 0 && e.reply === 110
222
+
223
+ /**
224
+ * How many `CSI 0 n` make up `buf[0, n)` exactly, or -1 when it holds
225
+ * anything else: the read a frame loop gets most often, counted without
226
+ * decoding it.
227
+ */
228
+ const termIoOnlyAcks = (buf: u8[], n: i32): i32 => {
229
+ if (n <= 0 || n % 4 !== 0) {
230
+ return -1
231
+ }
232
+ if (n > toI32(buf.length)) {
233
+ return -1
234
+ }
235
+ for (let i: i32 = 0; i >= 0 && i + 3 < toI32(buf.length) && i < n; i = i + 4) {
236
+ if (buf[i] !== 27 || buf[i + 1] !== 91 || buf[i + 2] !== 48 || buf[i + 3] !== 110) {
237
+ return -1
238
+ }
239
+ }
240
+ return (n / 4) | 0
241
+ }
242
+
243
+ /** Parameter `i` of a reply, or -1 when it has fewer. */
244
+ const termIoParam = (e: TermEvent, i: i32): i32 => (i < toI32(e.params.length) ? e.params[i] : -1)
245
+
246
+ /**
247
+ * An open terminal: what it can do, the `Screen` and `Framebuffer` a program
248
+ * draws into, and the state that reads its events and paces its frames.
249
+ * Made by `openTerminal`.
250
+ */
251
+ export class Terminal {
252
+ options: TerminalOptions
253
+ caps: TermCaps
254
+ /** The cell grid the program draws into, the terminal's size. */
255
+ screen: Screen
256
+ canvas: Canvas
257
+ /** The pixels the program draws into, `options.framebufferWidth × framebufferHeight`. */
258
+ framebuffer: Framebuffer
259
+ /** The framebuffer shrunk by the controller's resolution divisor. */
260
+ scaled: Framebuffer
261
+ pixels: PixelBytes
262
+ decoder: KeyDecoder
263
+ keys: KeyState
264
+ rate: RateController
265
+ /** The events read during the probe, handed to the program first. */
266
+ held: TermEvent[]
267
+ /** The send times of the frames not yet acknowledged, a ring of `TERM_IO_ACKS`. */
268
+ ackSent: f64[]
269
+ ackHead: i32
270
+ ackCount: i32
271
+ readBuf: u8[]
272
+ /** Where the framebuffer is drawn, for `rectCols × rectRows` cells and `rectBackend`. */
273
+ rect: PixelRect
274
+ /** When the last frame acknowledgement arrived, or -1. */
275
+ lastAckMs: f64
276
+ /** The last acknowledged frame's own round trip, in milliseconds, which the controller's smoothed one trails. */
277
+ lastRttMs: f64
278
+ /** The program's work toward the next frame (its `update` and `render`), in nanoseconds, which `present` adds to the frame's cost. */
279
+ workNs: f64
280
+ /** When the last frame's write began, on the monotonic clock. */
281
+ sendStartNs: i64
282
+ /** The cells `rect` was fitted to. */
283
+ rectCols: i32
284
+ rectRows: i32
285
+ rectBackend: i32
286
+ /** The best backend the terminal has, a `GRAPHICS_*`. */
287
+ graphics: i32
288
+ /** The backend the last frame used. */
289
+ backend: i32
290
+ cellWidthPx: i32
291
+ cellHeightPx: i32
292
+ /** The size `terminalSize` last answered. */
293
+ sizeCols: i32
294
+ sizeRows: i32
295
+ /** The size last given to the `Screen`, before it clamped it, which `checkResize` compares. */
296
+ shownCols: i32
297
+ shownRows: i32
298
+ /** The mouse mode turned on, or 0. */
299
+ mouseMode: i32
300
+ open: boolean
301
+ kittyKeyboardOn: boolean
302
+ pixelMouseOn: boolean
303
+ pasteOn: boolean
304
+ focusOn: boolean
305
+ /** Whether a kitty image may be on the screen, so closing deletes it. */
306
+ kittyImage: boolean
307
+ /** Whether `screen` was drawn in 256 colours last frame. */
308
+ palette256: boolean
309
+
310
+ constructor(options: TerminalOptions, caps: TermCaps, cols: i32, rows: i32, held: TermEvent[]) {
311
+ this.options = options
312
+ this.caps = caps
313
+ this.palette256 = !caps.trueColor
314
+ this.screen = new Screen(cols, rows, this.palette256)
315
+ this.screen.sync = false
316
+ this.canvas = new Canvas(this.screen)
317
+ const fbW: i32 =
318
+ options.framebufferWidth > 0 && options.framebufferHeight > 0 ? options.framebufferWidth : 0
319
+ const fbH: i32 = fbW > 0 ? options.framebufferHeight : 0
320
+ this.framebuffer = new Framebuffer(fbW, fbH)
321
+ this.scaled = new Framebuffer(0, 0)
322
+ this.pixels = new PixelBytes(4096)
323
+ this.decoder = new KeyDecoder(TERM_IO_ESCAPE_MS)
324
+ this.keys = new KeyState()
325
+ this.keys.quitOnCtrlC = options.quitOnCtrlC
326
+ this.keys.releaseGapMs = options.releaseGapMs
327
+ const rateOptions: RateOptions = new RateOptions()
328
+ rateOptions.probeRttMs = caps.probeRttMs > 0.0 ? caps.probeRttMs : 0.0
329
+ rateOptions.minFps = options.minFps
330
+ rateOptions.maxFps = options.maxFps
331
+ rateOptions.fixedFps = options.fixedFps
332
+ rateOptions.prefer = options.prefer
333
+ rateOptions.allow120 = options.allow120
334
+ rateOptions.remoteHint = caps.remoteHint
335
+ rateOptions.envFps = parseInt(termIoGetenv("NISH_TERM_FPS"))
336
+ this.rate = new RateController(rateOptions)
337
+ // The probe carries `NISH_TERM_FPS` without reading it; the controller applies it, so say so here.
338
+ if (rateOptions.envFps > 0) {
339
+ const forced: string = `NISH_TERM_FPS=${termIoGetenv("NISH_TERM_FPS")}: ${this.rate.fixedFps} frames a second`
340
+ caps.overrides = caps.overrides === "" ? forced : `${caps.overrides}; ${forced}`
341
+ }
342
+ this.held = held
343
+ this.ackSent = new Array<f64>(TERM_IO_ACKS)
344
+ this.ackSent.fill(0.0)
345
+ this.ackHead = 0
346
+ this.ackCount = 0
347
+ this.readBuf = new Array<u8>(TERM_IO_READ)
348
+ this.readBuf.fill(0)
349
+ this.rect = new PixelRect(0, 0, 0, 0, 0, 0, 0.0)
350
+ this.rectCols = -1
351
+ this.rectRows = -1
352
+ this.rectBackend = -1
353
+ this.graphics = termIoGraphics(caps.graphics)
354
+ this.backend = this.graphics
355
+ this.cellWidthPx = termIoCellPx(caps.cellWidthPx)
356
+ this.cellHeightPx = termIoCellPx(caps.cellHeightPx)
357
+ this.sizeCols = cols
358
+ this.sizeRows = rows
359
+ this.shownCols = cols
360
+ this.shownRows = rows
361
+ this.lastAckMs = -1.0
362
+ this.lastRttMs = 0.0
363
+ this.workNs = 0.0
364
+ this.sendStartNs = 0
365
+ this.mouseMode = 0
366
+ this.open = true
367
+ this.kittyKeyboardOn = false
368
+ this.pixelMouseOn = false
369
+ this.pasteOn = false
370
+ this.focusOn = false
371
+ this.kittyImage = false
372
+ }
373
+
374
+ /** The bytes that undo everything turned on: what `close` writes and the runtime writes on a panic or signal. */
375
+ restoreString(): string {
376
+ let s: string = "\x18\x1b[?2026l"
377
+ if (this.kittyImage) {
378
+ s = s + kittyDelete(TERM_IO_IMAGE_ID)
379
+ }
380
+ if (this.kittyKeyboardOn) {
381
+ s = s + "\x1b[<u"
382
+ }
383
+ if (this.mouseMode > 0) {
384
+ s = s + (this.pixelMouseOn ? "\x1b[?1016l" : "") + `\x1b[?1006l\x1b[?${this.mouseMode}l`
385
+ }
386
+ if (this.pasteOn) {
387
+ s = s + "\x1b[?2004l"
388
+ }
389
+ if (this.focusOn) {
390
+ s = s + "\x1b[?1004l"
391
+ }
392
+ return s + resetStyle() + showCursor() + leaveAltScreen()
393
+ }
394
+
395
+ /** Set the runtime's restore string to `restoreString()`, then write `bytes`, which turn something on. */
396
+ turnOn(bytes: string): void {
397
+ setTerminalRestore(this.restoreString())
398
+ write(bytes)
399
+ }
400
+
401
+ /** Turn on what the options ask for and the terminal has, the restore string kept ahead of each. */
402
+ enable(): void {
403
+ const o: TerminalOptions = this.options
404
+ if (o.kittyKeyboard && this.caps.kittyKeyboard) {
405
+ this.kittyKeyboardOn = true
406
+ this.keys.releasesAreSynthetic = false
407
+ this.turnOn("\x1b[>11u")
408
+ }
409
+ if (o.mouse === 1000 || o.mouse === 1002 || o.mouse === 1003) {
410
+ this.mouseMode = o.mouse
411
+ this.pixelMouseOn = this.caps.pixelMouse && this.cellWidthPx > 0 && this.cellHeightPx > 0
412
+ this.decoder.pixelMouse = this.pixelMouseOn
413
+ this.decoder.cellWidthPx = this.cellWidthPx
414
+ this.decoder.cellHeightPx = this.cellHeightPx
415
+ this.turnOn(`\x1b[?${o.mouse}h\x1b[?1006h` + (this.pixelMouseOn ? "\x1b[?1016h" : ""))
416
+ }
417
+ if (o.paste) {
418
+ this.pasteOn = true
419
+ this.turnOn("\x1b[?2004h")
420
+ }
421
+ if (o.focus) {
422
+ this.focusOn = true
423
+ this.turnOn("\x1b[?1004h")
424
+ }
425
+ if (this.graphics === GRAPHICS_KITTY && this.framebuffer.width > 0) {
426
+ // Before the first frame, so a panic mid-image deletes it.
427
+ this.kittyImage = true
428
+ setTerminalRestore(this.restoreString())
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Undo everything `openTerminal` did and leave raw mode. The
434
+ * acknowledgements still in flight are read first, for twice the round trip
435
+ * (the controller's or the last frame's, whichever is longer; 50 to
436
+ * 500 ms), so the terminal's answers are not echoed after the program. A
437
+ * second call does nothing.
438
+ */
439
+ close(): void {
440
+ if (!this.open) {
441
+ return
442
+ }
443
+ this.open = false
444
+ write(this.restoreString())
445
+ this.drainAcks()
446
+ setTerminalRestore("")
447
+ setRawMode(false)
448
+ }
449
+
450
+ /**
451
+ * Read and drop input until every frame sent is answered, stdin ends or the
452
+ * round trip's bound passes. Before any answer the probe's round trip is
453
+ * the one known.
454
+ */
455
+ drainAcks(): void {
456
+ let owed: i32 = this.rate.unacked() + this.rate.stale
457
+ let rtt: f64 = this.rate.rttMs > this.lastRttMs ? this.rate.rttMs : this.lastRttMs
458
+ rtt = rtt > this.caps.probeRttMs ? rtt : this.caps.probeRttMs
459
+ let bound: f64 = 2.0 * rtt
460
+ bound = bound > TERM_IO_DRAIN_MIN_MS ? bound : TERM_IO_DRAIN_MIN_MS
461
+ bound = bound < TERM_IO_DRAIN_MAX_MS ? bound : TERM_IO_DRAIN_MAX_MS
462
+ const deadline: f64 = termIoNowMs() + bound
463
+ let ended: boolean = false
464
+ while (owed > 0 && !ended) {
465
+ const left: i32 = toI32(Math.ceil(deadline - termIoNowMs()))
466
+ if (left <= 0 || !waitStdin(left)) {
467
+ return
468
+ }
469
+ const n: i32 = readStdin(this.readBuf, 0, TERM_IO_READ)
470
+ if (n > 0) {
471
+ const only: i32 = this.decoder.waiting() ? -1 : termIoOnlyAcks(this.readBuf, n)
472
+ if (only > 0) {
473
+ owed = owed - only
474
+ } else {
475
+ for (const e of this.decoder.feedEvents(this.readBuf, n, termIoNowMs())) {
476
+ if (termIoIsAck(e)) {
477
+ owed = owed - 1
478
+ }
479
+ }
480
+ }
481
+ } else if (n !== -11) {
482
+ ended = true
483
+ }
484
+ }
485
+ }
486
+
487
+ /** What the controller measured, with the backend the last frame actually used. */
488
+ stats(): TermStats {
489
+ const s: TermStats = this.rate.stats()
490
+ s.backend = this.backend
491
+ return s
492
+ }
493
+
494
+ /** Read `terminalSize` into `sizeCols` and `sizeRows`; its array dies here. */
495
+ readSize(): void {
496
+ const size: i32[] = terminalSize()
497
+ if (toI32(size.length) >= 2 && size[0] > 0 && size[1] > 0) {
498
+ this.sizeCols = size[0]
499
+ this.sizeRows = size[1]
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Whether the terminal changed size since the last call; if it did, the
505
+ * `Screen` takes the new size and the framebuffer is fitted again. It
506
+ * compares the size the terminal answers, not the `Screen`'s, which clamps
507
+ * a huge one, so a huge terminal is not resized every frame.
508
+ */
509
+ checkResize(): boolean {
510
+ this.readSize()
511
+ if (this.sizeCols === this.shownCols && this.sizeRows === this.shownRows) {
512
+ return false
513
+ }
514
+ this.shownCols = this.sizeCols
515
+ this.shownRows = this.sizeRows
516
+ this.screen.resize(this.sizeCols, this.sizeRows)
517
+ this.canvas = new Canvas(this.screen)
518
+ this.screen.invalidate()
519
+ this.rectCols = -1
520
+ return true
521
+ }
522
+
523
+ /** Forget the send times of the frames the controller gave up on, oldest first, so the ring holds only frames it still waits for. */
524
+ dropLost(): void {
525
+ while (this.ackCount > this.rate.unacked()) {
526
+ this.ackHead = (this.ackHead + 1) % TERM_IO_ACKS
527
+ this.ackCount = this.ackCount - 1
528
+ }
529
+ }
530
+
531
+ /** Remember that a frame acknowledgement was asked for at `nowMs`. */
532
+ ackPush(nowMs: f64): void {
533
+ if (this.ackCount === TERM_IO_ACKS) {
534
+ this.ackHead = (this.ackHead + 1) % TERM_IO_ACKS
535
+ this.ackCount = this.ackCount - 1
536
+ }
537
+ this.ackSent[(this.ackHead + this.ackCount) % TERM_IO_ACKS] = nowMs
538
+ this.ackCount = this.ackCount + 1
539
+ }
540
+
541
+ /**
542
+ * `count` acknowledgements arrived at `nowMs`: each answers the controller
543
+ * and, unless it is the late answer of a frame the controller gave up on,
544
+ * marks its frame's send time delivered.
545
+ */
546
+ acked(count: i32, nowMs: f64): void {
547
+ for (let k: i32 = 0; k < count; k++) {
548
+ this.dropLost()
549
+ if (this.rate.stale === 0 && this.ackCount > 0) {
550
+ this.keys.delivered(this.ackSent[this.ackHead])
551
+ this.lastRttMs = nowMs - this.ackSent[this.ackHead]
552
+ this.ackHead = (this.ackHead + 1) % TERM_IO_ACKS
553
+ this.ackCount = this.ackCount - 1
554
+ }
555
+ this.rate.ackReceived(nowMs)
556
+ }
557
+ if (count > 0) {
558
+ this.lastAckMs = nowMs
559
+ }
560
+ }
561
+
562
+ /** A reply other than an acknowledgement, which came after the probe: learn the cell size from it if the probe did not. */
563
+ lateReply(e: TermEvent): void {
564
+ if (
565
+ this.cellWidthPx > 0 ||
566
+ e.introducer !== 91 ||
567
+ e.prefix !== 0 ||
568
+ e.intermediate !== 0 ||
569
+ e.reply !== 116
570
+ ) {
571
+ return
572
+ }
573
+ let w: i32 = 0
574
+ let h: i32 = 0
575
+ if (termIoParam(e, 0) === 6) {
576
+ h = termIoParam(e, 1)
577
+ w = termIoParam(e, 2)
578
+ } else if (termIoParam(e, 0) === 4 && this.sizeCols > 0 && this.sizeRows > 0) {
579
+ h = (termIoParam(e, 1) / this.sizeRows) | 0
580
+ w = (termIoParam(e, 2) / this.sizeCols) | 0
581
+ }
582
+ if (termIoCellPx(w) > 0 && termIoCellPx(h) > 0) {
583
+ this.cellWidthPx = termIoCellPx(w)
584
+ this.cellHeightPx = termIoCellPx(h)
585
+ this.caps.cellWidthPx = this.cellWidthPx
586
+ this.caps.cellHeightPx = this.cellHeightPx
587
+ this.decoder.cellWidthPx = this.cellWidthPx
588
+ this.decoder.cellHeightPx = this.cellHeightPx
589
+ this.rectCols = -1
590
+ // Pixel mouse waited on the cell size; the mouse is on, so add it.
591
+ if (this.mouseMode > 0 && this.caps.pixelMouse && !this.pixelMouseOn) {
592
+ this.pixelMouseOn = true
593
+ this.decoder.pixelMouse = true
594
+ this.turnOn("\x1b[?1016h")
595
+ }
596
+ }
597
+ }
598
+
599
+ /**
600
+ * Take decoded `events`, read at `nowMs`: the program's go through
601
+ * `KeyState` into `out`, then the acknowledgements count. Every reply is
602
+ * the terminal's and is taken here, a late cell size learned.
603
+ */
604
+ take(events: TermEvent[], nowMs: f64, out: TermEvent[]): void {
605
+ let replies: i32 = 0
606
+ for (const e of events) {
607
+ if (e.type === EVENT_REPLY) {
608
+ replies = replies + 1
609
+ }
610
+ }
611
+ let program: TermEvent[] = events
612
+ let acks: i32 = 0
613
+ if (replies > 0) {
614
+ program = []
615
+ for (const e of events) {
616
+ if (e.type !== EVENT_REPLY) {
617
+ program.push(e)
618
+ } else if (termIoIsAck(e)) {
619
+ acks = acks + 1
620
+ } else {
621
+ this.lateReply(e)
622
+ }
623
+ }
624
+ }
625
+ for (const e of this.keys.apply(program, nowMs)) {
626
+ out.push(e)
627
+ }
628
+ this.acked(acks, nowMs)
629
+ }
630
+
631
+ /**
632
+ * Wait up to `waitMs` milliseconds (a negative wait has no limit) for input,
633
+ * and append what it means for the program to `out`: keys, mouse, paste and
634
+ * focus events, the releases made up from silence, `EVENT_QUIT` for Ctrl-C.
635
+ * Frame acknowledgements are taken here and never appended. Answers false
636
+ * once stdin has ended.
637
+ */
638
+ readEvents(waitMs: i32, out: TermEvent[]): boolean {
639
+ if (toI32(this.held.length) > 0) {
640
+ const held: TermEvent[] = this.held
641
+ this.held = []
642
+ this.take(held, termIoNowMs(), out)
643
+ }
644
+ let ended: boolean = false
645
+ let decoded: boolean = false
646
+ if (waitStdin(waitMs)) {
647
+ const n: i32 = readStdin(this.readBuf, 0, TERM_IO_READ)
648
+ const nowMs: f64 = termIoNowMs()
649
+ const only: i32 = this.decoder.waiting() ? -1 : termIoOnlyAcks(this.readBuf, n)
650
+ if (only > 0) {
651
+ this.acked(only, nowMs)
652
+ } else if (n > 0) {
653
+ this.take(this.decoder.feedEvents(this.readBuf, n, nowMs), nowMs, out)
654
+ decoded = true
655
+ } else if (n !== -11) {
656
+ ended = true
657
+ }
658
+ }
659
+ const nowMs: f64 = termIoNowMs()
660
+ if (this.decoder.waiting()) {
661
+ const late: TermEvent[] = this.decoder.pollEvents(nowMs)
662
+ if (toI32(late.length) > 0) {
663
+ this.take(late, nowMs, out)
664
+ decoded = true
665
+ }
666
+ }
667
+ if (!decoded && toI32(this.keys.downKeys.length) > 0) {
668
+ for (const e of this.keys.tick(nowMs)) {
669
+ out.push(e)
670
+ }
671
+ }
672
+ if (this.keys.watermarked && nowMs - this.lastAckMs > TERM_IO_WATERMARK_MS) {
673
+ // Answers stopped (no frames lately): measure silence against the clock again.
674
+ this.keys.watermarked = false
675
+ }
676
+ return !ended
677
+ }
678
+
679
+ /** Fit the framebuffer to the screen for `backend`, unless the size and backend are those of the last fit. */
680
+ fit(backend: i32): void {
681
+ if (
682
+ this.rectCols === this.screen.width &&
683
+ this.rectRows === this.screen.height &&
684
+ this.rectBackend === backend
685
+ ) {
686
+ return
687
+ }
688
+ this.rectCols = this.screen.width
689
+ this.rectRows = this.screen.height
690
+ this.rectBackend = backend
691
+ const fb: Framebuffer = this.framebuffer
692
+ this.rect = fitFramebuffer(
693
+ fb.width,
694
+ fb.height,
695
+ this.screen.width,
696
+ this.screen.height,
697
+ this.cellWidthPx,
698
+ this.cellHeightPx,
699
+ backend
700
+ )
701
+ }
702
+
703
+ /**
704
+ * Write one frame as a single write: the screen's changes, then the pixels'
705
+ * bytes when `withPixels`, wrapped in synchronized output when the terminal
706
+ * has it, then the DSR 5 query that acknowledges it. Records when the write
707
+ * began in `sendStartNs` and answers the bytes written. Its strings die
708
+ * here, so a frame loop does not keep them.
709
+ */
710
+ send(withPixels: boolean): i32 {
711
+ const body: string = this.screen.flush()
712
+ const image: string = withPixels ? this.pixels.text() : ""
713
+ const frame: string = this.caps.sync
714
+ ? `${beginSync()}${body}${image}${endSync()}\x1b[5n`
715
+ : `${body}${image}\x1b[5n`
716
+ this.sendStartNs = monotonicNanos()
717
+ write(frame)
718
+ return toI32(frame.length)
719
+ }
720
+
721
+ /**
722
+ * Send a frame: the framebuffer, if any, drawn with the backend the
723
+ * terminal and the controller allow, the stats overlay if the options ask
724
+ * for it, and the screen's changes, as one write followed by the query that
725
+ * acknowledges it. Its cost to the controller is `workNs` and the encoding,
726
+ * and its send time is when the write began. A closed terminal is not
727
+ * written to.
728
+ */
729
+ present(): void {
730
+ if (!this.open) {
731
+ return
732
+ }
733
+ const start: i64 = monotonicNanos()
734
+ const d = this.rate.decision()
735
+ const palette: boolean = !this.caps.trueColor || d.palette256
736
+ if (palette !== this.palette256) {
737
+ this.palette256 = palette
738
+ this.screen.palette256 = palette
739
+ this.screen.invalidate()
740
+ }
741
+ const backend: i32 = Math.min(this.graphics, d.backendCap)
742
+ if (backend !== this.backend) {
743
+ if (this.backend === GRAPHICS_KITTY && this.kittyImage) {
744
+ write(kittyDelete(TERM_IO_IMAGE_ID))
745
+ }
746
+ this.screen.invalidate()
747
+ this.backend = backend
748
+ }
749
+ let withPixels: boolean = false
750
+ const fb: Framebuffer = this.framebuffer
751
+ if (fb.width > 0) {
752
+ this.fit(backend)
753
+ let src: Framebuffer = fb
754
+ if (d.resolutionDivisor > 1) {
755
+ scaleFramebuffer(fb, d.resolutionDivisor, this.scaled)
756
+ src = this.scaled
757
+ }
758
+ if (backend === GRAPHICS_KITTY) {
759
+ this.pixels.reset()
760
+ kittyFrame(src, this.rect, TERM_IO_IMAGE_ID, this.pixels)
761
+ withPixels = true
762
+ } else if (backend === GRAPHICS_SIXEL) {
763
+ this.pixels.reset()
764
+ sixelFrame(src, this.rect, true, this.pixels)
765
+ withPixels = true
766
+ } else if (backend === GRAPHICS_HALFBLOCK) {
767
+ drawHalfBlocks(src, this.canvas, this.rect)
768
+ } else {
769
+ drawAscii(src, this.screen, this.rect, this.caps.colors)
770
+ }
771
+ }
772
+ if (this.options.overlay >= 0) {
773
+ drawStatsOverlay(this.screen, this.stats(), this.options.overlay)
774
+ }
775
+ const bytes: i32 = this.send(withPixels)
776
+ const sentAtMs: f64 = toF64(this.sendStartNs) / 1000000.0
777
+ this.dropLost()
778
+ this.rate.frameSent(bytes, this.workNs + toF64(this.sendStartNs - start), sentAtMs)
779
+ this.workNs = 0.0
780
+ this.ackPush(sentAtMs)
781
+ }
782
+ }
783
+
784
+ /** Refuse, with one sentence naming the descriptor, unless stdin and stdout are both a terminal. */
785
+ const termIoRefusal = (): string => {
786
+ if (!isTerminal(0)) {
787
+ return "stdin is not a terminal (a pipe or CI?)"
788
+ }
789
+ return isTerminal(1) ? "" : "stdout is not a terminal (a pipe or a file?)"
790
+ }
791
+
792
+ /**
793
+ * Open the terminal: refuse a pipe, enter raw mode and the alternate screen,
794
+ * hide the cursor, probe what the terminal can do and turn on what `options`
795
+ * asks for and it has. The restore string undoes all of it should the
796
+ * program panic or be signalled before `close`.
797
+ */
798
+ export const openTerminal = (options: TerminalOptions): Result<Terminal, string> => {
799
+ const refused: string = termIoRefusal()
800
+ if (refused !== "") {
801
+ return Err(refused)
802
+ }
803
+ if (setRawMode(true) !== 0) {
804
+ return Err("the terminal refused raw mode")
805
+ }
806
+ const env: TermEnv = termIoEnv()
807
+ const size: i32[] = terminalSize()
808
+ const sized: boolean = toI32(size.length) >= 2 && size[0] > 0 && size[1] > 0
809
+ const cols: i32 = sized ? size[0] : TERM_IO_COLS
810
+ const rows: i32 = sized ? size[1] : TERM_IO_ROWS
811
+ setTerminalRestore(resetStyle() + showCursor() + leaveAltScreen())
812
+ write(enterAltScreen() + hideCursor() + probeQueries(env))
813
+ const probe: TermProbe = new TermProbe(env, cols, rows, termIoNowMs())
814
+ const buf: u8[] = new Array<u8>(TERM_IO_READ)
815
+ buf.fill(0)
816
+ let finished: boolean = false
817
+ while (!finished && !probe.done()) {
818
+ const nowMs: f64 = termIoNowMs()
819
+ if (probe.timedOut(nowMs)) {
820
+ finished = true
821
+ continue
822
+ }
823
+ const left: i32 = toI32(Math.ceil(probe.sentAtMs + probe.timeoutMs - nowMs))
824
+ if (waitStdin(left > 0 ? left : 0)) {
825
+ const n: i32 = readStdin(buf, 0, TERM_IO_READ)
826
+ if (n > 0) {
827
+ probe.feed(buf, n, termIoNowMs())
828
+ } else if (n !== -11) {
829
+ probe.finish(termIoNowMs())
830
+ finished = true
831
+ }
832
+ }
833
+ }
834
+ if (!finished) {
835
+ probe.finish(termIoNowMs())
836
+ }
837
+ const term: Terminal = new Terminal(options, probe.result(), cols, rows, probe.heldEvents())
838
+ term.enable()
839
+ return Ok(term)
840
+ }
841
+
842
+ /** What one `update` step gets: the events since the last step and which keys are down. */
843
+ export class TermInput {
844
+ events: TermEvent[]
845
+ keys: KeyState
846
+
847
+ constructor(keys: KeyState) {
848
+ this.events = []
849
+ this.keys = keys
850
+ }
851
+
852
+ /** Whether the key of kind `key` (a `KEY_*`; `code` for `KEY_CHAR`) is down. */
853
+ isDown(key: i32, code: i32): boolean {
854
+ return this.keys.isDown(key, code)
855
+ }
856
+ }
857
+
858
+ /** What `render` draws into: the cells, the pixels, how far into the next step the clock is, and the stats. */
859
+ export class Frame {
860
+ screen: Screen
861
+ framebuffer: Framebuffer
862
+ /** From 0 up to 1: how far past the last `update` step this frame is, to interpolate with. */
863
+ alpha: f64
864
+ stats: TermStats
865
+ caps: TermCaps
866
+
867
+ constructor(term: Terminal) {
868
+ this.screen = term.screen
869
+ this.framebuffer = term.framebuffer
870
+ this.alpha = 0.0
871
+ this.stats = term.stats()
872
+ this.caps = term.caps
873
+ }
874
+ }
875
+
876
+ /** Empty `events` keeping its storage. */
877
+ const termIoClear = (events: TermEvent[]): void => {
878
+ while (toI32(events.length) > 0) {
879
+ events.pop()
880
+ }
881
+ }
882
+
883
+ /** Whether `events` holds `EVENT_QUIT`. */
884
+ const termIoQuits = (events: TermEvent[]): boolean => {
885
+ for (const e of events) {
886
+ if (e.type === EVENT_QUIT) {
887
+ return true
888
+ }
889
+ }
890
+ return false
891
+ }
892
+
893
+ /** The time `step` next steps, on the clock its `advance` was given. */
894
+ const termIoDue = (step: FixedStep): f64 => step.lastMs + step.stepMs - step.carry
895
+
896
+ /** How long the loop may wait at `nowMs`: until the next step or frame is due, and no longer than an unfinished escape may be held. */
897
+ const termIoWait = (term: Terminal, step: FixedStep, nowMs: f64): i32 => {
898
+ let due: f64 = termIoDue(step)
899
+ const pacer: FixedStep = term.rate.pacer
900
+ if (pacer.started) {
901
+ const frame: f64 = termIoDue(pacer)
902
+ due = frame < due ? frame : due
903
+ }
904
+ let wait: f64 = due - nowMs
905
+ if (term.decoder.waiting() && wait > TERM_IO_ESCAPE_MS) {
906
+ wait = TERM_IO_ESCAPE_MS
907
+ }
908
+ if (!(wait > 0.0)) {
909
+ return 0
910
+ }
911
+ return toI32(Math.ceil(wait))
912
+ }
913
+
914
+ /**
915
+ * Open the terminal and run the program: `update(state, input)` once per
916
+ * fixed step (`options.stepsPerSecond`), the events since the last step in
917
+ * `input.events`, and `render(state, frame)` whenever the controller allows a
918
+ * frame, which is then presented. A resize is an `EVENT_RESIZE` (`x` the new
919
+ * columns, `y` the rows) with the `Screen` already resized. The loop ends when
920
+ * `update` answers false or the user quits (`EVENT_QUIT`), answering `Ok(0)`,
921
+ * and the terminal is closed however it ends. An `Err` is a terminal that
922
+ * could not be opened, or stdin ending.
923
+ */
924
+ export const runTerminal = <G>(
925
+ options: TerminalOptions,
926
+ state: G,
927
+ update: (g: G, input: TermInput) => boolean,
928
+ render: (g: G, frame: Frame) => void
929
+ ): Result<i32, string> => {
930
+ const opened: Result<Terminal, string> = openTerminal(options)
931
+ if (!opened.isOk()) {
932
+ return Err(opened.error)
933
+ }
934
+ const term: Terminal = opened.value
935
+ const input: TermInput = new TermInput(term.keys)
936
+ const frame: Frame = new Frame(term)
937
+ const step: FixedStep = new FixedStep(options.stepsPerSecond, 5)
938
+ step.advance(termIoNowMs())
939
+ let ended: boolean = false
940
+ let quit: boolean = false
941
+ while (!quit && !ended) {
942
+ ended = !term.readEvents(termIoWait(term, step, termIoNowMs()), input.events)
943
+ if (termIoQuits(input.events)) {
944
+ quit = true
945
+ continue
946
+ }
947
+ const nowMs: f64 = termIoNowMs()
948
+ term.rate.tick(nowMs)
949
+ const draw: boolean = term.rate.mayDraw(nowMs)
950
+ if (draw && term.checkResize()) {
951
+ const e: TermEvent = new TermEvent(EVENT_RESIZE)
952
+ e.x = term.screen.width
953
+ e.y = term.screen.height
954
+ input.events.push(e)
955
+ }
956
+ const steps: i32 = step.advance(nowMs)
957
+ const work: i64 = monotonicNanos()
958
+ for (let k: i32 = 0; k < steps && !quit; k++) {
959
+ quit = !update(state, input)
960
+ termIoClear(input.events)
961
+ }
962
+ if (draw && !quit) {
963
+ frame.alpha = step.alpha()
964
+ frame.stats = term.stats()
965
+ frame.framebuffer = term.framebuffer
966
+ render(state, frame)
967
+ }
968
+ term.workNs = term.workNs + toF64(monotonicNanos() - work)
969
+ if (draw && !quit) {
970
+ term.present()
971
+ }
972
+ }
973
+ term.close()
974
+ if (ended && !quit) {
975
+ return Err("stdin ended")
976
+ }
977
+ return Ok(toI32(0))
978
+ }