@amritk/nish-x86_64-linux 0.20.0 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +52 -10
- package/bin/nish +0 -0
- package/docs/AI.md +2777 -0
- package/package.json +2 -1
- package/runtime/browser.d.ts +6 -0
- package/runtime/browser.mjs +77 -0
- package/runtime/core/arrays.d.ts +36 -0
- package/runtime/core/arrays.mjs +153 -0
- package/runtime/core/checks.mjs +20 -0
- package/runtime/core/host.d.ts +80 -0
- package/runtime/core/host.mjs +325 -0
- package/runtime/core/numbers.d.ts +66 -0
- package/runtime/core/numbers.mjs +343 -0
- package/runtime/core/result.d.ts +45 -0
- package/runtime/core/result.mjs +140 -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 +46 -2
- package/runtime/nish.h +75 -2
- package/runtime/nish.mjs +23 -3
- package/runtime/node.mjs +386 -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-net.c +12 -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 +311 -59
- package/std/LICENSE-fast-float +23 -0
- package/std/LICENSE-fdlibm +8 -0
- package/std/README.md +32 -12
- package/std/base64.ts +116 -0
- package/std/bytes.ts +256 -0
- package/std/cli.ts +344 -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 +1557 -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 +87 -8
- package/std/math.ts +608 -0
- package/std/net/http1-server.ts +282 -40
- package/std/net/http1.ts +40 -21
- package/std/net/http3-server.ts +50 -12
- package/std/net/postgres.ts +2294 -0
- package/std/net/prefork.ts +23 -3
- package/std/net/quic-listener.ts +20 -4
- package/std/random.ts +66 -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
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
|
+
}
|