├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── assets
├── asciinema-player.css
├── asciinema-player.min.js
└── index.html
├── flake.lock
├── flake.nix
└── src
├── api.rs
├── api
├── http.rs
└── stdio.rs
├── cli.rs
├── command.rs
├── locale.rs
├── main.rs
├── nbio.rs
├── pty.rs
└── session.rs
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*.*.*"
7 |
8 | jobs:
9 | publish:
10 | name: ${{ matrix.target }}
11 | runs-on: ${{ matrix.os }}
12 | strategy:
13 | matrix:
14 | include:
15 | - os: ubuntu-latest
16 | target: x86_64-unknown-linux-gnu
17 | use-cross: false
18 |
19 | - os: ubuntu-latest
20 | target: x86_64-unknown-linux-musl
21 | use-cross: false
22 |
23 | - os: ubuntu-latest
24 | target: aarch64-unknown-linux-gnu
25 | use-cross: true
26 |
27 | - os: macos-latest
28 | target: x86_64-apple-darwin
29 | use-cross: false
30 |
31 | - os: macos-latest
32 | target: aarch64-apple-darwin
33 | use-cross: false
34 |
35 | steps:
36 | - uses: actions/checkout@v3
37 |
38 | - name: Install Rust
39 | uses: actions-rs/toolchain@v1
40 | with:
41 | toolchain: stable
42 | profile: minimal
43 | override: true
44 | target: ${{ matrix.target }}
45 |
46 | - name: Build
47 | uses: actions-rs/cargo@v1
48 | with:
49 | use-cross: ${{ matrix.use-cross }}
50 | command: build
51 | args: --target ${{ matrix.target }} --release --locked
52 |
53 | - name: Upload binaries to the release
54 | uses: svenstaro/upload-release-action@v2
55 | with:
56 | repo_token: ${{ secrets.GITHUB_TOKEN }}
57 | file: target/${{ matrix.target }}/release/ht
58 | asset_name: ht-${{ matrix.target }}
59 | tag: ${{ github.ref }}
60 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /.direnv
3 | .envrc
4 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "addr2line"
7 | version = "0.22.0"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
10 | dependencies = [
11 | "gimli",
12 | ]
13 |
14 | [[package]]
15 | name = "adler"
16 | version = "1.0.2"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
19 |
20 | [[package]]
21 | name = "anstream"
22 | version = "0.6.13"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
25 | dependencies = [
26 | "anstyle",
27 | "anstyle-parse",
28 | "anstyle-query",
29 | "anstyle-wincon",
30 | "colorchoice",
31 | "utf8parse",
32 | ]
33 |
34 | [[package]]
35 | name = "anstyle"
36 | version = "1.0.6"
37 | source = "registry+https://github.com/rust-lang/crates.io-index"
38 | checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
39 |
40 | [[package]]
41 | name = "anstyle-parse"
42 | version = "0.2.3"
43 | source = "registry+https://github.com/rust-lang/crates.io-index"
44 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
45 | dependencies = [
46 | "utf8parse",
47 | ]
48 |
49 | [[package]]
50 | name = "anstyle-query"
51 | version = "1.0.2"
52 | source = "registry+https://github.com/rust-lang/crates.io-index"
53 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
54 | dependencies = [
55 | "windows-sys 0.52.0",
56 | ]
57 |
58 | [[package]]
59 | name = "anstyle-wincon"
60 | version = "3.0.2"
61 | source = "registry+https://github.com/rust-lang/crates.io-index"
62 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
63 | dependencies = [
64 | "anstyle",
65 | "windows-sys 0.52.0",
66 | ]
67 |
68 | [[package]]
69 | name = "anyhow"
70 | version = "1.0.81"
71 | source = "registry+https://github.com/rust-lang/crates.io-index"
72 | checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
73 |
74 | [[package]]
75 | name = "async-trait"
76 | version = "0.1.80"
77 | source = "registry+https://github.com/rust-lang/crates.io-index"
78 | checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
79 | dependencies = [
80 | "proc-macro2",
81 | "quote",
82 | "syn",
83 | ]
84 |
85 | [[package]]
86 | name = "autocfg"
87 | version = "1.3.0"
88 | source = "registry+https://github.com/rust-lang/crates.io-index"
89 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
90 |
91 | [[package]]
92 | name = "avt"
93 | version = "0.11.1"
94 | source = "registry+https://github.com/rust-lang/crates.io-index"
95 | checksum = "50c216cde1660eddece428ecce4d0255d87c771c7317f59f168cb184efdc754c"
96 | dependencies = [
97 | "rgb",
98 | "serde",
99 | "unicode-width",
100 | ]
101 |
102 | [[package]]
103 | name = "axum"
104 | version = "0.7.5"
105 | source = "registry+https://github.com/rust-lang/crates.io-index"
106 | checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf"
107 | dependencies = [
108 | "async-trait",
109 | "axum-core",
110 | "base64",
111 | "bytes",
112 | "futures-util",
113 | "http",
114 | "http-body",
115 | "http-body-util",
116 | "hyper",
117 | "hyper-util",
118 | "itoa",
119 | "matchit",
120 | "memchr",
121 | "mime",
122 | "percent-encoding",
123 | "pin-project-lite",
124 | "rustversion",
125 | "serde",
126 | "serde_urlencoded",
127 | "sha1",
128 | "sync_wrapper 1.0.1",
129 | "tokio",
130 | "tokio-tungstenite",
131 | "tower",
132 | "tower-layer",
133 | "tower-service",
134 | ]
135 |
136 | [[package]]
137 | name = "axum-core"
138 | version = "0.4.3"
139 | source = "registry+https://github.com/rust-lang/crates.io-index"
140 | checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3"
141 | dependencies = [
142 | "async-trait",
143 | "bytes",
144 | "futures-util",
145 | "http",
146 | "http-body",
147 | "http-body-util",
148 | "mime",
149 | "pin-project-lite",
150 | "rustversion",
151 | "sync_wrapper 0.1.2",
152 | "tower-layer",
153 | "tower-service",
154 | ]
155 |
156 | [[package]]
157 | name = "backtrace"
158 | version = "0.3.73"
159 | source = "registry+https://github.com/rust-lang/crates.io-index"
160 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
161 | dependencies = [
162 | "addr2line",
163 | "cc",
164 | "cfg-if",
165 | "libc",
166 | "miniz_oxide",
167 | "object",
168 | "rustc-demangle",
169 | ]
170 |
171 | [[package]]
172 | name = "base64"
173 | version = "0.21.7"
174 | source = "registry+https://github.com/rust-lang/crates.io-index"
175 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
176 |
177 | [[package]]
178 | name = "bitflags"
179 | version = "2.5.0"
180 | source = "registry+https://github.com/rust-lang/crates.io-index"
181 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
182 |
183 | [[package]]
184 | name = "block-buffer"
185 | version = "0.10.4"
186 | source = "registry+https://github.com/rust-lang/crates.io-index"
187 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
188 | dependencies = [
189 | "generic-array",
190 | ]
191 |
192 | [[package]]
193 | name = "bytemuck"
194 | version = "1.15.0"
195 | source = "registry+https://github.com/rust-lang/crates.io-index"
196 | checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15"
197 |
198 | [[package]]
199 | name = "byteorder"
200 | version = "1.5.0"
201 | source = "registry+https://github.com/rust-lang/crates.io-index"
202 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
203 |
204 | [[package]]
205 | name = "bytes"
206 | version = "1.6.0"
207 | source = "registry+https://github.com/rust-lang/crates.io-index"
208 | checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
209 |
210 | [[package]]
211 | name = "cc"
212 | version = "1.0.100"
213 | source = "registry+https://github.com/rust-lang/crates.io-index"
214 | checksum = "c891175c3fb232128f48de6590095e59198bbeb8620c310be349bfc3afd12c7b"
215 |
216 | [[package]]
217 | name = "cfg-if"
218 | version = "1.0.0"
219 | source = "registry+https://github.com/rust-lang/crates.io-index"
220 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
221 |
222 | [[package]]
223 | name = "cfg_aliases"
224 | version = "0.1.1"
225 | source = "registry+https://github.com/rust-lang/crates.io-index"
226 | checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
227 |
228 | [[package]]
229 | name = "clap"
230 | version = "4.5.4"
231 | source = "registry+https://github.com/rust-lang/crates.io-index"
232 | checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
233 | dependencies = [
234 | "clap_builder",
235 | "clap_derive",
236 | ]
237 |
238 | [[package]]
239 | name = "clap_builder"
240 | version = "4.5.2"
241 | source = "registry+https://github.com/rust-lang/crates.io-index"
242 | checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
243 | dependencies = [
244 | "anstream",
245 | "anstyle",
246 | "clap_lex",
247 | "strsim",
248 | ]
249 |
250 | [[package]]
251 | name = "clap_derive"
252 | version = "4.5.4"
253 | source = "registry+https://github.com/rust-lang/crates.io-index"
254 | checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
255 | dependencies = [
256 | "heck",
257 | "proc-macro2",
258 | "quote",
259 | "syn",
260 | ]
261 |
262 | [[package]]
263 | name = "clap_lex"
264 | version = "0.7.0"
265 | source = "registry+https://github.com/rust-lang/crates.io-index"
266 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
267 |
268 | [[package]]
269 | name = "colorchoice"
270 | version = "1.0.0"
271 | source = "registry+https://github.com/rust-lang/crates.io-index"
272 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
273 |
274 | [[package]]
275 | name = "cpufeatures"
276 | version = "0.2.12"
277 | source = "registry+https://github.com/rust-lang/crates.io-index"
278 | checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
279 | dependencies = [
280 | "libc",
281 | ]
282 |
283 | [[package]]
284 | name = "crypto-common"
285 | version = "0.1.6"
286 | source = "registry+https://github.com/rust-lang/crates.io-index"
287 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
288 | dependencies = [
289 | "generic-array",
290 | "typenum",
291 | ]
292 |
293 | [[package]]
294 | name = "data-encoding"
295 | version = "2.6.0"
296 | source = "registry+https://github.com/rust-lang/crates.io-index"
297 | checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
298 |
299 | [[package]]
300 | name = "digest"
301 | version = "0.10.7"
302 | source = "registry+https://github.com/rust-lang/crates.io-index"
303 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
304 | dependencies = [
305 | "block-buffer",
306 | "crypto-common",
307 | ]
308 |
309 | [[package]]
310 | name = "fnv"
311 | version = "1.0.7"
312 | source = "registry+https://github.com/rust-lang/crates.io-index"
313 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
314 |
315 | [[package]]
316 | name = "form_urlencoded"
317 | version = "1.2.1"
318 | source = "registry+https://github.com/rust-lang/crates.io-index"
319 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
320 | dependencies = [
321 | "percent-encoding",
322 | ]
323 |
324 | [[package]]
325 | name = "futures-channel"
326 | version = "0.3.30"
327 | source = "registry+https://github.com/rust-lang/crates.io-index"
328 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
329 | dependencies = [
330 | "futures-core",
331 | ]
332 |
333 | [[package]]
334 | name = "futures-core"
335 | version = "0.3.30"
336 | source = "registry+https://github.com/rust-lang/crates.io-index"
337 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
338 |
339 | [[package]]
340 | name = "futures-macro"
341 | version = "0.3.30"
342 | source = "registry+https://github.com/rust-lang/crates.io-index"
343 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
344 | dependencies = [
345 | "proc-macro2",
346 | "quote",
347 | "syn",
348 | ]
349 |
350 | [[package]]
351 | name = "futures-sink"
352 | version = "0.3.30"
353 | source = "registry+https://github.com/rust-lang/crates.io-index"
354 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
355 |
356 | [[package]]
357 | name = "futures-task"
358 | version = "0.3.30"
359 | source = "registry+https://github.com/rust-lang/crates.io-index"
360 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
361 |
362 | [[package]]
363 | name = "futures-util"
364 | version = "0.3.30"
365 | source = "registry+https://github.com/rust-lang/crates.io-index"
366 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
367 | dependencies = [
368 | "futures-core",
369 | "futures-macro",
370 | "futures-sink",
371 | "futures-task",
372 | "pin-project-lite",
373 | "pin-utils",
374 | "slab",
375 | ]
376 |
377 | [[package]]
378 | name = "generic-array"
379 | version = "0.14.7"
380 | source = "registry+https://github.com/rust-lang/crates.io-index"
381 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
382 | dependencies = [
383 | "typenum",
384 | "version_check",
385 | ]
386 |
387 | [[package]]
388 | name = "getrandom"
389 | version = "0.2.14"
390 | source = "registry+https://github.com/rust-lang/crates.io-index"
391 | checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
392 | dependencies = [
393 | "cfg-if",
394 | "libc",
395 | "wasi",
396 | ]
397 |
398 | [[package]]
399 | name = "gimli"
400 | version = "0.29.0"
401 | source = "registry+https://github.com/rust-lang/crates.io-index"
402 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
403 |
404 | [[package]]
405 | name = "heck"
406 | version = "0.5.0"
407 | source = "registry+https://github.com/rust-lang/crates.io-index"
408 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
409 |
410 | [[package]]
411 | name = "hermit-abi"
412 | version = "0.3.9"
413 | source = "registry+https://github.com/rust-lang/crates.io-index"
414 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
415 |
416 | [[package]]
417 | name = "ht"
418 | version = "0.3.0"
419 | dependencies = [
420 | "anyhow",
421 | "avt",
422 | "axum",
423 | "clap",
424 | "futures-util",
425 | "mime_guess",
426 | "mio",
427 | "nix",
428 | "rust-embed",
429 | "serde",
430 | "serde_json",
431 | "tokio",
432 | "tokio-stream",
433 | ]
434 |
435 | [[package]]
436 | name = "http"
437 | version = "1.1.0"
438 | source = "registry+https://github.com/rust-lang/crates.io-index"
439 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
440 | dependencies = [
441 | "bytes",
442 | "fnv",
443 | "itoa",
444 | ]
445 |
446 | [[package]]
447 | name = "http-body"
448 | version = "1.0.0"
449 | source = "registry+https://github.com/rust-lang/crates.io-index"
450 | checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
451 | dependencies = [
452 | "bytes",
453 | "http",
454 | ]
455 |
456 | [[package]]
457 | name = "http-body-util"
458 | version = "0.1.2"
459 | source = "registry+https://github.com/rust-lang/crates.io-index"
460 | checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
461 | dependencies = [
462 | "bytes",
463 | "futures-util",
464 | "http",
465 | "http-body",
466 | "pin-project-lite",
467 | ]
468 |
469 | [[package]]
470 | name = "httparse"
471 | version = "1.9.4"
472 | source = "registry+https://github.com/rust-lang/crates.io-index"
473 | checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
474 |
475 | [[package]]
476 | name = "httpdate"
477 | version = "1.0.3"
478 | source = "registry+https://github.com/rust-lang/crates.io-index"
479 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
480 |
481 | [[package]]
482 | name = "hyper"
483 | version = "1.3.1"
484 | source = "registry+https://github.com/rust-lang/crates.io-index"
485 | checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
486 | dependencies = [
487 | "bytes",
488 | "futures-channel",
489 | "futures-util",
490 | "http",
491 | "http-body",
492 | "httparse",
493 | "httpdate",
494 | "itoa",
495 | "pin-project-lite",
496 | "smallvec",
497 | "tokio",
498 | ]
499 |
500 | [[package]]
501 | name = "hyper-util"
502 | version = "0.1.5"
503 | source = "registry+https://github.com/rust-lang/crates.io-index"
504 | checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56"
505 | dependencies = [
506 | "bytes",
507 | "futures-util",
508 | "http",
509 | "http-body",
510 | "hyper",
511 | "pin-project-lite",
512 | "tokio",
513 | ]
514 |
515 | [[package]]
516 | name = "idna"
517 | version = "0.5.0"
518 | source = "registry+https://github.com/rust-lang/crates.io-index"
519 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
520 | dependencies = [
521 | "unicode-bidi",
522 | "unicode-normalization",
523 | ]
524 |
525 | [[package]]
526 | name = "itoa"
527 | version = "1.0.11"
528 | source = "registry+https://github.com/rust-lang/crates.io-index"
529 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
530 |
531 | [[package]]
532 | name = "libc"
533 | version = "0.2.153"
534 | source = "registry+https://github.com/rust-lang/crates.io-index"
535 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
536 |
537 | [[package]]
538 | name = "lock_api"
539 | version = "0.4.12"
540 | source = "registry+https://github.com/rust-lang/crates.io-index"
541 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
542 | dependencies = [
543 | "autocfg",
544 | "scopeguard",
545 | ]
546 |
547 | [[package]]
548 | name = "log"
549 | version = "0.4.21"
550 | source = "registry+https://github.com/rust-lang/crates.io-index"
551 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
552 |
553 | [[package]]
554 | name = "matchit"
555 | version = "0.7.3"
556 | source = "registry+https://github.com/rust-lang/crates.io-index"
557 | checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
558 |
559 | [[package]]
560 | name = "memchr"
561 | version = "2.7.4"
562 | source = "registry+https://github.com/rust-lang/crates.io-index"
563 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
564 |
565 | [[package]]
566 | name = "mime"
567 | version = "0.3.17"
568 | source = "registry+https://github.com/rust-lang/crates.io-index"
569 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
570 |
571 | [[package]]
572 | name = "mime_guess"
573 | version = "2.0.5"
574 | source = "registry+https://github.com/rust-lang/crates.io-index"
575 | checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
576 | dependencies = [
577 | "mime",
578 | "unicase",
579 | ]
580 |
581 | [[package]]
582 | name = "miniz_oxide"
583 | version = "0.7.4"
584 | source = "registry+https://github.com/rust-lang/crates.io-index"
585 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
586 | dependencies = [
587 | "adler",
588 | ]
589 |
590 | [[package]]
591 | name = "mio"
592 | version = "0.8.11"
593 | source = "registry+https://github.com/rust-lang/crates.io-index"
594 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
595 | dependencies = [
596 | "libc",
597 | "log",
598 | "wasi",
599 | "windows-sys 0.48.0",
600 | ]
601 |
602 | [[package]]
603 | name = "nix"
604 | version = "0.28.0"
605 | source = "registry+https://github.com/rust-lang/crates.io-index"
606 | checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
607 | dependencies = [
608 | "bitflags",
609 | "cfg-if",
610 | "cfg_aliases",
611 | "libc",
612 | ]
613 |
614 | [[package]]
615 | name = "num_cpus"
616 | version = "1.16.0"
617 | source = "registry+https://github.com/rust-lang/crates.io-index"
618 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
619 | dependencies = [
620 | "hermit-abi",
621 | "libc",
622 | ]
623 |
624 | [[package]]
625 | name = "object"
626 | version = "0.36.0"
627 | source = "registry+https://github.com/rust-lang/crates.io-index"
628 | checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434"
629 | dependencies = [
630 | "memchr",
631 | ]
632 |
633 | [[package]]
634 | name = "parking_lot"
635 | version = "0.12.3"
636 | source = "registry+https://github.com/rust-lang/crates.io-index"
637 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
638 | dependencies = [
639 | "lock_api",
640 | "parking_lot_core",
641 | ]
642 |
643 | [[package]]
644 | name = "parking_lot_core"
645 | version = "0.9.10"
646 | source = "registry+https://github.com/rust-lang/crates.io-index"
647 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
648 | dependencies = [
649 | "cfg-if",
650 | "libc",
651 | "redox_syscall",
652 | "smallvec",
653 | "windows-targets 0.52.4",
654 | ]
655 |
656 | [[package]]
657 | name = "percent-encoding"
658 | version = "2.3.1"
659 | source = "registry+https://github.com/rust-lang/crates.io-index"
660 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
661 |
662 | [[package]]
663 | name = "pin-project"
664 | version = "1.1.5"
665 | source = "registry+https://github.com/rust-lang/crates.io-index"
666 | checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
667 | dependencies = [
668 | "pin-project-internal",
669 | ]
670 |
671 | [[package]]
672 | name = "pin-project-internal"
673 | version = "1.1.5"
674 | source = "registry+https://github.com/rust-lang/crates.io-index"
675 | checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
676 | dependencies = [
677 | "proc-macro2",
678 | "quote",
679 | "syn",
680 | ]
681 |
682 | [[package]]
683 | name = "pin-project-lite"
684 | version = "0.2.14"
685 | source = "registry+https://github.com/rust-lang/crates.io-index"
686 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
687 |
688 | [[package]]
689 | name = "pin-utils"
690 | version = "0.1.0"
691 | source = "registry+https://github.com/rust-lang/crates.io-index"
692 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
693 |
694 | [[package]]
695 | name = "ppv-lite86"
696 | version = "0.2.17"
697 | source = "registry+https://github.com/rust-lang/crates.io-index"
698 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
699 |
700 | [[package]]
701 | name = "proc-macro2"
702 | version = "1.0.79"
703 | source = "registry+https://github.com/rust-lang/crates.io-index"
704 | checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
705 | dependencies = [
706 | "unicode-ident",
707 | ]
708 |
709 | [[package]]
710 | name = "quote"
711 | version = "1.0.35"
712 | source = "registry+https://github.com/rust-lang/crates.io-index"
713 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
714 | dependencies = [
715 | "proc-macro2",
716 | ]
717 |
718 | [[package]]
719 | name = "rand"
720 | version = "0.8.5"
721 | source = "registry+https://github.com/rust-lang/crates.io-index"
722 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
723 | dependencies = [
724 | "libc",
725 | "rand_chacha",
726 | "rand_core",
727 | ]
728 |
729 | [[package]]
730 | name = "rand_chacha"
731 | version = "0.3.1"
732 | source = "registry+https://github.com/rust-lang/crates.io-index"
733 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
734 | dependencies = [
735 | "ppv-lite86",
736 | "rand_core",
737 | ]
738 |
739 | [[package]]
740 | name = "rand_core"
741 | version = "0.6.4"
742 | source = "registry+https://github.com/rust-lang/crates.io-index"
743 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
744 | dependencies = [
745 | "getrandom",
746 | ]
747 |
748 | [[package]]
749 | name = "redox_syscall"
750 | version = "0.5.2"
751 | source = "registry+https://github.com/rust-lang/crates.io-index"
752 | checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd"
753 | dependencies = [
754 | "bitflags",
755 | ]
756 |
757 | [[package]]
758 | name = "rgb"
759 | version = "0.8.37"
760 | source = "registry+https://github.com/rust-lang/crates.io-index"
761 | checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8"
762 | dependencies = [
763 | "bytemuck",
764 | ]
765 |
766 | [[package]]
767 | name = "rust-embed"
768 | version = "8.4.0"
769 | source = "registry+https://github.com/rust-lang/crates.io-index"
770 | checksum = "19549741604902eb99a7ed0ee177a0663ee1eda51a29f71401f166e47e77806a"
771 | dependencies = [
772 | "rust-embed-impl",
773 | "rust-embed-utils",
774 | "walkdir",
775 | ]
776 |
777 | [[package]]
778 | name = "rust-embed-impl"
779 | version = "8.4.0"
780 | source = "registry+https://github.com/rust-lang/crates.io-index"
781 | checksum = "cb9f96e283ec64401f30d3df8ee2aaeb2561f34c824381efa24a35f79bf40ee4"
782 | dependencies = [
783 | "proc-macro2",
784 | "quote",
785 | "rust-embed-utils",
786 | "syn",
787 | "walkdir",
788 | ]
789 |
790 | [[package]]
791 | name = "rust-embed-utils"
792 | version = "8.4.0"
793 | source = "registry+https://github.com/rust-lang/crates.io-index"
794 | checksum = "38c74a686185620830701348de757fd36bef4aa9680fd23c49fc539ddcc1af32"
795 | dependencies = [
796 | "sha2",
797 | "walkdir",
798 | ]
799 |
800 | [[package]]
801 | name = "rustc-demangle"
802 | version = "0.1.24"
803 | source = "registry+https://github.com/rust-lang/crates.io-index"
804 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
805 |
806 | [[package]]
807 | name = "rustversion"
808 | version = "1.0.17"
809 | source = "registry+https://github.com/rust-lang/crates.io-index"
810 | checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
811 |
812 | [[package]]
813 | name = "ryu"
814 | version = "1.0.17"
815 | source = "registry+https://github.com/rust-lang/crates.io-index"
816 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
817 |
818 | [[package]]
819 | name = "same-file"
820 | version = "1.0.6"
821 | source = "registry+https://github.com/rust-lang/crates.io-index"
822 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
823 | dependencies = [
824 | "winapi-util",
825 | ]
826 |
827 | [[package]]
828 | name = "scopeguard"
829 | version = "1.2.0"
830 | source = "registry+https://github.com/rust-lang/crates.io-index"
831 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
832 |
833 | [[package]]
834 | name = "serde"
835 | version = "1.0.203"
836 | source = "registry+https://github.com/rust-lang/crates.io-index"
837 | checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
838 | dependencies = [
839 | "serde_derive",
840 | ]
841 |
842 | [[package]]
843 | name = "serde_derive"
844 | version = "1.0.203"
845 | source = "registry+https://github.com/rust-lang/crates.io-index"
846 | checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
847 | dependencies = [
848 | "proc-macro2",
849 | "quote",
850 | "syn",
851 | ]
852 |
853 | [[package]]
854 | name = "serde_json"
855 | version = "1.0.117"
856 | source = "registry+https://github.com/rust-lang/crates.io-index"
857 | checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
858 | dependencies = [
859 | "itoa",
860 | "ryu",
861 | "serde",
862 | ]
863 |
864 | [[package]]
865 | name = "serde_urlencoded"
866 | version = "0.7.1"
867 | source = "registry+https://github.com/rust-lang/crates.io-index"
868 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
869 | dependencies = [
870 | "form_urlencoded",
871 | "itoa",
872 | "ryu",
873 | "serde",
874 | ]
875 |
876 | [[package]]
877 | name = "sha1"
878 | version = "0.10.6"
879 | source = "registry+https://github.com/rust-lang/crates.io-index"
880 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
881 | dependencies = [
882 | "cfg-if",
883 | "cpufeatures",
884 | "digest",
885 | ]
886 |
887 | [[package]]
888 | name = "sha2"
889 | version = "0.10.8"
890 | source = "registry+https://github.com/rust-lang/crates.io-index"
891 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
892 | dependencies = [
893 | "cfg-if",
894 | "cpufeatures",
895 | "digest",
896 | ]
897 |
898 | [[package]]
899 | name = "signal-hook-registry"
900 | version = "1.4.2"
901 | source = "registry+https://github.com/rust-lang/crates.io-index"
902 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
903 | dependencies = [
904 | "libc",
905 | ]
906 |
907 | [[package]]
908 | name = "slab"
909 | version = "0.4.9"
910 | source = "registry+https://github.com/rust-lang/crates.io-index"
911 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
912 | dependencies = [
913 | "autocfg",
914 | ]
915 |
916 | [[package]]
917 | name = "smallvec"
918 | version = "1.13.2"
919 | source = "registry+https://github.com/rust-lang/crates.io-index"
920 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
921 |
922 | [[package]]
923 | name = "socket2"
924 | version = "0.5.7"
925 | source = "registry+https://github.com/rust-lang/crates.io-index"
926 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
927 | dependencies = [
928 | "libc",
929 | "windows-sys 0.52.0",
930 | ]
931 |
932 | [[package]]
933 | name = "strsim"
934 | version = "0.11.1"
935 | source = "registry+https://github.com/rust-lang/crates.io-index"
936 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
937 |
938 | [[package]]
939 | name = "syn"
940 | version = "2.0.57"
941 | source = "registry+https://github.com/rust-lang/crates.io-index"
942 | checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35"
943 | dependencies = [
944 | "proc-macro2",
945 | "quote",
946 | "unicode-ident",
947 | ]
948 |
949 | [[package]]
950 | name = "sync_wrapper"
951 | version = "0.1.2"
952 | source = "registry+https://github.com/rust-lang/crates.io-index"
953 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
954 |
955 | [[package]]
956 | name = "sync_wrapper"
957 | version = "1.0.1"
958 | source = "registry+https://github.com/rust-lang/crates.io-index"
959 | checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
960 |
961 | [[package]]
962 | name = "thiserror"
963 | version = "1.0.61"
964 | source = "registry+https://github.com/rust-lang/crates.io-index"
965 | checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
966 | dependencies = [
967 | "thiserror-impl",
968 | ]
969 |
970 | [[package]]
971 | name = "thiserror-impl"
972 | version = "1.0.61"
973 | source = "registry+https://github.com/rust-lang/crates.io-index"
974 | checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
975 | dependencies = [
976 | "proc-macro2",
977 | "quote",
978 | "syn",
979 | ]
980 |
981 | [[package]]
982 | name = "tinyvec"
983 | version = "1.6.1"
984 | source = "registry+https://github.com/rust-lang/crates.io-index"
985 | checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82"
986 | dependencies = [
987 | "tinyvec_macros",
988 | ]
989 |
990 | [[package]]
991 | name = "tinyvec_macros"
992 | version = "0.1.1"
993 | source = "registry+https://github.com/rust-lang/crates.io-index"
994 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
995 |
996 | [[package]]
997 | name = "tokio"
998 | version = "1.38.0"
999 | source = "registry+https://github.com/rust-lang/crates.io-index"
1000 | checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
1001 | dependencies = [
1002 | "backtrace",
1003 | "bytes",
1004 | "libc",
1005 | "mio",
1006 | "num_cpus",
1007 | "parking_lot",
1008 | "pin-project-lite",
1009 | "signal-hook-registry",
1010 | "socket2",
1011 | "tokio-macros",
1012 | "windows-sys 0.48.0",
1013 | ]
1014 |
1015 | [[package]]
1016 | name = "tokio-macros"
1017 | version = "2.3.0"
1018 | source = "registry+https://github.com/rust-lang/crates.io-index"
1019 | checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
1020 | dependencies = [
1021 | "proc-macro2",
1022 | "quote",
1023 | "syn",
1024 | ]
1025 |
1026 | [[package]]
1027 | name = "tokio-stream"
1028 | version = "0.1.15"
1029 | source = "registry+https://github.com/rust-lang/crates.io-index"
1030 | checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
1031 | dependencies = [
1032 | "futures-core",
1033 | "pin-project-lite",
1034 | "tokio",
1035 | "tokio-util",
1036 | ]
1037 |
1038 | [[package]]
1039 | name = "tokio-tungstenite"
1040 | version = "0.21.0"
1041 | source = "registry+https://github.com/rust-lang/crates.io-index"
1042 | checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38"
1043 | dependencies = [
1044 | "futures-util",
1045 | "log",
1046 | "tokio",
1047 | "tungstenite",
1048 | ]
1049 |
1050 | [[package]]
1051 | name = "tokio-util"
1052 | version = "0.7.11"
1053 | source = "registry+https://github.com/rust-lang/crates.io-index"
1054 | checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
1055 | dependencies = [
1056 | "bytes",
1057 | "futures-core",
1058 | "futures-sink",
1059 | "pin-project-lite",
1060 | "tokio",
1061 | ]
1062 |
1063 | [[package]]
1064 | name = "tower"
1065 | version = "0.4.13"
1066 | source = "registry+https://github.com/rust-lang/crates.io-index"
1067 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
1068 | dependencies = [
1069 | "futures-core",
1070 | "futures-util",
1071 | "pin-project",
1072 | "pin-project-lite",
1073 | "tokio",
1074 | "tower-layer",
1075 | "tower-service",
1076 | ]
1077 |
1078 | [[package]]
1079 | name = "tower-layer"
1080 | version = "0.3.2"
1081 | source = "registry+https://github.com/rust-lang/crates.io-index"
1082 | checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
1083 |
1084 | [[package]]
1085 | name = "tower-service"
1086 | version = "0.3.2"
1087 | source = "registry+https://github.com/rust-lang/crates.io-index"
1088 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
1089 |
1090 | [[package]]
1091 | name = "tungstenite"
1092 | version = "0.21.0"
1093 | source = "registry+https://github.com/rust-lang/crates.io-index"
1094 | checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1"
1095 | dependencies = [
1096 | "byteorder",
1097 | "bytes",
1098 | "data-encoding",
1099 | "http",
1100 | "httparse",
1101 | "log",
1102 | "rand",
1103 | "sha1",
1104 | "thiserror",
1105 | "url",
1106 | "utf-8",
1107 | ]
1108 |
1109 | [[package]]
1110 | name = "typenum"
1111 | version = "1.17.0"
1112 | source = "registry+https://github.com/rust-lang/crates.io-index"
1113 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
1114 |
1115 | [[package]]
1116 | name = "unicase"
1117 | version = "2.7.0"
1118 | source = "registry+https://github.com/rust-lang/crates.io-index"
1119 | checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
1120 | dependencies = [
1121 | "version_check",
1122 | ]
1123 |
1124 | [[package]]
1125 | name = "unicode-bidi"
1126 | version = "0.3.15"
1127 | source = "registry+https://github.com/rust-lang/crates.io-index"
1128 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
1129 |
1130 | [[package]]
1131 | name = "unicode-ident"
1132 | version = "1.0.12"
1133 | source = "registry+https://github.com/rust-lang/crates.io-index"
1134 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
1135 |
1136 | [[package]]
1137 | name = "unicode-normalization"
1138 | version = "0.1.23"
1139 | source = "registry+https://github.com/rust-lang/crates.io-index"
1140 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
1141 | dependencies = [
1142 | "tinyvec",
1143 | ]
1144 |
1145 | [[package]]
1146 | name = "unicode-width"
1147 | version = "0.1.13"
1148 | source = "registry+https://github.com/rust-lang/crates.io-index"
1149 | checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
1150 |
1151 | [[package]]
1152 | name = "url"
1153 | version = "2.5.2"
1154 | source = "registry+https://github.com/rust-lang/crates.io-index"
1155 | checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
1156 | dependencies = [
1157 | "form_urlencoded",
1158 | "idna",
1159 | "percent-encoding",
1160 | ]
1161 |
1162 | [[package]]
1163 | name = "utf-8"
1164 | version = "0.7.6"
1165 | source = "registry+https://github.com/rust-lang/crates.io-index"
1166 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
1167 |
1168 | [[package]]
1169 | name = "utf8parse"
1170 | version = "0.2.1"
1171 | source = "registry+https://github.com/rust-lang/crates.io-index"
1172 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
1173 |
1174 | [[package]]
1175 | name = "version_check"
1176 | version = "0.9.4"
1177 | source = "registry+https://github.com/rust-lang/crates.io-index"
1178 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
1179 |
1180 | [[package]]
1181 | name = "walkdir"
1182 | version = "2.5.0"
1183 | source = "registry+https://github.com/rust-lang/crates.io-index"
1184 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
1185 | dependencies = [
1186 | "same-file",
1187 | "winapi-util",
1188 | ]
1189 |
1190 | [[package]]
1191 | name = "wasi"
1192 | version = "0.11.0+wasi-snapshot-preview1"
1193 | source = "registry+https://github.com/rust-lang/crates.io-index"
1194 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
1195 |
1196 | [[package]]
1197 | name = "winapi-util"
1198 | version = "0.1.8"
1199 | source = "registry+https://github.com/rust-lang/crates.io-index"
1200 | checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
1201 | dependencies = [
1202 | "windows-sys 0.52.0",
1203 | ]
1204 |
1205 | [[package]]
1206 | name = "windows-sys"
1207 | version = "0.48.0"
1208 | source = "registry+https://github.com/rust-lang/crates.io-index"
1209 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
1210 | dependencies = [
1211 | "windows-targets 0.48.5",
1212 | ]
1213 |
1214 | [[package]]
1215 | name = "windows-sys"
1216 | version = "0.52.0"
1217 | source = "registry+https://github.com/rust-lang/crates.io-index"
1218 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
1219 | dependencies = [
1220 | "windows-targets 0.52.4",
1221 | ]
1222 |
1223 | [[package]]
1224 | name = "windows-targets"
1225 | version = "0.48.5"
1226 | source = "registry+https://github.com/rust-lang/crates.io-index"
1227 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
1228 | dependencies = [
1229 | "windows_aarch64_gnullvm 0.48.5",
1230 | "windows_aarch64_msvc 0.48.5",
1231 | "windows_i686_gnu 0.48.5",
1232 | "windows_i686_msvc 0.48.5",
1233 | "windows_x86_64_gnu 0.48.5",
1234 | "windows_x86_64_gnullvm 0.48.5",
1235 | "windows_x86_64_msvc 0.48.5",
1236 | ]
1237 |
1238 | [[package]]
1239 | name = "windows-targets"
1240 | version = "0.52.4"
1241 | source = "registry+https://github.com/rust-lang/crates.io-index"
1242 | checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
1243 | dependencies = [
1244 | "windows_aarch64_gnullvm 0.52.4",
1245 | "windows_aarch64_msvc 0.52.4",
1246 | "windows_i686_gnu 0.52.4",
1247 | "windows_i686_msvc 0.52.4",
1248 | "windows_x86_64_gnu 0.52.4",
1249 | "windows_x86_64_gnullvm 0.52.4",
1250 | "windows_x86_64_msvc 0.52.4",
1251 | ]
1252 |
1253 | [[package]]
1254 | name = "windows_aarch64_gnullvm"
1255 | version = "0.48.5"
1256 | source = "registry+https://github.com/rust-lang/crates.io-index"
1257 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
1258 |
1259 | [[package]]
1260 | name = "windows_aarch64_gnullvm"
1261 | version = "0.52.4"
1262 | source = "registry+https://github.com/rust-lang/crates.io-index"
1263 | checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
1264 |
1265 | [[package]]
1266 | name = "windows_aarch64_msvc"
1267 | version = "0.48.5"
1268 | source = "registry+https://github.com/rust-lang/crates.io-index"
1269 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
1270 |
1271 | [[package]]
1272 | name = "windows_aarch64_msvc"
1273 | version = "0.52.4"
1274 | source = "registry+https://github.com/rust-lang/crates.io-index"
1275 | checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
1276 |
1277 | [[package]]
1278 | name = "windows_i686_gnu"
1279 | version = "0.48.5"
1280 | source = "registry+https://github.com/rust-lang/crates.io-index"
1281 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
1282 |
1283 | [[package]]
1284 | name = "windows_i686_gnu"
1285 | version = "0.52.4"
1286 | source = "registry+https://github.com/rust-lang/crates.io-index"
1287 | checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
1288 |
1289 | [[package]]
1290 | name = "windows_i686_msvc"
1291 | version = "0.48.5"
1292 | source = "registry+https://github.com/rust-lang/crates.io-index"
1293 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
1294 |
1295 | [[package]]
1296 | name = "windows_i686_msvc"
1297 | version = "0.52.4"
1298 | source = "registry+https://github.com/rust-lang/crates.io-index"
1299 | checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
1300 |
1301 | [[package]]
1302 | name = "windows_x86_64_gnu"
1303 | version = "0.48.5"
1304 | source = "registry+https://github.com/rust-lang/crates.io-index"
1305 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
1306 |
1307 | [[package]]
1308 | name = "windows_x86_64_gnu"
1309 | version = "0.52.4"
1310 | source = "registry+https://github.com/rust-lang/crates.io-index"
1311 | checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
1312 |
1313 | [[package]]
1314 | name = "windows_x86_64_gnullvm"
1315 | version = "0.48.5"
1316 | source = "registry+https://github.com/rust-lang/crates.io-index"
1317 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
1318 |
1319 | [[package]]
1320 | name = "windows_x86_64_gnullvm"
1321 | version = "0.52.4"
1322 | source = "registry+https://github.com/rust-lang/crates.io-index"
1323 | checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
1324 |
1325 | [[package]]
1326 | name = "windows_x86_64_msvc"
1327 | version = "0.48.5"
1328 | source = "registry+https://github.com/rust-lang/crates.io-index"
1329 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
1330 |
1331 | [[package]]
1332 | name = "windows_x86_64_msvc"
1333 | version = "0.52.4"
1334 | source = "registry+https://github.com/rust-lang/crates.io-index"
1335 | checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
1336 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "ht"
3 | version = "0.3.0"
4 | edition = "2021"
5 | rust-version = "1.74"
6 |
7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8 |
9 | [dependencies]
10 | avt = "0.11.1"
11 | nix = { version = "0.28.0", features = ["term", "process", "fs", "signal"] }
12 | serde_json = "1.0.117"
13 | mio = { version = "0.8.11", features = ["os-poll", "os-ext"] }
14 | anyhow = "1.0.81"
15 | clap = { version = "4.5.4", features = ["derive"] }
16 | serde = "1.0.203"
17 | tokio = { version = "1.38.0", features = ["full"] }
18 | axum = { version = "0.7.5", default-features = false, features = ["http1", "ws", "query"] }
19 | tokio-stream = { version = "0.1.15", features = ["sync"] }
20 | futures-util = "0.3.30"
21 | rust-embed = "8.4.0"
22 | mime_guess = "2.0.5"
23 |
24 | [profile.release]
25 | strip = true
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2011-2017 Marcin Kulik
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ht - headless terminal
2 |
3 | `ht` (short for *headless terminal*) is a command line program that wraps an arbitrary other binary (e.g. `bash`, `vim`, etc.) with a VT100 style terminal interface--i.e. a pseudoterminal client (PTY) plus terminal server--and allows easy programmatic access to the input and output of that terminal (via JSON over STDIN/STDOUT). `ht` is built in rust and works on MacOS and Linux.
4 |
5 |
6 |
7 |
8 | ## Use Cases & Motivation
9 |
10 | `ht` is useful for programmatically interacting with terminals, which is important for programs that depend heavily on the Terminal as UI. It is useful for testing and for getting AI agents to interact with terminals the way humans do.
11 |
12 | The original motiving use case was making terminals easy for LLMs to use. I was trying to use LLM agents for coding, and needed something like a **headless browser** but for terminals.
13 |
14 | Terminals are one of the oldest and most prolific UI frameworks in all of computing. And they are stateful so, for example, when you use an editor in your terminal, the terminal has to manage state about the cursor location. Without ht, an agent struggles to manage this state directly; with ht, an agent can just observe the terminal like a human does.
15 |
16 | ## Installing
17 | Download and use [the latest binary](https://github.com/andyk/ht/releases/latest) for your architecture.
18 |
19 | ## Building
20 |
21 | Building from source requires the [Rust](https://www.rust-lang.org/) compiler
22 | (1.74 or later), and the [Cargo package
23 | manager](https://doc.rust-lang.org/cargo/). If they are not available via your
24 | system package manager then use [rustup](https://rustup.rs/).
25 |
26 | To download the source code, build the binary, and install it in
27 | `$HOME/.cargo/bin` run:
28 |
29 | ```sh
30 | cargo install --git https://github.com/andyk/ht
31 | ```
32 |
33 | Then, ensure `$HOME/.cargo/bin` is in your shell's `$PATH`.
34 |
35 | Alternatively, you can manually download the source code and build the binary
36 | with:
37 |
38 | ```sh
39 | git clone https://github.com/andyk/ht
40 | cd ht
41 | cargo build --release
42 | ```
43 |
44 | This produces the binary in _release mode_ (`--release`) at
45 | `target/release/ht`. There are no other build artifacts so you can just
46 | copy the binary to a directory in your `$PATH`.
47 |
48 | ## Usage
49 |
50 | Run `ht` to start interactive bash shell running in a PTY (pseudo-terminal).
51 |
52 | To launch a different program (a different shell, another program) run `ht
53 | `. For example:
54 |
55 | - `ht fish` - starts fish shell
56 | - `ht nano` - starts nano editor
57 | - `ht nano /etc/fstab` - starts nano editor with /etc/fstab opened
58 |
59 | Another way to run a specific program, e.g. `nano`, is to launch `ht` without a
60 | command, i.e. use bash by default, and start nano from bash by sending `nano\r`
61 | ("nano" followed by "return" control character) to the process input. See [input
62 | command](#input) below.
63 |
64 | Default size of the virtual terminal window is 120x40 (cols by rows), which can
65 | be changed with `--size` argument. For example: `ht --size 80x24`. The window
66 | size can also be dynamically changed - see [resize command](#resize) below.
67 |
68 | Run `ht -h` or `ht --help` to see all available options.
69 |
70 | ## Live terminal preview
71 |
72 | ht comes with a built-in HTTP server which provides a handy live terminal preview page.
73 |
74 | To enable it, start ht with `-l` / `--listen` option. This will print the URL of
75 | the live preview.
76 |
77 | By default it listens on `127.0.0.1` and a system assigned, dynamic port. If you
78 | need it to bind to another interface, or a specific port, pass the address to
79 | the `-l` option, e.g. `-l 0.0.0.0:9999`.
80 |
81 | ## API
82 |
83 | ht provides 2 types of API: STDIO and WebSocket.
84 |
85 | The STDIO API allows control and introspection of the terminal using STDIN,
86 | STDOUT and STDERR.
87 |
88 | WebSocket API provides several endpoints for getting terminal updates in
89 | real-time. Websocket API is _not_ enabled by default, and requires starting the
90 | built-in HTTP server with `-l` / `--listen` option.
91 |
92 | ### STDIO API
93 |
94 | ht uses simple JSON-based protocol for sending commands to its STDIN. Each
95 | command must be sent on a separate line and be a JSON object having `"type"`
96 | field set to one of the supported commands (below).
97 |
98 | Some of the commands trigger [events](#events). ht may also internally trigger
99 | various events on its own. To subscribe to desired events use `--subscribe
100 | [,,...]` option when starting ht. This will print the
101 | events as they occur to ht's STDOUT, as JSON-encoded objects. For example, to
102 | subscribe to view snapshots (triggered by sending `takeSnapshot` command) use
103 | `--subscribe snapshot` option. See [events](#events) below for a list of
104 | available event types and their payloads.
105 |
106 | Diagnostic messages (notices, errors) are printed to STDERR.
107 |
108 | #### sendKeys
109 |
110 | `sendKeys` command allows sending keys to a process running in the virtual
111 | terminal as if the keys were pressed on a keyboard.
112 |
113 | ```json
114 | { "type": "sendKeys", "keys": ["nano", "Enter"] }
115 | { "type": "sendKeys", "keys": ["hello", "Enter", "world"] }
116 | { "type": "sendKeys", "keys": ["^x", "n"] }
117 | ```
118 |
119 | Each element of the `keys` array can be either a key name or an arbitrary text.
120 | If a key is not matched by any supported key name then the text is sent to the
121 | process as is, i.e. like when using the `input` command.
122 |
123 | The key and modifier specifications were inspired by
124 | [tmux](https://github.com/tmux/tmux/wiki/Modifier-Keys).
125 |
126 | The following key specifications are currently supported:
127 |
128 | - `Enter`
129 | - `Space`
130 | - `Escape` or `^[` or `C-[`
131 | - `Tab`
132 | - `Left` - left arrow key
133 | - `Right` - right arrow key
134 | - `Up` - up arrow key
135 | - `Down` - down arrow key
136 | - `Home`
137 | - `End`
138 | - `PageUp`
139 | - `PageDown`
140 | - `F1` to `F12`
141 |
142 | Modifier keys are supported by prepending a key with one of the prefixes:
143 |
144 | - `^` - control - e.g. `^c` means Ctrl + C
145 | - `C-` - control - e.g. `C-c` means Ctrl + C
146 | - `S-` - shift - e.g. `S-F6` means Shift + F6
147 | - `A-` - alt/option - e.g. `A-Home` means Alt + Home
148 |
149 | Modifiers can be combined (for arrow keys only at the moment), so combinations
150 | such as `S-A-Up` or `C-S-Left` are possible.
151 |
152 | `C-` control modifier notation can be used with ASCII letters (both lower and
153 | upper case are supported) and most special key names. The caret control notation
154 | (`^`) may only be used with ASCII letters, not with special keys.
155 |
156 | Shift modifier can be used with special key names only, such as `Left`, `PageUp`
157 | etc. For text characters, instead of specifying e.g. `S-a` just use upper case
158 | `A`.
159 |
160 | Alt modifier can be used with any Unicode character and most special key names.
161 |
162 | This command doesn't trigger any event.
163 |
164 | #### input
165 |
166 | `input` command allows sending arbitrary raw input to a process running in the
167 | virtual terminal.
168 |
169 | ```json
170 | { "type": "input", "payload": "ls\r" }
171 | ```
172 |
173 | In most cases it's easier and recommended to use the `sendKeys` command instead.
174 |
175 | Use the `input` command if you don't want any special input processing, i.e. no
176 | mapping of key names to their respective control sequences.
177 |
178 | For example, to send Ctrl-C shortcut you must use `"\u0003"` (0x03) as the
179 | payload:
180 |
181 | ```json
182 | { "type": "input", "payload": "\u0003" }
183 | ```
184 |
185 | This command doesn't trigger any event.
186 |
187 | #### takeSnapshot
188 |
189 | `takeSnapshot` command allows taking a textual snapshot of the the terminal view.
190 |
191 | ```json
192 | { "type": "takeSnapshot" }
193 | ```
194 |
195 | This command triggers `snapshot` event.
196 |
197 | #### resize
198 |
199 | `resize` command allows resizing the virtual terminal window dynamically by
200 | specifying new width (`cols`) and height (`rows`).
201 |
202 | ```json
203 | { "type": "resize", "cols": 80, "rows": 24 }
204 | ```
205 |
206 | This command triggers `resize` event.
207 |
208 | ### WebSocket API
209 |
210 | The WebSocket API currently provides 2 endpoints:
211 |
212 | #### `/ws/events`
213 |
214 | This endpoint allows the client to subscribe to events that happen in ht.
215 |
216 | Query param `sub` should be set to a comma-separated list of desired events.
217 | E.g. `/ws/events?sub=init,snapshot`.
218 |
219 | Events are delivered as JSON encoded strings, using WebSocket text message type.
220 |
221 | See [events](#events) section below for the description of all available events.
222 |
223 | #### `/ws/alis`
224 |
225 | This endpoint implements JSON flavor of [asciinema live stream
226 | protocol](https://github.com/asciinema/asciinema-player/blob/develop/src/driver/websocket.js),
227 | therefore allows pointing asciinema player directly to ht to get a real-time
228 | terminal preview. This endpoint is used by the live terminal preview page
229 | mentioned above.
230 |
231 | ### Events
232 |
233 | The events emitted to STDOUT and via `/ws/events` WebSocket endpoint are
234 | identical, i.e. they are JSON-encoded objects with the same fields and payloads.
235 |
236 | Every event contains 2 top-level fields:
237 |
238 | - `type` - type of event,
239 | - `data` - associated data, specific to each event type.
240 |
241 | The following event types are currently available:
242 |
243 | #### `init`
244 |
245 | Same as `snapshot` event (see below) but sent only once, as the first event
246 | after ht's start (when sent to STDOUT) and upon establishing of WebSocket
247 | connection.
248 |
249 | #### `output`
250 |
251 | Terminal output. Sent when an application (e.g. shell) running under ht prints
252 | something to the terminal.
253 |
254 | Event data is an object with the following fields:
255 |
256 | - `seq` - a raw sequence of characters written to a terminal, potentially including control sequences (colors, cursor positioning, etc.)
257 |
258 | #### `resize`
259 |
260 | Terminal resize. Send when the terminal is resized with the `resize` command.
261 |
262 | Event data is an object with the following fields:
263 |
264 | - `cols` - current terminal width, number of columns
265 | - `rows` - current terminal height, number of rows
266 |
267 | #### `snapshot`
268 |
269 | Terminal window snapshot. Sent when the terminal snapshot is taken with the
270 | `takeSnapshot` command.
271 |
272 | Event data is an object with the following fields:
273 |
274 | - `cols` - current terminal width, number of columns
275 | - `rows` - current terminal height, number of rows
276 | - `text` - plain text snapshot as multi-line string, where each line represents a terminal row
277 | - `seq` - a raw sequence of characters, which when printed to a blank terminal puts it in the same state as [ht's virtual terminal](https://github.com/asciinema/avt)
278 |
279 | ## Testing on command line
280 |
281 | ht is aimed at programmatic use given its JSON-based API, however one can play
282 | with it by just launching it in a normal desktop terminal emulator and typing in
283 | JSON-encoded commands from keyboard and observing the output on STDOUT.
284 |
285 | [rlwrap](https://github.com/hanslub42/rlwrap) can be used to wrap STDIN in a
286 | readline based editable prompt, which also provides history (up/down arrows).
287 |
288 | To use `rlwrap` with `ht`:
289 |
290 | ```sh
291 | rlwrap ht [ht-args...]
292 | ```
293 |
294 | ## Python and Typescript libs
295 |
296 | Here are some experimental versions of a simple Python and Typescript libraries that wrap `ht`: [htlib.py](https://github.com/andyk/headlong/blob/24e9e5f37b79b3a667774eefa3a724b59b059775/packages/env/htlib.py) and a [htlib.ts](https://github.com/andyk/headlong/blob/24e9e5f37b79b3a667774eefa3a724b59b059775/packages/env/htlib.ts).
297 |
298 | TODO: either pull those into this repo or fork them into their own `htlib` repo.
299 |
300 | ## Possible future work
301 |
302 | * update the interface to return the view with additional color and style information (text color, background, bold/italic/etc) also in a simple JSON format (so no dealing with color-related escape sequence either), and the frontend could render this using HTML (e.g. with styled pre/span tags, similar to how asciinema-player does it) or with SVG.
303 | * support subscribing to view updates, to avoid needing to poll (see [issue #9](https://github.com/andyk/ht/issues/9))
304 | * native integration with asciinema for recording terminal sessions (see [issue #8](https://github.com/andyk/ht/issues/8))
305 |
306 | ## Alternatives and related projects
307 | [`expect`](https://core.tcl-lang.org/expect/index) is an old related tool that let's you `spawn` an arbitrary binary and then `send` input to it and specify what output you `expect` it to generate next.
308 |
309 | Also, note that if there exists an explicit API to achieve your given task (e.g. a library that comes with the tool you're targeting), it will probably be less bug prone/finicky to use the API directly rather than working witht your tool through `ht`.
310 |
311 | See also [this hackernews discussion](https://news.ycombinator.com/item?id=40552257) where a bunch of other tools were discussed!
312 |
313 | ## Design doc
314 |
315 | Here is [the original design doc](https://docs.google.com/document/d/1L1prpWos3gIYTkfCgeZ2hLScypkA73WJ9KxME5NNbNk/edit) we used to drive the project development.
316 |
317 | ## License
318 |
319 | All code is licensed under the Apache License, Version 2.0. See LICENSE file for
320 | details.
321 |
--------------------------------------------------------------------------------
/assets/asciinema-player.css:
--------------------------------------------------------------------------------
1 | div.ap-wrapper {
2 | outline: none;
3 | height: 100%;
4 | display: flex;
5 | justify-content: center;
6 | }
7 | div.ap-wrapper .title-bar {
8 | display: none;
9 | top: -78px;
10 | transition: top 0.15s linear;
11 | position: absolute;
12 | left: 0;
13 | right: 0;
14 | box-sizing: content-box;
15 | font-size: 20px;
16 | line-height: 1em;
17 | padding: 15px;
18 | font-family: sans-serif;
19 | color: white;
20 | background-color: rgba(0, 0, 0, 0.8);
21 | }
22 | div.ap-wrapper .title-bar img {
23 | vertical-align: middle;
24 | height: 48px;
25 | margin-right: 16px;
26 | }
27 | div.ap-wrapper .title-bar a {
28 | color: white;
29 | text-decoration: underline;
30 | }
31 | div.ap-wrapper .title-bar a:hover {
32 | text-decoration: none;
33 | }
34 | div.ap-wrapper:fullscreen {
35 | background-color: #000;
36 | width: 100%;
37 | align-items: center;
38 | }
39 | div.ap-wrapper:fullscreen .title-bar {
40 | display: initial;
41 | }
42 | div.ap-wrapper:fullscreen.hud .title-bar {
43 | top: 0;
44 | }
45 | div.ap-wrapper div.ap-player {
46 | text-align: left;
47 | display: inline-block;
48 | padding: 0px;
49 | position: relative;
50 | box-sizing: content-box;
51 | overflow: hidden;
52 | max-width: 100%;
53 | border-radius: 4px;
54 | font-size: 15px;
55 | background-color: var(--term-color-background);
56 | }
57 | .ap-player {
58 | --term-color-foreground: #ffffff;
59 | --term-color-background: #000000;
60 | --term-color-0: var(--term-color-foreground);
61 | --term-color-1: var(--term-color-foreground);
62 | --term-color-2: var(--term-color-foreground);
63 | --term-color-3: var(--term-color-foreground);
64 | --term-color-4: var(--term-color-foreground);
65 | --term-color-5: var(--term-color-foreground);
66 | --term-color-6: var(--term-color-foreground);
67 | --term-color-7: var(--term-color-foreground);
68 | --term-color-8: var(--term-color-0);
69 | --term-color-9: var(--term-color-1);
70 | --term-color-10: var(--term-color-2);
71 | --term-color-11: var(--term-color-3);
72 | --term-color-12: var(--term-color-4);
73 | --term-color-13: var(--term-color-5);
74 | --term-color-14: var(--term-color-6);
75 | --term-color-15: var(--term-color-7);
76 | }
77 | .ap-player .fg-0 {
78 | --fg: var(--term-color-0);
79 | }
80 | .ap-player .bg-0 {
81 | --bg: var(--term-color-0);
82 | }
83 | .ap-player .fg-1 {
84 | --fg: var(--term-color-1);
85 | }
86 | .ap-player .bg-1 {
87 | --bg: var(--term-color-1);
88 | }
89 | .ap-player .fg-2 {
90 | --fg: var(--term-color-2);
91 | }
92 | .ap-player .bg-2 {
93 | --bg: var(--term-color-2);
94 | }
95 | .ap-player .fg-3 {
96 | --fg: var(--term-color-3);
97 | }
98 | .ap-player .bg-3 {
99 | --bg: var(--term-color-3);
100 | }
101 | .ap-player .fg-4 {
102 | --fg: var(--term-color-4);
103 | }
104 | .ap-player .bg-4 {
105 | --bg: var(--term-color-4);
106 | }
107 | .ap-player .fg-5 {
108 | --fg: var(--term-color-5);
109 | }
110 | .ap-player .bg-5 {
111 | --bg: var(--term-color-5);
112 | }
113 | .ap-player .fg-6 {
114 | --fg: var(--term-color-6);
115 | }
116 | .ap-player .bg-6 {
117 | --bg: var(--term-color-6);
118 | }
119 | .ap-player .fg-7 {
120 | --fg: var(--term-color-7);
121 | }
122 | .ap-player .bg-7 {
123 | --bg: var(--term-color-7);
124 | }
125 | .ap-player .fg-8 {
126 | --fg: var(--term-color-8);
127 | }
128 | .ap-player .bg-8 {
129 | --bg: var(--term-color-8);
130 | }
131 | .ap-player .fg-9 {
132 | --fg: var(--term-color-9);
133 | }
134 | .ap-player .bg-9 {
135 | --bg: var(--term-color-9);
136 | }
137 | .ap-player .fg-10 {
138 | --fg: var(--term-color-10);
139 | }
140 | .ap-player .bg-10 {
141 | --bg: var(--term-color-10);
142 | }
143 | .ap-player .fg-11 {
144 | --fg: var(--term-color-11);
145 | }
146 | .ap-player .bg-11 {
147 | --bg: var(--term-color-11);
148 | }
149 | .ap-player .fg-12 {
150 | --fg: var(--term-color-12);
151 | }
152 | .ap-player .bg-12 {
153 | --bg: var(--term-color-12);
154 | }
155 | .ap-player .fg-13 {
156 | --fg: var(--term-color-13);
157 | }
158 | .ap-player .bg-13 {
159 | --bg: var(--term-color-13);
160 | }
161 | .ap-player .fg-14 {
162 | --fg: var(--term-color-14);
163 | }
164 | .ap-player .bg-14 {
165 | --bg: var(--term-color-14);
166 | }
167 | .ap-player .fg-15 {
168 | --fg: var(--term-color-15);
169 | }
170 | .ap-player .bg-15 {
171 | --bg: var(--term-color-15);
172 | }
173 | .ap-player .fg-8,
174 | .ap-player .fg-9,
175 | .ap-player .fg-10,
176 | .ap-player .fg-11,
177 | .ap-player .fg-12,
178 | .ap-player .fg-13,
179 | .ap-player .fg-14,
180 | .ap-player .fg-15 {
181 | font-weight: bold;
182 | }
183 | pre.ap-terminal {
184 | box-sizing: content-box;
185 | overflow: hidden;
186 | padding: 0;
187 | margin: 0px;
188 | display: block;
189 | white-space: pre;
190 | word-wrap: normal;
191 | word-break: normal;
192 | border-radius: 0;
193 | border-style: solid;
194 | cursor: text;
195 | border-width: 0.75em;
196 | color: var(--term-color-foreground);
197 | background-color: var(--term-color-background);
198 | border-color: var(--term-color-background);
199 | outline: none;
200 | line-height: var(--term-line-height);
201 | font-family: Consolas, Menlo, 'Bitstream Vera Sans Mono', monospace, 'Powerline Symbols';
202 | font-variant-ligatures: none;
203 | }
204 | pre.ap-terminal .ap-line {
205 | letter-spacing: normal;
206 | overflow: hidden;
207 | }
208 | pre.ap-terminal .ap-line span {
209 | padding: 0;
210 | display: inline-block;
211 | height: 100%;
212 | }
213 | pre.ap-terminal .ap-line {
214 | display: block;
215 | width: 100%;
216 | height: var(--term-line-height);
217 | position: relative;
218 | }
219 | pre.ap-terminal .ap-line span {
220 | position: absolute;
221 | left: calc(100% * var(--offset) / var(--term-cols));
222 | color: var(--fg);
223 | background-color: var(--bg);
224 | }
225 | pre.ap-terminal .ap-line .ap-inverse {
226 | color: var(--bg);
227 | background-color: var(--fg);
228 | }
229 | pre.ap-terminal .ap-line .cp-2580 {
230 | border-top: calc(0.5 * var(--term-line-height)) solid var(--fg);
231 | box-sizing: border-box;
232 | }
233 | pre.ap-terminal .ap-line .cp-2581 {
234 | border-bottom: calc(0.125 * var(--term-line-height)) solid var(--fg);
235 | box-sizing: border-box;
236 | }
237 | pre.ap-terminal .ap-line .cp-2582 {
238 | border-bottom: calc(0.25 * var(--term-line-height)) solid var(--fg);
239 | box-sizing: border-box;
240 | }
241 | pre.ap-terminal .ap-line .cp-2583 {
242 | border-bottom: calc(0.375 * var(--term-line-height)) solid var(--fg);
243 | box-sizing: border-box;
244 | }
245 | pre.ap-terminal .ap-line .cp-2584 {
246 | border-bottom: calc(0.5 * var(--term-line-height)) solid var(--fg);
247 | box-sizing: border-box;
248 | }
249 | pre.ap-terminal .ap-line .cp-2585 {
250 | border-bottom: calc(0.625 * var(--term-line-height)) solid var(--fg);
251 | box-sizing: border-box;
252 | }
253 | pre.ap-terminal .ap-line .cp-2586 {
254 | border-bottom: calc(0.75 * var(--term-line-height)) solid var(--fg);
255 | box-sizing: border-box;
256 | }
257 | pre.ap-terminal .ap-line .cp-2587 {
258 | border-bottom: calc(0.875 * var(--term-line-height)) solid var(--fg);
259 | box-sizing: border-box;
260 | }
261 | pre.ap-terminal .ap-line .cp-2588 {
262 | background-color: var(--fg);
263 | }
264 | pre.ap-terminal .ap-line .cp-2589 {
265 | border-left: 0.875ch solid var(--fg);
266 | box-sizing: border-box;
267 | }
268 | pre.ap-terminal .ap-line .cp-258a {
269 | border-left: 0.75ch solid var(--fg);
270 | box-sizing: border-box;
271 | }
272 | pre.ap-terminal .ap-line .cp-258b {
273 | border-left: 0.625ch solid var(--fg);
274 | box-sizing: border-box;
275 | }
276 | pre.ap-terminal .ap-line .cp-258c {
277 | border-left: 0.5ch solid var(--fg);
278 | box-sizing: border-box;
279 | }
280 | pre.ap-terminal .ap-line .cp-258d {
281 | border-left: 0.375ch solid var(--fg);
282 | box-sizing: border-box;
283 | }
284 | pre.ap-terminal .ap-line .cp-258e {
285 | border-left: 0.25ch solid var(--fg);
286 | box-sizing: border-box;
287 | }
288 | pre.ap-terminal .ap-line .cp-258f {
289 | border-left: 0.125ch solid var(--fg);
290 | box-sizing: border-box;
291 | }
292 | pre.ap-terminal .ap-line .cp-2590 {
293 | border-right: 0.5ch solid var(--fg);
294 | box-sizing: border-box;
295 | }
296 | pre.ap-terminal .ap-line .cp-2591 {
297 | background-color: color-mix(in srgb, var(--fg) 25%, var(--bg));
298 | }
299 | pre.ap-terminal .ap-line .cp-2592 {
300 | background-color: color-mix(in srgb, var(--fg) 50%, var(--bg));
301 | }
302 | pre.ap-terminal .ap-line .cp-2593 {
303 | background-color: color-mix(in srgb, var(--fg) 75%, var(--bg));
304 | }
305 | pre.ap-terminal .ap-line .cp-2594 {
306 | border-top: calc(0.125 * var(--term-line-height)) solid var(--fg);
307 | box-sizing: border-box;
308 | }
309 | pre.ap-terminal .ap-line .cp-2595 {
310 | border-right: 0.125ch solid var(--fg);
311 | box-sizing: border-box;
312 | }
313 | pre.ap-terminal .ap-line .cp-2596 {
314 | border-right: 0.5ch solid var(--bg);
315 | border-top: calc(0.5 * var(--term-line-height)) solid var(--bg);
316 | background-color: var(--fg);
317 | box-sizing: border-box;
318 | }
319 | pre.ap-terminal .ap-line .cp-2597 {
320 | border-left: 0.5ch solid var(--bg);
321 | border-top: calc(0.5 * var(--term-line-height)) solid var(--bg);
322 | background-color: var(--fg);
323 | box-sizing: border-box;
324 | }
325 | pre.ap-terminal .ap-line .cp-2598 {
326 | border-right: 0.5ch solid var(--bg);
327 | border-bottom: calc(0.5 * var(--term-line-height)) solid var(--bg);
328 | background-color: var(--fg);
329 | box-sizing: border-box;
330 | }
331 | pre.ap-terminal .ap-line .cp-2599 {
332 | border-left: 0.5ch solid var(--fg);
333 | border-bottom: calc(0.5 * var(--term-line-height)) solid var(--fg);
334 | box-sizing: border-box;
335 | }
336 | pre.ap-terminal .ap-line .cp-259a {
337 | box-sizing: border-box;
338 | }
339 | pre.ap-terminal .ap-line .cp-259a::before,
340 | pre.ap-terminal .ap-line .cp-259a::after {
341 | content: '';
342 | position: absolute;
343 | width: 0.5ch;
344 | height: calc(0.5 * var(--term-line-height));
345 | background-color: var(--fg);
346 | }
347 | pre.ap-terminal .ap-line .cp-259a::before {
348 | top: 0;
349 | left: 0;
350 | }
351 | pre.ap-terminal .ap-line .cp-259a::after {
352 | bottom: 0;
353 | right: 0;
354 | }
355 | pre.ap-terminal .ap-line .cp-259b {
356 | border-left: 0.5ch solid var(--fg);
357 | border-top: calc(0.5 * var(--term-line-height)) solid var(--fg);
358 | box-sizing: border-box;
359 | }
360 | pre.ap-terminal .ap-line .cp-259c {
361 | border-right: 0.5ch solid var(--fg);
362 | border-top: calc(0.5 * var(--term-line-height)) solid var(--fg);
363 | box-sizing: border-box;
364 | }
365 | pre.ap-terminal .ap-line .cp-259d {
366 | border-left: 0.5ch solid var(--bg);
367 | border-bottom: calc(0.5 * var(--term-line-height)) solid var(--bg);
368 | background-color: var(--fg);
369 | box-sizing: border-box;
370 | }
371 | pre.ap-terminal .ap-line .cp-259e {
372 | box-sizing: border-box;
373 | }
374 | pre.ap-terminal .ap-line .cp-259e::before,
375 | pre.ap-terminal .ap-line .cp-259e::after {
376 | content: '';
377 | position: absolute;
378 | width: 0.5ch;
379 | height: calc(0.5 * var(--term-line-height));
380 | background-color: var(--fg);
381 | }
382 | pre.ap-terminal .ap-line .cp-259e::before {
383 | top: 0;
384 | right: 0;
385 | }
386 | pre.ap-terminal .ap-line .cp-259e::after {
387 | bottom: 0;
388 | left: 0;
389 | }
390 | pre.ap-terminal .ap-line .cp-259f {
391 | border-right: 0.5ch solid var(--fg);
392 | border-bottom: calc(0.5 * var(--term-line-height)) solid var(--fg);
393 | box-sizing: border-box;
394 | }
395 | pre.ap-terminal .ap-line .cp-e0b0 {
396 | border-left: 1ch solid var(--fg);
397 | border-top: calc(0.5 * var(--term-line-height)) solid transparent;
398 | border-bottom: calc(0.5 * var(--term-line-height)) solid transparent;
399 | box-sizing: border-box;
400 | }
401 | pre.ap-terminal .ap-line .cp-e0b2 {
402 | border-right: 1ch solid var(--fg);
403 | border-top: calc(0.5 * var(--term-line-height)) solid transparent;
404 | border-bottom: calc(0.5 * var(--term-line-height)) solid transparent;
405 | box-sizing: border-box;
406 | }
407 | pre.ap-terminal.ap-cursor-on .ap-line .ap-cursor {
408 | color: var(--bg);
409 | background-color: var(--fg);
410 | border-radius: 0.05em;
411 | }
412 | pre.ap-terminal.ap-cursor-on .ap-line .ap-cursor.ap-inverse {
413 | color: var(--fg);
414 | background-color: var(--bg);
415 | }
416 | pre.ap-terminal:not(.ap-blink) .ap-line .ap-blink {
417 | color: transparent;
418 | }
419 | pre.ap-terminal .ap-bright {
420 | font-weight: bold;
421 | }
422 | pre.ap-terminal .ap-faint {
423 | opacity: 0.5;
424 | }
425 | pre.ap-terminal .ap-underline {
426 | text-decoration: underline;
427 | }
428 | pre.ap-terminal .ap-italic {
429 | font-style: italic;
430 | }
431 | pre.ap-terminal .ap-strikethrough {
432 | text-decoration: line-through;
433 | }
434 | .ap-line span {
435 | --fg: var(--term-color-foreground);
436 | --bg: var(--term-color-background);
437 | }
438 | div.ap-player div.ap-control-bar {
439 | width: 100%;
440 | height: 32px;
441 | display: flex;
442 | justify-content: space-between;
443 | align-items: stretch;
444 | color: var(--term-color-foreground);
445 | box-sizing: content-box;
446 | line-height: 1;
447 | position: absolute;
448 | bottom: 0;
449 | left: 0;
450 | opacity: 0;
451 | transition: opacity 0.15s linear;
452 | user-select: none;
453 | border-top: 2px solid color-mix(in oklab, black 33%, var(--term-color-background));
454 | z-index: 30;
455 | }
456 | div.ap-player div.ap-control-bar * {
457 | box-sizing: inherit;
458 | }
459 | div.ap-control-bar svg.ap-icon path {
460 | fill: var(--term-color-foreground);
461 | }
462 | div.ap-control-bar span.ap-playback-button {
463 | display: flex;
464 | flex: 0 0 auto;
465 | cursor: pointer;
466 | height: 12px;
467 | width: 12px;
468 | padding: 10px;
469 | }
470 | div.ap-control-bar span.ap-playback-button svg {
471 | height: 12px;
472 | width: 12px;
473 | }
474 | div.ap-control-bar span.ap-timer {
475 | display: flex;
476 | flex: 0 0 auto;
477 | min-width: 50px;
478 | margin: 0 10px;
479 | height: 100%;
480 | text-align: center;
481 | font-size: 13px;
482 | line-height: 100%;
483 | cursor: default;
484 | }
485 | div.ap-control-bar span.ap-timer span {
486 | font-family: Consolas, Menlo, 'Bitstream Vera Sans Mono', monospace;
487 | font-size: inherit;
488 | font-weight: 600;
489 | margin: auto;
490 | }
491 | div.ap-control-bar span.ap-timer .ap-time-remaining {
492 | display: none;
493 | }
494 | div.ap-control-bar span.ap-timer:hover .ap-time-elapsed {
495 | display: none;
496 | }
497 | div.ap-control-bar span.ap-timer:hover .ap-time-remaining {
498 | display: flex;
499 | }
500 | div.ap-control-bar .ap-progressbar {
501 | display: block;
502 | flex: 1 1 auto;
503 | height: 100%;
504 | padding: 0 10px;
505 | }
506 | div.ap-control-bar .ap-progressbar .ap-bar {
507 | display: block;
508 | position: relative;
509 | cursor: default;
510 | height: 100%;
511 | font-size: 0;
512 | }
513 | div.ap-control-bar .ap-progressbar .ap-bar .ap-gutter {
514 | display: block;
515 | position: absolute;
516 | top: 15px;
517 | left: 0;
518 | right: 0;
519 | height: 3px;
520 | }
521 | div.ap-control-bar .ap-progressbar .ap-bar .ap-gutter-empty {
522 | background-color: color-mix(in oklab, var(--term-color-foreground) 20%, var(--term-color-background));
523 | }
524 | div.ap-control-bar .ap-progressbar .ap-bar .ap-gutter-full {
525 | width: 100%;
526 | transform-origin: left center;
527 | background-color: var(--term-color-foreground);
528 | border-radius: 3px;
529 | }
530 | div.ap-control-bar.ap-seekable .ap-progressbar .ap-bar {
531 | cursor: pointer;
532 | }
533 | div.ap-control-bar .ap-fullscreen-button {
534 | display: block;
535 | flex: 0 0 auto;
536 | width: 14px;
537 | height: 14px;
538 | padding: 9px;
539 | cursor: pointer;
540 | position: relative;
541 | }
542 | div.ap-control-bar .ap-fullscreen-button svg {
543 | width: 14px;
544 | height: 14px;
545 | }
546 | div.ap-control-bar .ap-fullscreen-button svg.ap-icon-fullscreen-on {
547 | display: inline;
548 | }
549 | div.ap-control-bar .ap-fullscreen-button svg.ap-icon-fullscreen-off {
550 | display: none;
551 | }
552 | div.ap-control-bar .ap-fullscreen-button .ap-tooltip {
553 | right: 5px;
554 | left: initial;
555 | transform: none;
556 | }
557 | div.ap-wrapper.ap-hud .ap-control-bar {
558 | opacity: 1;
559 | }
560 | div.ap-wrapper:fullscreen .ap-fullscreen-button svg.ap-icon-fullscreen-on {
561 | display: none;
562 | }
563 | div.ap-wrapper:fullscreen .ap-fullscreen-button svg.ap-icon-fullscreen-off {
564 | display: inline;
565 | }
566 | span.ap-progressbar span.ap-marker-container {
567 | display: block;
568 | top: 0;
569 | bottom: 0;
570 | width: 21px;
571 | position: absolute;
572 | margin-left: -10px;
573 | }
574 | span.ap-marker-container span.ap-marker {
575 | display: block;
576 | top: 13px;
577 | bottom: 12px;
578 | left: 7px;
579 | right: 7px;
580 | background-color: color-mix(in oklab, var(--term-color-foreground) 33%, var(--term-color-background));
581 | position: absolute;
582 | transition: top 0.1s, bottom 0.1s, left 0.1s, right 0.1s, background-color 0.1s;
583 | border-radius: 50%;
584 | }
585 | span.ap-marker-container span.ap-marker.ap-marker-past {
586 | background-color: var(--term-color-foreground);
587 | }
588 | span.ap-marker-container span.ap-marker:hover,
589 | span.ap-marker-container:hover span.ap-marker {
590 | background-color: var(--term-color-foreground);
591 | top: 11px;
592 | bottom: 10px;
593 | left: 5px;
594 | right: 5px;
595 | }
596 | .ap-tooltip-container span.ap-tooltip {
597 | visibility: hidden;
598 | background-color: var(--term-color-foreground);
599 | color: var(--term-color-background);
600 | font-family: Consolas, Menlo, 'Bitstream Vera Sans Mono', monospace;
601 | font-weight: bold;
602 | text-align: center;
603 | padding: 0 0.5em;
604 | border-radius: 4px;
605 | position: absolute;
606 | z-index: 1;
607 | white-space: nowrap;
608 | /* Prevents the text from wrapping and makes sure the tooltip width adapts to the text length */
609 | font-size: 13px;
610 | line-height: 2em;
611 | bottom: 100%;
612 | left: 50%;
613 | transform: translateX(-50%);
614 | }
615 | .ap-tooltip-container:hover span.ap-tooltip {
616 | visibility: visible;
617 | }
618 | .ap-player .ap-overlay {
619 | z-index: 10;
620 | background-repeat: no-repeat;
621 | background-position: center;
622 | position: absolute;
623 | top: 0;
624 | left: 0;
625 | right: 0;
626 | bottom: 0;
627 | display: flex;
628 | justify-content: center;
629 | align-items: center;
630 | }
631 | .ap-player .ap-overlay-start {
632 | cursor: pointer;
633 | }
634 | .ap-player .ap-overlay-start .ap-play-button {
635 | font-size: 0px;
636 | position: absolute;
637 | left: 0;
638 | top: 0;
639 | right: 0;
640 | bottom: 0;
641 | text-align: center;
642 | color: white;
643 | height: 80px;
644 | max-height: 66%;
645 | margin: auto;
646 | }
647 | .ap-player .ap-overlay-start .ap-play-button div {
648 | height: 100%;
649 | }
650 | .ap-player .ap-overlay-start .ap-play-button div span {
651 | height: 100%;
652 | display: block;
653 | }
654 | .ap-player .ap-overlay-start .ap-play-button div span svg {
655 | height: 100%;
656 | }
657 | .ap-player .ap-overlay-start .ap-play-button svg {
658 | filter: drop-shadow(0px 0px 5px rgba(0, 0, 0, 0.4));
659 | }
660 | .ap-player .ap-overlay-loading .ap-loader {
661 | width: 48px;
662 | height: 48px;
663 | border-radius: 50%;
664 | display: inline-block;
665 | position: relative;
666 | border: 10px solid;
667 | border-color: rgba(255, 255, 255, 0.3) rgba(255, 255, 255, 0.5) rgba(255, 255, 255, 0.7) #ffffff;
668 | border-color: color-mix(in srgb, var(--term-color-foreground) 30%, var(--term-color-background)) color-mix(in srgb, var(--term-color-foreground) 50%, var(--term-color-background)) color-mix(in srgb, var(--term-color-foreground) 70%, var(--term-color-background)) color-mix(in srgb, var(--term-color-foreground) 100%, var(--term-color-background));
669 | box-sizing: border-box;
670 | animation: ap-loader-rotation 1s linear infinite;
671 | }
672 | .ap-player .ap-overlay-info {
673 | background-color: var(--term-color-background);
674 | }
675 | .ap-player .ap-overlay-info span {
676 | font-family: Consolas, Menlo, 'Bitstream Vera Sans Mono', monospace, 'Powerline Symbols';
677 | font-variant-ligatures: none;
678 | font-size: 2em;
679 | color: var(--term-color-foreground);
680 | }
681 | .ap-player .ap-overlay-info span .ap-line {
682 | letter-spacing: normal;
683 | overflow: hidden;
684 | }
685 | .ap-player .ap-overlay-info span .ap-line span {
686 | padding: 0;
687 | display: inline-block;
688 | height: 100%;
689 | }
690 | .ap-player .ap-overlay-help {
691 | background-color: rgba(0, 0, 0, 0.8);
692 | container-type: inline-size;
693 | }
694 | .ap-player .ap-overlay-help > div {
695 | font-family: Consolas, Menlo, 'Bitstream Vera Sans Mono', monospace, 'Powerline Symbols';
696 | font-variant-ligatures: none;
697 | max-width: 85%;
698 | max-height: 85%;
699 | font-size: 18px;
700 | color: var(--term-color-foreground);
701 | background-color: var(--term-color-background);
702 | border-radius: 6px;
703 | box-sizing: border-box;
704 | margin-bottom: 32px;
705 | }
706 | .ap-player .ap-overlay-help > div .ap-line {
707 | letter-spacing: normal;
708 | overflow: hidden;
709 | }
710 | .ap-player .ap-overlay-help > div .ap-line span {
711 | padding: 0;
712 | display: inline-block;
713 | height: 100%;
714 | }
715 | .ap-player .ap-overlay-help > div div {
716 | padding: calc(min(4cqw, 40px));
717 | font-size: calc(min(1.9cqw, 18px));
718 | }
719 | .ap-player .ap-overlay-help > div div p {
720 | font-weight: bold;
721 | margin: 0 0 2em 0;
722 | }
723 | .ap-player .ap-overlay-help > div div ul {
724 | list-style: none;
725 | padding: 0;
726 | }
727 | .ap-player .ap-overlay-help > div div ul li {
728 | margin: 0 0 0.75em 0;
729 | }
730 | .ap-player .ap-overlay-help > div div kbd {
731 | color: var(--term-color-background);
732 | background-color: var(--term-color-foreground);
733 | padding: 0.2em 0.5em;
734 | border-radius: 0.2em;
735 | font-family: inherit;
736 | font-size: 0.85em;
737 | border: none;
738 | margin: 0;
739 | }
740 | .ap-player .ap-overlay-error span {
741 | font-size: 8em;
742 | }
743 | @keyframes ap-loader-rotation {
744 | 0% {
745 | transform: rotate(0deg);
746 | }
747 | 100% {
748 | transform: rotate(360deg);
749 | }
750 | }
751 | .ap-terminal .fg-16 {
752 | --fg: #000000;
753 | }
754 | .ap-terminal .bg-16 {
755 | --bg: #000000;
756 | }
757 | .ap-terminal .fg-17 {
758 | --fg: #00005f;
759 | }
760 | .ap-terminal .bg-17 {
761 | --bg: #00005f;
762 | }
763 | .ap-terminal .fg-18 {
764 | --fg: #000087;
765 | }
766 | .ap-terminal .bg-18 {
767 | --bg: #000087;
768 | }
769 | .ap-terminal .fg-19 {
770 | --fg: #0000af;
771 | }
772 | .ap-terminal .bg-19 {
773 | --bg: #0000af;
774 | }
775 | .ap-terminal .fg-20 {
776 | --fg: #0000d7;
777 | }
778 | .ap-terminal .bg-20 {
779 | --bg: #0000d7;
780 | }
781 | .ap-terminal .fg-21 {
782 | --fg: #0000ff;
783 | }
784 | .ap-terminal .bg-21 {
785 | --bg: #0000ff;
786 | }
787 | .ap-terminal .fg-22 {
788 | --fg: #005f00;
789 | }
790 | .ap-terminal .bg-22 {
791 | --bg: #005f00;
792 | }
793 | .ap-terminal .fg-23 {
794 | --fg: #005f5f;
795 | }
796 | .ap-terminal .bg-23 {
797 | --bg: #005f5f;
798 | }
799 | .ap-terminal .fg-24 {
800 | --fg: #005f87;
801 | }
802 | .ap-terminal .bg-24 {
803 | --bg: #005f87;
804 | }
805 | .ap-terminal .fg-25 {
806 | --fg: #005faf;
807 | }
808 | .ap-terminal .bg-25 {
809 | --bg: #005faf;
810 | }
811 | .ap-terminal .fg-26 {
812 | --fg: #005fd7;
813 | }
814 | .ap-terminal .bg-26 {
815 | --bg: #005fd7;
816 | }
817 | .ap-terminal .fg-27 {
818 | --fg: #005fff;
819 | }
820 | .ap-terminal .bg-27 {
821 | --bg: #005fff;
822 | }
823 | .ap-terminal .fg-28 {
824 | --fg: #008700;
825 | }
826 | .ap-terminal .bg-28 {
827 | --bg: #008700;
828 | }
829 | .ap-terminal .fg-29 {
830 | --fg: #00875f;
831 | }
832 | .ap-terminal .bg-29 {
833 | --bg: #00875f;
834 | }
835 | .ap-terminal .fg-30 {
836 | --fg: #008787;
837 | }
838 | .ap-terminal .bg-30 {
839 | --bg: #008787;
840 | }
841 | .ap-terminal .fg-31 {
842 | --fg: #0087af;
843 | }
844 | .ap-terminal .bg-31 {
845 | --bg: #0087af;
846 | }
847 | .ap-terminal .fg-32 {
848 | --fg: #0087d7;
849 | }
850 | .ap-terminal .bg-32 {
851 | --bg: #0087d7;
852 | }
853 | .ap-terminal .fg-33 {
854 | --fg: #0087ff;
855 | }
856 | .ap-terminal .bg-33 {
857 | --bg: #0087ff;
858 | }
859 | .ap-terminal .fg-34 {
860 | --fg: #00af00;
861 | }
862 | .ap-terminal .bg-34 {
863 | --bg: #00af00;
864 | }
865 | .ap-terminal .fg-35 {
866 | --fg: #00af5f;
867 | }
868 | .ap-terminal .bg-35 {
869 | --bg: #00af5f;
870 | }
871 | .ap-terminal .fg-36 {
872 | --fg: #00af87;
873 | }
874 | .ap-terminal .bg-36 {
875 | --bg: #00af87;
876 | }
877 | .ap-terminal .fg-37 {
878 | --fg: #00afaf;
879 | }
880 | .ap-terminal .bg-37 {
881 | --bg: #00afaf;
882 | }
883 | .ap-terminal .fg-38 {
884 | --fg: #00afd7;
885 | }
886 | .ap-terminal .bg-38 {
887 | --bg: #00afd7;
888 | }
889 | .ap-terminal .fg-39 {
890 | --fg: #00afff;
891 | }
892 | .ap-terminal .bg-39 {
893 | --bg: #00afff;
894 | }
895 | .ap-terminal .fg-40 {
896 | --fg: #00d700;
897 | }
898 | .ap-terminal .bg-40 {
899 | --bg: #00d700;
900 | }
901 | .ap-terminal .fg-41 {
902 | --fg: #00d75f;
903 | }
904 | .ap-terminal .bg-41 {
905 | --bg: #00d75f;
906 | }
907 | .ap-terminal .fg-42 {
908 | --fg: #00d787;
909 | }
910 | .ap-terminal .bg-42 {
911 | --bg: #00d787;
912 | }
913 | .ap-terminal .fg-43 {
914 | --fg: #00d7af;
915 | }
916 | .ap-terminal .bg-43 {
917 | --bg: #00d7af;
918 | }
919 | .ap-terminal .fg-44 {
920 | --fg: #00d7d7;
921 | }
922 | .ap-terminal .bg-44 {
923 | --bg: #00d7d7;
924 | }
925 | .ap-terminal .fg-45 {
926 | --fg: #00d7ff;
927 | }
928 | .ap-terminal .bg-45 {
929 | --bg: #00d7ff;
930 | }
931 | .ap-terminal .fg-46 {
932 | --fg: #00ff00;
933 | }
934 | .ap-terminal .bg-46 {
935 | --bg: #00ff00;
936 | }
937 | .ap-terminal .fg-47 {
938 | --fg: #00ff5f;
939 | }
940 | .ap-terminal .bg-47 {
941 | --bg: #00ff5f;
942 | }
943 | .ap-terminal .fg-48 {
944 | --fg: #00ff87;
945 | }
946 | .ap-terminal .bg-48 {
947 | --bg: #00ff87;
948 | }
949 | .ap-terminal .fg-49 {
950 | --fg: #00ffaf;
951 | }
952 | .ap-terminal .bg-49 {
953 | --bg: #00ffaf;
954 | }
955 | .ap-terminal .fg-50 {
956 | --fg: #00ffd7;
957 | }
958 | .ap-terminal .bg-50 {
959 | --bg: #00ffd7;
960 | }
961 | .ap-terminal .fg-51 {
962 | --fg: #00ffff;
963 | }
964 | .ap-terminal .bg-51 {
965 | --bg: #00ffff;
966 | }
967 | .ap-terminal .fg-52 {
968 | --fg: #5f0000;
969 | }
970 | .ap-terminal .bg-52 {
971 | --bg: #5f0000;
972 | }
973 | .ap-terminal .fg-53 {
974 | --fg: #5f005f;
975 | }
976 | .ap-terminal .bg-53 {
977 | --bg: #5f005f;
978 | }
979 | .ap-terminal .fg-54 {
980 | --fg: #5f0087;
981 | }
982 | .ap-terminal .bg-54 {
983 | --bg: #5f0087;
984 | }
985 | .ap-terminal .fg-55 {
986 | --fg: #5f00af;
987 | }
988 | .ap-terminal .bg-55 {
989 | --bg: #5f00af;
990 | }
991 | .ap-terminal .fg-56 {
992 | --fg: #5f00d7;
993 | }
994 | .ap-terminal .bg-56 {
995 | --bg: #5f00d7;
996 | }
997 | .ap-terminal .fg-57 {
998 | --fg: #5f00ff;
999 | }
1000 | .ap-terminal .bg-57 {
1001 | --bg: #5f00ff;
1002 | }
1003 | .ap-terminal .fg-58 {
1004 | --fg: #5f5f00;
1005 | }
1006 | .ap-terminal .bg-58 {
1007 | --bg: #5f5f00;
1008 | }
1009 | .ap-terminal .fg-59 {
1010 | --fg: #5f5f5f;
1011 | }
1012 | .ap-terminal .bg-59 {
1013 | --bg: #5f5f5f;
1014 | }
1015 | .ap-terminal .fg-60 {
1016 | --fg: #5f5f87;
1017 | }
1018 | .ap-terminal .bg-60 {
1019 | --bg: #5f5f87;
1020 | }
1021 | .ap-terminal .fg-61 {
1022 | --fg: #5f5faf;
1023 | }
1024 | .ap-terminal .bg-61 {
1025 | --bg: #5f5faf;
1026 | }
1027 | .ap-terminal .fg-62 {
1028 | --fg: #5f5fd7;
1029 | }
1030 | .ap-terminal .bg-62 {
1031 | --bg: #5f5fd7;
1032 | }
1033 | .ap-terminal .fg-63 {
1034 | --fg: #5f5fff;
1035 | }
1036 | .ap-terminal .bg-63 {
1037 | --bg: #5f5fff;
1038 | }
1039 | .ap-terminal .fg-64 {
1040 | --fg: #5f8700;
1041 | }
1042 | .ap-terminal .bg-64 {
1043 | --bg: #5f8700;
1044 | }
1045 | .ap-terminal .fg-65 {
1046 | --fg: #5f875f;
1047 | }
1048 | .ap-terminal .bg-65 {
1049 | --bg: #5f875f;
1050 | }
1051 | .ap-terminal .fg-66 {
1052 | --fg: #5f8787;
1053 | }
1054 | .ap-terminal .bg-66 {
1055 | --bg: #5f8787;
1056 | }
1057 | .ap-terminal .fg-67 {
1058 | --fg: #5f87af;
1059 | }
1060 | .ap-terminal .bg-67 {
1061 | --bg: #5f87af;
1062 | }
1063 | .ap-terminal .fg-68 {
1064 | --fg: #5f87d7;
1065 | }
1066 | .ap-terminal .bg-68 {
1067 | --bg: #5f87d7;
1068 | }
1069 | .ap-terminal .fg-69 {
1070 | --fg: #5f87ff;
1071 | }
1072 | .ap-terminal .bg-69 {
1073 | --bg: #5f87ff;
1074 | }
1075 | .ap-terminal .fg-70 {
1076 | --fg: #5faf00;
1077 | }
1078 | .ap-terminal .bg-70 {
1079 | --bg: #5faf00;
1080 | }
1081 | .ap-terminal .fg-71 {
1082 | --fg: #5faf5f;
1083 | }
1084 | .ap-terminal .bg-71 {
1085 | --bg: #5faf5f;
1086 | }
1087 | .ap-terminal .fg-72 {
1088 | --fg: #5faf87;
1089 | }
1090 | .ap-terminal .bg-72 {
1091 | --bg: #5faf87;
1092 | }
1093 | .ap-terminal .fg-73 {
1094 | --fg: #5fafaf;
1095 | }
1096 | .ap-terminal .bg-73 {
1097 | --bg: #5fafaf;
1098 | }
1099 | .ap-terminal .fg-74 {
1100 | --fg: #5fafd7;
1101 | }
1102 | .ap-terminal .bg-74 {
1103 | --bg: #5fafd7;
1104 | }
1105 | .ap-terminal .fg-75 {
1106 | --fg: #5fafff;
1107 | }
1108 | .ap-terminal .bg-75 {
1109 | --bg: #5fafff;
1110 | }
1111 | .ap-terminal .fg-76 {
1112 | --fg: #5fd700;
1113 | }
1114 | .ap-terminal .bg-76 {
1115 | --bg: #5fd700;
1116 | }
1117 | .ap-terminal .fg-77 {
1118 | --fg: #5fd75f;
1119 | }
1120 | .ap-terminal .bg-77 {
1121 | --bg: #5fd75f;
1122 | }
1123 | .ap-terminal .fg-78 {
1124 | --fg: #5fd787;
1125 | }
1126 | .ap-terminal .bg-78 {
1127 | --bg: #5fd787;
1128 | }
1129 | .ap-terminal .fg-79 {
1130 | --fg: #5fd7af;
1131 | }
1132 | .ap-terminal .bg-79 {
1133 | --bg: #5fd7af;
1134 | }
1135 | .ap-terminal .fg-80 {
1136 | --fg: #5fd7d7;
1137 | }
1138 | .ap-terminal .bg-80 {
1139 | --bg: #5fd7d7;
1140 | }
1141 | .ap-terminal .fg-81 {
1142 | --fg: #5fd7ff;
1143 | }
1144 | .ap-terminal .bg-81 {
1145 | --bg: #5fd7ff;
1146 | }
1147 | .ap-terminal .fg-82 {
1148 | --fg: #5fff00;
1149 | }
1150 | .ap-terminal .bg-82 {
1151 | --bg: #5fff00;
1152 | }
1153 | .ap-terminal .fg-83 {
1154 | --fg: #5fff5f;
1155 | }
1156 | .ap-terminal .bg-83 {
1157 | --bg: #5fff5f;
1158 | }
1159 | .ap-terminal .fg-84 {
1160 | --fg: #5fff87;
1161 | }
1162 | .ap-terminal .bg-84 {
1163 | --bg: #5fff87;
1164 | }
1165 | .ap-terminal .fg-85 {
1166 | --fg: #5fffaf;
1167 | }
1168 | .ap-terminal .bg-85 {
1169 | --bg: #5fffaf;
1170 | }
1171 | .ap-terminal .fg-86 {
1172 | --fg: #5fffd7;
1173 | }
1174 | .ap-terminal .bg-86 {
1175 | --bg: #5fffd7;
1176 | }
1177 | .ap-terminal .fg-87 {
1178 | --fg: #5fffff;
1179 | }
1180 | .ap-terminal .bg-87 {
1181 | --bg: #5fffff;
1182 | }
1183 | .ap-terminal .fg-88 {
1184 | --fg: #870000;
1185 | }
1186 | .ap-terminal .bg-88 {
1187 | --bg: #870000;
1188 | }
1189 | .ap-terminal .fg-89 {
1190 | --fg: #87005f;
1191 | }
1192 | .ap-terminal .bg-89 {
1193 | --bg: #87005f;
1194 | }
1195 | .ap-terminal .fg-90 {
1196 | --fg: #870087;
1197 | }
1198 | .ap-terminal .bg-90 {
1199 | --bg: #870087;
1200 | }
1201 | .ap-terminal .fg-91 {
1202 | --fg: #8700af;
1203 | }
1204 | .ap-terminal .bg-91 {
1205 | --bg: #8700af;
1206 | }
1207 | .ap-terminal .fg-92 {
1208 | --fg: #8700d7;
1209 | }
1210 | .ap-terminal .bg-92 {
1211 | --bg: #8700d7;
1212 | }
1213 | .ap-terminal .fg-93 {
1214 | --fg: #8700ff;
1215 | }
1216 | .ap-terminal .bg-93 {
1217 | --bg: #8700ff;
1218 | }
1219 | .ap-terminal .fg-94 {
1220 | --fg: #875f00;
1221 | }
1222 | .ap-terminal .bg-94 {
1223 | --bg: #875f00;
1224 | }
1225 | .ap-terminal .fg-95 {
1226 | --fg: #875f5f;
1227 | }
1228 | .ap-terminal .bg-95 {
1229 | --bg: #875f5f;
1230 | }
1231 | .ap-terminal .fg-96 {
1232 | --fg: #875f87;
1233 | }
1234 | .ap-terminal .bg-96 {
1235 | --bg: #875f87;
1236 | }
1237 | .ap-terminal .fg-97 {
1238 | --fg: #875faf;
1239 | }
1240 | .ap-terminal .bg-97 {
1241 | --bg: #875faf;
1242 | }
1243 | .ap-terminal .fg-98 {
1244 | --fg: #875fd7;
1245 | }
1246 | .ap-terminal .bg-98 {
1247 | --bg: #875fd7;
1248 | }
1249 | .ap-terminal .fg-99 {
1250 | --fg: #875fff;
1251 | }
1252 | .ap-terminal .bg-99 {
1253 | --bg: #875fff;
1254 | }
1255 | .ap-terminal .fg-100 {
1256 | --fg: #878700;
1257 | }
1258 | .ap-terminal .bg-100 {
1259 | --bg: #878700;
1260 | }
1261 | .ap-terminal .fg-101 {
1262 | --fg: #87875f;
1263 | }
1264 | .ap-terminal .bg-101 {
1265 | --bg: #87875f;
1266 | }
1267 | .ap-terminal .fg-102 {
1268 | --fg: #878787;
1269 | }
1270 | .ap-terminal .bg-102 {
1271 | --bg: #878787;
1272 | }
1273 | .ap-terminal .fg-103 {
1274 | --fg: #8787af;
1275 | }
1276 | .ap-terminal .bg-103 {
1277 | --bg: #8787af;
1278 | }
1279 | .ap-terminal .fg-104 {
1280 | --fg: #8787d7;
1281 | }
1282 | .ap-terminal .bg-104 {
1283 | --bg: #8787d7;
1284 | }
1285 | .ap-terminal .fg-105 {
1286 | --fg: #8787ff;
1287 | }
1288 | .ap-terminal .bg-105 {
1289 | --bg: #8787ff;
1290 | }
1291 | .ap-terminal .fg-106 {
1292 | --fg: #87af00;
1293 | }
1294 | .ap-terminal .bg-106 {
1295 | --bg: #87af00;
1296 | }
1297 | .ap-terminal .fg-107 {
1298 | --fg: #87af5f;
1299 | }
1300 | .ap-terminal .bg-107 {
1301 | --bg: #87af5f;
1302 | }
1303 | .ap-terminal .fg-108 {
1304 | --fg: #87af87;
1305 | }
1306 | .ap-terminal .bg-108 {
1307 | --bg: #87af87;
1308 | }
1309 | .ap-terminal .fg-109 {
1310 | --fg: #87afaf;
1311 | }
1312 | .ap-terminal .bg-109 {
1313 | --bg: #87afaf;
1314 | }
1315 | .ap-terminal .fg-110 {
1316 | --fg: #87afd7;
1317 | }
1318 | .ap-terminal .bg-110 {
1319 | --bg: #87afd7;
1320 | }
1321 | .ap-terminal .fg-111 {
1322 | --fg: #87afff;
1323 | }
1324 | .ap-terminal .bg-111 {
1325 | --bg: #87afff;
1326 | }
1327 | .ap-terminal .fg-112 {
1328 | --fg: #87d700;
1329 | }
1330 | .ap-terminal .bg-112 {
1331 | --bg: #87d700;
1332 | }
1333 | .ap-terminal .fg-113 {
1334 | --fg: #87d75f;
1335 | }
1336 | .ap-terminal .bg-113 {
1337 | --bg: #87d75f;
1338 | }
1339 | .ap-terminal .fg-114 {
1340 | --fg: #87d787;
1341 | }
1342 | .ap-terminal .bg-114 {
1343 | --bg: #87d787;
1344 | }
1345 | .ap-terminal .fg-115 {
1346 | --fg: #87d7af;
1347 | }
1348 | .ap-terminal .bg-115 {
1349 | --bg: #87d7af;
1350 | }
1351 | .ap-terminal .fg-116 {
1352 | --fg: #87d7d7;
1353 | }
1354 | .ap-terminal .bg-116 {
1355 | --bg: #87d7d7;
1356 | }
1357 | .ap-terminal .fg-117 {
1358 | --fg: #87d7ff;
1359 | }
1360 | .ap-terminal .bg-117 {
1361 | --bg: #87d7ff;
1362 | }
1363 | .ap-terminal .fg-118 {
1364 | --fg: #87ff00;
1365 | }
1366 | .ap-terminal .bg-118 {
1367 | --bg: #87ff00;
1368 | }
1369 | .ap-terminal .fg-119 {
1370 | --fg: #87ff5f;
1371 | }
1372 | .ap-terminal .bg-119 {
1373 | --bg: #87ff5f;
1374 | }
1375 | .ap-terminal .fg-120 {
1376 | --fg: #87ff87;
1377 | }
1378 | .ap-terminal .bg-120 {
1379 | --bg: #87ff87;
1380 | }
1381 | .ap-terminal .fg-121 {
1382 | --fg: #87ffaf;
1383 | }
1384 | .ap-terminal .bg-121 {
1385 | --bg: #87ffaf;
1386 | }
1387 | .ap-terminal .fg-122 {
1388 | --fg: #87ffd7;
1389 | }
1390 | .ap-terminal .bg-122 {
1391 | --bg: #87ffd7;
1392 | }
1393 | .ap-terminal .fg-123 {
1394 | --fg: #87ffff;
1395 | }
1396 | .ap-terminal .bg-123 {
1397 | --bg: #87ffff;
1398 | }
1399 | .ap-terminal .fg-124 {
1400 | --fg: #af0000;
1401 | }
1402 | .ap-terminal .bg-124 {
1403 | --bg: #af0000;
1404 | }
1405 | .ap-terminal .fg-125 {
1406 | --fg: #af005f;
1407 | }
1408 | .ap-terminal .bg-125 {
1409 | --bg: #af005f;
1410 | }
1411 | .ap-terminal .fg-126 {
1412 | --fg: #af0087;
1413 | }
1414 | .ap-terminal .bg-126 {
1415 | --bg: #af0087;
1416 | }
1417 | .ap-terminal .fg-127 {
1418 | --fg: #af00af;
1419 | }
1420 | .ap-terminal .bg-127 {
1421 | --bg: #af00af;
1422 | }
1423 | .ap-terminal .fg-128 {
1424 | --fg: #af00d7;
1425 | }
1426 | .ap-terminal .bg-128 {
1427 | --bg: #af00d7;
1428 | }
1429 | .ap-terminal .fg-129 {
1430 | --fg: #af00ff;
1431 | }
1432 | .ap-terminal .bg-129 {
1433 | --bg: #af00ff;
1434 | }
1435 | .ap-terminal .fg-130 {
1436 | --fg: #af5f00;
1437 | }
1438 | .ap-terminal .bg-130 {
1439 | --bg: #af5f00;
1440 | }
1441 | .ap-terminal .fg-131 {
1442 | --fg: #af5f5f;
1443 | }
1444 | .ap-terminal .bg-131 {
1445 | --bg: #af5f5f;
1446 | }
1447 | .ap-terminal .fg-132 {
1448 | --fg: #af5f87;
1449 | }
1450 | .ap-terminal .bg-132 {
1451 | --bg: #af5f87;
1452 | }
1453 | .ap-terminal .fg-133 {
1454 | --fg: #af5faf;
1455 | }
1456 | .ap-terminal .bg-133 {
1457 | --bg: #af5faf;
1458 | }
1459 | .ap-terminal .fg-134 {
1460 | --fg: #af5fd7;
1461 | }
1462 | .ap-terminal .bg-134 {
1463 | --bg: #af5fd7;
1464 | }
1465 | .ap-terminal .fg-135 {
1466 | --fg: #af5fff;
1467 | }
1468 | .ap-terminal .bg-135 {
1469 | --bg: #af5fff;
1470 | }
1471 | .ap-terminal .fg-136 {
1472 | --fg: #af8700;
1473 | }
1474 | .ap-terminal .bg-136 {
1475 | --bg: #af8700;
1476 | }
1477 | .ap-terminal .fg-137 {
1478 | --fg: #af875f;
1479 | }
1480 | .ap-terminal .bg-137 {
1481 | --bg: #af875f;
1482 | }
1483 | .ap-terminal .fg-138 {
1484 | --fg: #af8787;
1485 | }
1486 | .ap-terminal .bg-138 {
1487 | --bg: #af8787;
1488 | }
1489 | .ap-terminal .fg-139 {
1490 | --fg: #af87af;
1491 | }
1492 | .ap-terminal .bg-139 {
1493 | --bg: #af87af;
1494 | }
1495 | .ap-terminal .fg-140 {
1496 | --fg: #af87d7;
1497 | }
1498 | .ap-terminal .bg-140 {
1499 | --bg: #af87d7;
1500 | }
1501 | .ap-terminal .fg-141 {
1502 | --fg: #af87ff;
1503 | }
1504 | .ap-terminal .bg-141 {
1505 | --bg: #af87ff;
1506 | }
1507 | .ap-terminal .fg-142 {
1508 | --fg: #afaf00;
1509 | }
1510 | .ap-terminal .bg-142 {
1511 | --bg: #afaf00;
1512 | }
1513 | .ap-terminal .fg-143 {
1514 | --fg: #afaf5f;
1515 | }
1516 | .ap-terminal .bg-143 {
1517 | --bg: #afaf5f;
1518 | }
1519 | .ap-terminal .fg-144 {
1520 | --fg: #afaf87;
1521 | }
1522 | .ap-terminal .bg-144 {
1523 | --bg: #afaf87;
1524 | }
1525 | .ap-terminal .fg-145 {
1526 | --fg: #afafaf;
1527 | }
1528 | .ap-terminal .bg-145 {
1529 | --bg: #afafaf;
1530 | }
1531 | .ap-terminal .fg-146 {
1532 | --fg: #afafd7;
1533 | }
1534 | .ap-terminal .bg-146 {
1535 | --bg: #afafd7;
1536 | }
1537 | .ap-terminal .fg-147 {
1538 | --fg: #afafff;
1539 | }
1540 | .ap-terminal .bg-147 {
1541 | --bg: #afafff;
1542 | }
1543 | .ap-terminal .fg-148 {
1544 | --fg: #afd700;
1545 | }
1546 | .ap-terminal .bg-148 {
1547 | --bg: #afd700;
1548 | }
1549 | .ap-terminal .fg-149 {
1550 | --fg: #afd75f;
1551 | }
1552 | .ap-terminal .bg-149 {
1553 | --bg: #afd75f;
1554 | }
1555 | .ap-terminal .fg-150 {
1556 | --fg: #afd787;
1557 | }
1558 | .ap-terminal .bg-150 {
1559 | --bg: #afd787;
1560 | }
1561 | .ap-terminal .fg-151 {
1562 | --fg: #afd7af;
1563 | }
1564 | .ap-terminal .bg-151 {
1565 | --bg: #afd7af;
1566 | }
1567 | .ap-terminal .fg-152 {
1568 | --fg: #afd7d7;
1569 | }
1570 | .ap-terminal .bg-152 {
1571 | --bg: #afd7d7;
1572 | }
1573 | .ap-terminal .fg-153 {
1574 | --fg: #afd7ff;
1575 | }
1576 | .ap-terminal .bg-153 {
1577 | --bg: #afd7ff;
1578 | }
1579 | .ap-terminal .fg-154 {
1580 | --fg: #afff00;
1581 | }
1582 | .ap-terminal .bg-154 {
1583 | --bg: #afff00;
1584 | }
1585 | .ap-terminal .fg-155 {
1586 | --fg: #afff5f;
1587 | }
1588 | .ap-terminal .bg-155 {
1589 | --bg: #afff5f;
1590 | }
1591 | .ap-terminal .fg-156 {
1592 | --fg: #afff87;
1593 | }
1594 | .ap-terminal .bg-156 {
1595 | --bg: #afff87;
1596 | }
1597 | .ap-terminal .fg-157 {
1598 | --fg: #afffaf;
1599 | }
1600 | .ap-terminal .bg-157 {
1601 | --bg: #afffaf;
1602 | }
1603 | .ap-terminal .fg-158 {
1604 | --fg: #afffd7;
1605 | }
1606 | .ap-terminal .bg-158 {
1607 | --bg: #afffd7;
1608 | }
1609 | .ap-terminal .fg-159 {
1610 | --fg: #afffff;
1611 | }
1612 | .ap-terminal .bg-159 {
1613 | --bg: #afffff;
1614 | }
1615 | .ap-terminal .fg-160 {
1616 | --fg: #d70000;
1617 | }
1618 | .ap-terminal .bg-160 {
1619 | --bg: #d70000;
1620 | }
1621 | .ap-terminal .fg-161 {
1622 | --fg: #d7005f;
1623 | }
1624 | .ap-terminal .bg-161 {
1625 | --bg: #d7005f;
1626 | }
1627 | .ap-terminal .fg-162 {
1628 | --fg: #d70087;
1629 | }
1630 | .ap-terminal .bg-162 {
1631 | --bg: #d70087;
1632 | }
1633 | .ap-terminal .fg-163 {
1634 | --fg: #d700af;
1635 | }
1636 | .ap-terminal .bg-163 {
1637 | --bg: #d700af;
1638 | }
1639 | .ap-terminal .fg-164 {
1640 | --fg: #d700d7;
1641 | }
1642 | .ap-terminal .bg-164 {
1643 | --bg: #d700d7;
1644 | }
1645 | .ap-terminal .fg-165 {
1646 | --fg: #d700ff;
1647 | }
1648 | .ap-terminal .bg-165 {
1649 | --bg: #d700ff;
1650 | }
1651 | .ap-terminal .fg-166 {
1652 | --fg: #d75f00;
1653 | }
1654 | .ap-terminal .bg-166 {
1655 | --bg: #d75f00;
1656 | }
1657 | .ap-terminal .fg-167 {
1658 | --fg: #d75f5f;
1659 | }
1660 | .ap-terminal .bg-167 {
1661 | --bg: #d75f5f;
1662 | }
1663 | .ap-terminal .fg-168 {
1664 | --fg: #d75f87;
1665 | }
1666 | .ap-terminal .bg-168 {
1667 | --bg: #d75f87;
1668 | }
1669 | .ap-terminal .fg-169 {
1670 | --fg: #d75faf;
1671 | }
1672 | .ap-terminal .bg-169 {
1673 | --bg: #d75faf;
1674 | }
1675 | .ap-terminal .fg-170 {
1676 | --fg: #d75fd7;
1677 | }
1678 | .ap-terminal .bg-170 {
1679 | --bg: #d75fd7;
1680 | }
1681 | .ap-terminal .fg-171 {
1682 | --fg: #d75fff;
1683 | }
1684 | .ap-terminal .bg-171 {
1685 | --bg: #d75fff;
1686 | }
1687 | .ap-terminal .fg-172 {
1688 | --fg: #d78700;
1689 | }
1690 | .ap-terminal .bg-172 {
1691 | --bg: #d78700;
1692 | }
1693 | .ap-terminal .fg-173 {
1694 | --fg: #d7875f;
1695 | }
1696 | .ap-terminal .bg-173 {
1697 | --bg: #d7875f;
1698 | }
1699 | .ap-terminal .fg-174 {
1700 | --fg: #d78787;
1701 | }
1702 | .ap-terminal .bg-174 {
1703 | --bg: #d78787;
1704 | }
1705 | .ap-terminal .fg-175 {
1706 | --fg: #d787af;
1707 | }
1708 | .ap-terminal .bg-175 {
1709 | --bg: #d787af;
1710 | }
1711 | .ap-terminal .fg-176 {
1712 | --fg: #d787d7;
1713 | }
1714 | .ap-terminal .bg-176 {
1715 | --bg: #d787d7;
1716 | }
1717 | .ap-terminal .fg-177 {
1718 | --fg: #d787ff;
1719 | }
1720 | .ap-terminal .bg-177 {
1721 | --bg: #d787ff;
1722 | }
1723 | .ap-terminal .fg-178 {
1724 | --fg: #d7af00;
1725 | }
1726 | .ap-terminal .bg-178 {
1727 | --bg: #d7af00;
1728 | }
1729 | .ap-terminal .fg-179 {
1730 | --fg: #d7af5f;
1731 | }
1732 | .ap-terminal .bg-179 {
1733 | --bg: #d7af5f;
1734 | }
1735 | .ap-terminal .fg-180 {
1736 | --fg: #d7af87;
1737 | }
1738 | .ap-terminal .bg-180 {
1739 | --bg: #d7af87;
1740 | }
1741 | .ap-terminal .fg-181 {
1742 | --fg: #d7afaf;
1743 | }
1744 | .ap-terminal .bg-181 {
1745 | --bg: #d7afaf;
1746 | }
1747 | .ap-terminal .fg-182 {
1748 | --fg: #d7afd7;
1749 | }
1750 | .ap-terminal .bg-182 {
1751 | --bg: #d7afd7;
1752 | }
1753 | .ap-terminal .fg-183 {
1754 | --fg: #d7afff;
1755 | }
1756 | .ap-terminal .bg-183 {
1757 | --bg: #d7afff;
1758 | }
1759 | .ap-terminal .fg-184 {
1760 | --fg: #d7d700;
1761 | }
1762 | .ap-terminal .bg-184 {
1763 | --bg: #d7d700;
1764 | }
1765 | .ap-terminal .fg-185 {
1766 | --fg: #d7d75f;
1767 | }
1768 | .ap-terminal .bg-185 {
1769 | --bg: #d7d75f;
1770 | }
1771 | .ap-terminal .fg-186 {
1772 | --fg: #d7d787;
1773 | }
1774 | .ap-terminal .bg-186 {
1775 | --bg: #d7d787;
1776 | }
1777 | .ap-terminal .fg-187 {
1778 | --fg: #d7d7af;
1779 | }
1780 | .ap-terminal .bg-187 {
1781 | --bg: #d7d7af;
1782 | }
1783 | .ap-terminal .fg-188 {
1784 | --fg: #d7d7d7;
1785 | }
1786 | .ap-terminal .bg-188 {
1787 | --bg: #d7d7d7;
1788 | }
1789 | .ap-terminal .fg-189 {
1790 | --fg: #d7d7ff;
1791 | }
1792 | .ap-terminal .bg-189 {
1793 | --bg: #d7d7ff;
1794 | }
1795 | .ap-terminal .fg-190 {
1796 | --fg: #d7ff00;
1797 | }
1798 | .ap-terminal .bg-190 {
1799 | --bg: #d7ff00;
1800 | }
1801 | .ap-terminal .fg-191 {
1802 | --fg: #d7ff5f;
1803 | }
1804 | .ap-terminal .bg-191 {
1805 | --bg: #d7ff5f;
1806 | }
1807 | .ap-terminal .fg-192 {
1808 | --fg: #d7ff87;
1809 | }
1810 | .ap-terminal .bg-192 {
1811 | --bg: #d7ff87;
1812 | }
1813 | .ap-terminal .fg-193 {
1814 | --fg: #d7ffaf;
1815 | }
1816 | .ap-terminal .bg-193 {
1817 | --bg: #d7ffaf;
1818 | }
1819 | .ap-terminal .fg-194 {
1820 | --fg: #d7ffd7;
1821 | }
1822 | .ap-terminal .bg-194 {
1823 | --bg: #d7ffd7;
1824 | }
1825 | .ap-terminal .fg-195 {
1826 | --fg: #d7ffff;
1827 | }
1828 | .ap-terminal .bg-195 {
1829 | --bg: #d7ffff;
1830 | }
1831 | .ap-terminal .fg-196 {
1832 | --fg: #ff0000;
1833 | }
1834 | .ap-terminal .bg-196 {
1835 | --bg: #ff0000;
1836 | }
1837 | .ap-terminal .fg-197 {
1838 | --fg: #ff005f;
1839 | }
1840 | .ap-terminal .bg-197 {
1841 | --bg: #ff005f;
1842 | }
1843 | .ap-terminal .fg-198 {
1844 | --fg: #ff0087;
1845 | }
1846 | .ap-terminal .bg-198 {
1847 | --bg: #ff0087;
1848 | }
1849 | .ap-terminal .fg-199 {
1850 | --fg: #ff00af;
1851 | }
1852 | .ap-terminal .bg-199 {
1853 | --bg: #ff00af;
1854 | }
1855 | .ap-terminal .fg-200 {
1856 | --fg: #ff00d7;
1857 | }
1858 | .ap-terminal .bg-200 {
1859 | --bg: #ff00d7;
1860 | }
1861 | .ap-terminal .fg-201 {
1862 | --fg: #ff00ff;
1863 | }
1864 | .ap-terminal .bg-201 {
1865 | --bg: #ff00ff;
1866 | }
1867 | .ap-terminal .fg-202 {
1868 | --fg: #ff5f00;
1869 | }
1870 | .ap-terminal .bg-202 {
1871 | --bg: #ff5f00;
1872 | }
1873 | .ap-terminal .fg-203 {
1874 | --fg: #ff5f5f;
1875 | }
1876 | .ap-terminal .bg-203 {
1877 | --bg: #ff5f5f;
1878 | }
1879 | .ap-terminal .fg-204 {
1880 | --fg: #ff5f87;
1881 | }
1882 | .ap-terminal .bg-204 {
1883 | --bg: #ff5f87;
1884 | }
1885 | .ap-terminal .fg-205 {
1886 | --fg: #ff5faf;
1887 | }
1888 | .ap-terminal .bg-205 {
1889 | --bg: #ff5faf;
1890 | }
1891 | .ap-terminal .fg-206 {
1892 | --fg: #ff5fd7;
1893 | }
1894 | .ap-terminal .bg-206 {
1895 | --bg: #ff5fd7;
1896 | }
1897 | .ap-terminal .fg-207 {
1898 | --fg: #ff5fff;
1899 | }
1900 | .ap-terminal .bg-207 {
1901 | --bg: #ff5fff;
1902 | }
1903 | .ap-terminal .fg-208 {
1904 | --fg: #ff8700;
1905 | }
1906 | .ap-terminal .bg-208 {
1907 | --bg: #ff8700;
1908 | }
1909 | .ap-terminal .fg-209 {
1910 | --fg: #ff875f;
1911 | }
1912 | .ap-terminal .bg-209 {
1913 | --bg: #ff875f;
1914 | }
1915 | .ap-terminal .fg-210 {
1916 | --fg: #ff8787;
1917 | }
1918 | .ap-terminal .bg-210 {
1919 | --bg: #ff8787;
1920 | }
1921 | .ap-terminal .fg-211 {
1922 | --fg: #ff87af;
1923 | }
1924 | .ap-terminal .bg-211 {
1925 | --bg: #ff87af;
1926 | }
1927 | .ap-terminal .fg-212 {
1928 | --fg: #ff87d7;
1929 | }
1930 | .ap-terminal .bg-212 {
1931 | --bg: #ff87d7;
1932 | }
1933 | .ap-terminal .fg-213 {
1934 | --fg: #ff87ff;
1935 | }
1936 | .ap-terminal .bg-213 {
1937 | --bg: #ff87ff;
1938 | }
1939 | .ap-terminal .fg-214 {
1940 | --fg: #ffaf00;
1941 | }
1942 | .ap-terminal .bg-214 {
1943 | --bg: #ffaf00;
1944 | }
1945 | .ap-terminal .fg-215 {
1946 | --fg: #ffaf5f;
1947 | }
1948 | .ap-terminal .bg-215 {
1949 | --bg: #ffaf5f;
1950 | }
1951 | .ap-terminal .fg-216 {
1952 | --fg: #ffaf87;
1953 | }
1954 | .ap-terminal .bg-216 {
1955 | --bg: #ffaf87;
1956 | }
1957 | .ap-terminal .fg-217 {
1958 | --fg: #ffafaf;
1959 | }
1960 | .ap-terminal .bg-217 {
1961 | --bg: #ffafaf;
1962 | }
1963 | .ap-terminal .fg-218 {
1964 | --fg: #ffafd7;
1965 | }
1966 | .ap-terminal .bg-218 {
1967 | --bg: #ffafd7;
1968 | }
1969 | .ap-terminal .fg-219 {
1970 | --fg: #ffafff;
1971 | }
1972 | .ap-terminal .bg-219 {
1973 | --bg: #ffafff;
1974 | }
1975 | .ap-terminal .fg-220 {
1976 | --fg: #ffd700;
1977 | }
1978 | .ap-terminal .bg-220 {
1979 | --bg: #ffd700;
1980 | }
1981 | .ap-terminal .fg-221 {
1982 | --fg: #ffd75f;
1983 | }
1984 | .ap-terminal .bg-221 {
1985 | --bg: #ffd75f;
1986 | }
1987 | .ap-terminal .fg-222 {
1988 | --fg: #ffd787;
1989 | }
1990 | .ap-terminal .bg-222 {
1991 | --bg: #ffd787;
1992 | }
1993 | .ap-terminal .fg-223 {
1994 | --fg: #ffd7af;
1995 | }
1996 | .ap-terminal .bg-223 {
1997 | --bg: #ffd7af;
1998 | }
1999 | .ap-terminal .fg-224 {
2000 | --fg: #ffd7d7;
2001 | }
2002 | .ap-terminal .bg-224 {
2003 | --bg: #ffd7d7;
2004 | }
2005 | .ap-terminal .fg-225 {
2006 | --fg: #ffd7ff;
2007 | }
2008 | .ap-terminal .bg-225 {
2009 | --bg: #ffd7ff;
2010 | }
2011 | .ap-terminal .fg-226 {
2012 | --fg: #ffff00;
2013 | }
2014 | .ap-terminal .bg-226 {
2015 | --bg: #ffff00;
2016 | }
2017 | .ap-terminal .fg-227 {
2018 | --fg: #ffff5f;
2019 | }
2020 | .ap-terminal .bg-227 {
2021 | --bg: #ffff5f;
2022 | }
2023 | .ap-terminal .fg-228 {
2024 | --fg: #ffff87;
2025 | }
2026 | .ap-terminal .bg-228 {
2027 | --bg: #ffff87;
2028 | }
2029 | .ap-terminal .fg-229 {
2030 | --fg: #ffffaf;
2031 | }
2032 | .ap-terminal .bg-229 {
2033 | --bg: #ffffaf;
2034 | }
2035 | .ap-terminal .fg-230 {
2036 | --fg: #ffffd7;
2037 | }
2038 | .ap-terminal .bg-230 {
2039 | --bg: #ffffd7;
2040 | }
2041 | .ap-terminal .fg-231 {
2042 | --fg: #ffffff;
2043 | }
2044 | .ap-terminal .bg-231 {
2045 | --bg: #ffffff;
2046 | }
2047 | .ap-terminal .fg-232 {
2048 | --fg: #080808;
2049 | }
2050 | .ap-terminal .bg-232 {
2051 | --bg: #080808;
2052 | }
2053 | .ap-terminal .fg-233 {
2054 | --fg: #121212;
2055 | }
2056 | .ap-terminal .bg-233 {
2057 | --bg: #121212;
2058 | }
2059 | .ap-terminal .fg-234 {
2060 | --fg: #1c1c1c;
2061 | }
2062 | .ap-terminal .bg-234 {
2063 | --bg: #1c1c1c;
2064 | }
2065 | .ap-terminal .fg-235 {
2066 | --fg: #262626;
2067 | }
2068 | .ap-terminal .bg-235 {
2069 | --bg: #262626;
2070 | }
2071 | .ap-terminal .fg-236 {
2072 | --fg: #303030;
2073 | }
2074 | .ap-terminal .bg-236 {
2075 | --bg: #303030;
2076 | }
2077 | .ap-terminal .fg-237 {
2078 | --fg: #3a3a3a;
2079 | }
2080 | .ap-terminal .bg-237 {
2081 | --bg: #3a3a3a;
2082 | }
2083 | .ap-terminal .fg-238 {
2084 | --fg: #444444;
2085 | }
2086 | .ap-terminal .bg-238 {
2087 | --bg: #444444;
2088 | }
2089 | .ap-terminal .fg-239 {
2090 | --fg: #4e4e4e;
2091 | }
2092 | .ap-terminal .bg-239 {
2093 | --bg: #4e4e4e;
2094 | }
2095 | .ap-terminal .fg-240 {
2096 | --fg: #585858;
2097 | }
2098 | .ap-terminal .bg-240 {
2099 | --bg: #585858;
2100 | }
2101 | .ap-terminal .fg-241 {
2102 | --fg: #626262;
2103 | }
2104 | .ap-terminal .bg-241 {
2105 | --bg: #626262;
2106 | }
2107 | .ap-terminal .fg-242 {
2108 | --fg: #6c6c6c;
2109 | }
2110 | .ap-terminal .bg-242 {
2111 | --bg: #6c6c6c;
2112 | }
2113 | .ap-terminal .fg-243 {
2114 | --fg: #767676;
2115 | }
2116 | .ap-terminal .bg-243 {
2117 | --bg: #767676;
2118 | }
2119 | .ap-terminal .fg-244 {
2120 | --fg: #808080;
2121 | }
2122 | .ap-terminal .bg-244 {
2123 | --bg: #808080;
2124 | }
2125 | .ap-terminal .fg-245 {
2126 | --fg: #8a8a8a;
2127 | }
2128 | .ap-terminal .bg-245 {
2129 | --bg: #8a8a8a;
2130 | }
2131 | .ap-terminal .fg-246 {
2132 | --fg: #949494;
2133 | }
2134 | .ap-terminal .bg-246 {
2135 | --bg: #949494;
2136 | }
2137 | .ap-terminal .fg-247 {
2138 | --fg: #9e9e9e;
2139 | }
2140 | .ap-terminal .bg-247 {
2141 | --bg: #9e9e9e;
2142 | }
2143 | .ap-terminal .fg-248 {
2144 | --fg: #a8a8a8;
2145 | }
2146 | .ap-terminal .bg-248 {
2147 | --bg: #a8a8a8;
2148 | }
2149 | .ap-terminal .fg-249 {
2150 | --fg: #b2b2b2;
2151 | }
2152 | .ap-terminal .bg-249 {
2153 | --bg: #b2b2b2;
2154 | }
2155 | .ap-terminal .fg-250 {
2156 | --fg: #bcbcbc;
2157 | }
2158 | .ap-terminal .bg-250 {
2159 | --bg: #bcbcbc;
2160 | }
2161 | .ap-terminal .fg-251 {
2162 | --fg: #c6c6c6;
2163 | }
2164 | .ap-terminal .bg-251 {
2165 | --bg: #c6c6c6;
2166 | }
2167 | .ap-terminal .fg-252 {
2168 | --fg: #d0d0d0;
2169 | }
2170 | .ap-terminal .bg-252 {
2171 | --bg: #d0d0d0;
2172 | }
2173 | .ap-terminal .fg-253 {
2174 | --fg: #dadada;
2175 | }
2176 | .ap-terminal .bg-253 {
2177 | --bg: #dadada;
2178 | }
2179 | .ap-terminal .fg-254 {
2180 | --fg: #e4e4e4;
2181 | }
2182 | .ap-terminal .bg-254 {
2183 | --bg: #e4e4e4;
2184 | }
2185 | .ap-terminal .fg-255 {
2186 | --fg: #eeeeee;
2187 | }
2188 | .ap-terminal .bg-255 {
2189 | --bg: #eeeeee;
2190 | }
2191 | .asciinema-player-theme-asciinema {
2192 | --term-color-foreground: #cccccc;
2193 | --term-color-background: #121314;
2194 | --term-color-0: hsl(0, 0%, 0%);
2195 | --term-color-1: hsl(343, 70%, 55%);
2196 | --term-color-2: hsl(103, 70%, 44%);
2197 | --term-color-3: hsl(43, 70%, 55%);
2198 | --term-color-4: hsl(193, 70%, 49.5%);
2199 | --term-color-5: hsl(283, 70%, 60.5%);
2200 | --term-color-6: hsl(163, 70%, 60.5%);
2201 | --term-color-7: hsl(0, 0%, 85%);
2202 | --term-color-8: hsl(0, 0%, 30%);
2203 | --term-color-9: hsl(343, 70%, 55%);
2204 | --term-color-10: hsl(103, 70%, 44%);
2205 | --term-color-11: hsl(43, 70%, 55%);
2206 | --term-color-12: hsl(193, 70%, 49.5%);
2207 | --term-color-13: hsl(283, 70%, 60.5%);
2208 | --term-color-14: hsl(163, 70%, 60.5%);
2209 | --term-color-15: hsl(0, 0%, 100%);
2210 | }
2211 | /*
2212 | Based on Dracula: https://draculatheme.com
2213 | */
2214 | .asciinema-player-theme-dracula {
2215 | --term-color-foreground: #f8f8f2;
2216 | --term-color-background: #282a36;
2217 | --term-color-0: #21222c;
2218 | --term-color-1: #ff5555;
2219 | --term-color-2: #50fa7b;
2220 | --term-color-3: #f1fa8c;
2221 | --term-color-4: #bd93f9;
2222 | --term-color-5: #ff79c6;
2223 | --term-color-6: #8be9fd;
2224 | --term-color-7: #f8f8f2;
2225 | --term-color-8: #6272a4;
2226 | --term-color-9: #ff6e6e;
2227 | --term-color-10: #69ff94;
2228 | --term-color-11: #ffffa5;
2229 | --term-color-12: #d6acff;
2230 | --term-color-13: #ff92df;
2231 | --term-color-14: #a4ffff;
2232 | --term-color-15: #ffffff;
2233 | }
2234 | /* Based on Monokai from base16 collection - https://github.com/chriskempson/base16 */
2235 | .asciinema-player-theme-monokai {
2236 | --term-color-foreground: #f8f8f2;
2237 | --term-color-background: #272822;
2238 | --term-color-0: #272822;
2239 | --term-color-1: #f92672;
2240 | --term-color-2: #a6e22e;
2241 | --term-color-3: #f4bf75;
2242 | --term-color-4: #66d9ef;
2243 | --term-color-5: #ae81ff;
2244 | --term-color-6: #a1efe4;
2245 | --term-color-7: #f8f8f2;
2246 | --term-color-8: #75715e;
2247 | --term-color-15: #f9f8f5;
2248 | }
2249 | /*
2250 | Based on Nord: https://github.com/arcticicestudio/nord
2251 | Via: https://github.com/neilotoole/asciinema-theme-nord
2252 | */
2253 | .asciinema-player-theme-nord {
2254 | --term-color-foreground: #eceff4;
2255 | --term-color-background: #2e3440;
2256 | --term-color-0: #3b4252;
2257 | --term-color-1: #bf616a;
2258 | --term-color-2: #a3be8c;
2259 | --term-color-3: #ebcb8b;
2260 | --term-color-4: #81a1c1;
2261 | --term-color-5: #b48ead;
2262 | --term-color-6: #88c0d0;
2263 | --term-color-7: #eceff4;
2264 | }
2265 | .asciinema-player-theme-seti {
2266 | --term-color-foreground: #cacecd;
2267 | --term-color-background: #111213;
2268 | --term-color-0: #323232;
2269 | --term-color-1: #c22832;
2270 | --term-color-2: #8ec43d;
2271 | --term-color-3: #e0c64f;
2272 | --term-color-4: #43a5d5;
2273 | --term-color-5: #8b57b5;
2274 | --term-color-6: #8ec43d;
2275 | --term-color-7: #eeeeee;
2276 | --term-color-15: #ffffff;
2277 | }
2278 | /*
2279 | Based on Solarized Dark: https://ethanschoonover.com/solarized/
2280 | */
2281 | .asciinema-player-theme-solarized-dark {
2282 | --term-color-foreground: #839496;
2283 | --term-color-background: #002b36;
2284 | --term-color-0: #073642;
2285 | --term-color-1: #dc322f;
2286 | --term-color-2: #859900;
2287 | --term-color-3: #b58900;
2288 | --term-color-4: #268bd2;
2289 | --term-color-5: #d33682;
2290 | --term-color-6: #2aa198;
2291 | --term-color-7: #eee8d5;
2292 | --term-color-8: #002b36;
2293 | --term-color-9: #cb4b16;
2294 | --term-color-10: #586e75;
2295 | --term-color-11: #657b83;
2296 | --term-color-12: #839496;
2297 | --term-color-13: #6c71c4;
2298 | --term-color-14: #93a1a1;
2299 | --term-color-15: #fdf6e3;
2300 | }
2301 | /*
2302 | Based on Solarized Light: https://ethanschoonover.com/solarized/
2303 | */
2304 | .asciinema-player-theme-solarized-light {
2305 | --term-color-foreground: #657b83;
2306 | --term-color-background: #fdf6e3;
2307 | --term-color-0: #073642;
2308 | --term-color-1: #dc322f;
2309 | --term-color-2: #859900;
2310 | --term-color-3: #b58900;
2311 | --term-color-4: #268bd2;
2312 | --term-color-5: #d33682;
2313 | --term-color-6: #2aa198;
2314 | --term-color-7: #eee8d5;
2315 | --term-color-8: #002b36;
2316 | --term-color-9: #cb4b16;
2317 | --term-color-10: #586e75;
2318 | --term-color-11: #657c83;
2319 | --term-color-12: #839496;
2320 | --term-color-13: #6c71c4;
2321 | --term-color-14: #93a1a1;
2322 | --term-color-15: #fdf6e3;
2323 | }
2324 | .asciinema-player-theme-solarized-light .ap-overlay-start .ap-play-button svg .ap-play-btn-fill {
2325 | fill: var(--term-color-1);
2326 | }
2327 | .asciinema-player-theme-solarized-light .ap-overlay-start .ap-play-button svg .ap-play-btn-stroke {
2328 | stroke: var(--term-color-1);
2329 | }
2330 | /*
2331 | Based on Tango: https://en.wikipedia.org/wiki/Tango_Desktop_Project
2332 | */
2333 | .asciinema-player-theme-tango {
2334 | --term-color-foreground: #cccccc;
2335 | --term-color-background: #121314;
2336 | --term-color-0: #000000;
2337 | --term-color-1: #cc0000;
2338 | --term-color-2: #4e9a06;
2339 | --term-color-3: #c4a000;
2340 | --term-color-4: #3465a4;
2341 | --term-color-5: #75507b;
2342 | --term-color-6: #06989a;
2343 | --term-color-7: #d3d7cf;
2344 | --term-color-8: #555753;
2345 | --term-color-9: #ef2929;
2346 | --term-color-10: #8ae234;
2347 | --term-color-11: #fce94f;
2348 | --term-color-12: #729fcf;
2349 | --term-color-13: #ad7fa8;
2350 | --term-color-14: #34e2e2;
2351 | --term-color-15: #eeeeec;
2352 | }
2353 |
--------------------------------------------------------------------------------
/assets/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Live preview - ht
8 |
30 |
31 |
32 |
33 |
34 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-utils": {
4 | "inputs": {
5 | "systems": "systems"
6 | },
7 | "locked": {
8 | "lastModified": 1710146030,
9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
10 | "owner": "numtide",
11 | "repo": "flake-utils",
12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
13 | "type": "github"
14 | },
15 | "original": {
16 | "owner": "numtide",
17 | "repo": "flake-utils",
18 | "type": "github"
19 | }
20 | },
21 | "flake-utils_2": {
22 | "inputs": {
23 | "systems": "systems_2"
24 | },
25 | "locked": {
26 | "lastModified": 1705309234,
27 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
28 | "owner": "numtide",
29 | "repo": "flake-utils",
30 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
31 | "type": "github"
32 | },
33 | "original": {
34 | "owner": "numtide",
35 | "repo": "flake-utils",
36 | "type": "github"
37 | }
38 | },
39 | "nixpkgs": {
40 | "locked": {
41 | "lastModified": 1711715736,
42 | "narHash": "sha256-9slQ609YqT9bT/MNX9+5k5jltL9zgpn36DpFB7TkttM=",
43 | "owner": "nixos",
44 | "repo": "nixpkgs",
45 | "rev": "807c549feabce7eddbf259dbdcec9e0600a0660d",
46 | "type": "github"
47 | },
48 | "original": {
49 | "owner": "nixos",
50 | "ref": "nixpkgs-unstable",
51 | "repo": "nixpkgs",
52 | "type": "github"
53 | }
54 | },
55 | "nixpkgs_2": {
56 | "locked": {
57 | "lastModified": 1706487304,
58 | "narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=",
59 | "owner": "NixOS",
60 | "repo": "nixpkgs",
61 | "rev": "90f456026d284c22b3e3497be980b2e47d0b28ac",
62 | "type": "github"
63 | },
64 | "original": {
65 | "owner": "NixOS",
66 | "ref": "nixpkgs-unstable",
67 | "repo": "nixpkgs",
68 | "type": "github"
69 | }
70 | },
71 | "root": {
72 | "inputs": {
73 | "flake-utils": "flake-utils",
74 | "nixpkgs": "nixpkgs",
75 | "rust-overlay": "rust-overlay"
76 | }
77 | },
78 | "rust-overlay": {
79 | "inputs": {
80 | "flake-utils": "flake-utils_2",
81 | "nixpkgs": "nixpkgs_2"
82 | },
83 | "locked": {
84 | "lastModified": 1717985971,
85 | "narHash": "sha256-24h/qKp0aeI+Ew13WdRF521kY24PYa5HOvw0mlrABjk=",
86 | "owner": "oxalica",
87 | "repo": "rust-overlay",
88 | "rev": "abfe5b3126b1b7e9e4daafc1c6478d17f0b584e7",
89 | "type": "github"
90 | },
91 | "original": {
92 | "owner": "oxalica",
93 | "repo": "rust-overlay",
94 | "type": "github"
95 | }
96 | },
97 | "systems": {
98 | "locked": {
99 | "lastModified": 1681028828,
100 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
101 | "owner": "nix-systems",
102 | "repo": "default",
103 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
104 | "type": "github"
105 | },
106 | "original": {
107 | "owner": "nix-systems",
108 | "repo": "default",
109 | "type": "github"
110 | }
111 | },
112 | "systems_2": {
113 | "locked": {
114 | "lastModified": 1681028828,
115 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
116 | "owner": "nix-systems",
117 | "repo": "default",
118 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
119 | "type": "github"
120 | },
121 | "original": {
122 | "owner": "nix-systems",
123 | "repo": "default",
124 | "type": "github"
125 | }
126 | }
127 | },
128 | "root": "root",
129 | "version": 7
130 | }
131 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "ht";
3 |
4 | inputs = {
5 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
6 | rust-overlay.url = "github:oxalica/rust-overlay";
7 | flake-utils.url = "github:numtide/flake-utils";
8 | };
9 |
10 | outputs =
11 | {
12 | self,
13 | nixpkgs,
14 | rust-overlay,
15 | flake-utils,
16 | }:
17 | flake-utils.lib.eachDefaultSystem (
18 | system:
19 | let
20 | overlays = [ (import rust-overlay) ];
21 | pkgs = import nixpkgs { inherit system overlays; };
22 | in
23 | {
24 | devShells.default = pkgs.mkShell {
25 | nativeBuildInputs =
26 | with pkgs;
27 | [
28 | (rust-bin.stable."1.74.0".default.override { extensions = [ "rust-src" ]; })
29 | bashInteractive
30 | ]
31 | ++ (lib.optionals stdenv.isDarwin [
32 | libiconv
33 | darwin.apple_sdk.frameworks.Foundation
34 | ]);
35 | };
36 | }
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/src/api.rs:
--------------------------------------------------------------------------------
1 | pub mod http;
2 | pub mod stdio;
3 | use std::str::FromStr;
4 |
5 | #[derive(Debug, Default, Copy, Clone)]
6 | pub struct Subscription {
7 | init: bool,
8 | snapshot: bool,
9 | resize: bool,
10 | output: bool,
11 | }
12 |
13 | impl FromStr for Subscription {
14 | type Err = String;
15 |
16 | fn from_str(s: &str) -> Result {
17 | let mut sub = Subscription::default();
18 |
19 | for event in s.split(',') {
20 | match event {
21 | "init" => sub.init = true,
22 | "output" => sub.output = true,
23 | "resize" => sub.resize = true,
24 | "snapshot" => sub.snapshot = true,
25 | _ => return Err(format!("invalid event name: {event}")),
26 | }
27 | }
28 |
29 | Ok(sub)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/api/http.rs:
--------------------------------------------------------------------------------
1 | use super::Subscription;
2 | use crate::session;
3 | use anyhow::Result;
4 | use axum::{
5 | extract::{connect_info::ConnectInfo, ws, Query, State},
6 | http::{header, StatusCode, Uri},
7 | response::IntoResponse,
8 | routing::get,
9 | Router,
10 | };
11 | use futures_util::{sink, stream, StreamExt};
12 | use rust_embed::RustEmbed;
13 | use serde::Deserialize;
14 | use serde_json::json;
15 | use std::borrow::Cow;
16 | use std::future::{self, Future, IntoFuture};
17 | use std::io;
18 | use std::net::{SocketAddr, TcpListener};
19 | use tokio::sync::mpsc;
20 | use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
21 |
22 | #[derive(RustEmbed)]
23 | #[folder = "assets/"]
24 | struct Assets;
25 |
26 | pub async fn start(
27 | listener: TcpListener,
28 | clients_tx: mpsc::Sender,
29 | ) -> Result>> {
30 | listener.set_nonblocking(true)?;
31 | let listener = tokio::net::TcpListener::from_std(listener)?;
32 | let addr = listener.local_addr().unwrap();
33 | eprintln!("HTTP server listening on {addr}");
34 | eprintln!("live preview available at http://{addr}");
35 |
36 | let app: Router<()> = Router::new()
37 | .route("/ws/alis", get(alis_handler))
38 | .route("/ws/events", get(event_stream_handler))
39 | .with_state(clients_tx)
40 | .fallback(static_handler);
41 |
42 | Ok(axum::serve(
43 | listener,
44 | app.into_make_service_with_connect_info::(),
45 | )
46 | .into_future())
47 | }
48 |
49 | /// ALiS protocol handler
50 | ///
51 | /// This endpoint implements ALiS (asciinema live stream) protocol (https://docs.asciinema.org/manual/alis/).
52 | /// It allows pointing asciinema player directly to ht to get a real-time terminal preview.
53 | async fn alis_handler(
54 | ws: ws::WebSocketUpgrade,
55 | ConnectInfo(_addr): ConnectInfo,
56 | State(clients_tx): State>,
57 | ) -> impl IntoResponse {
58 | ws.on_upgrade(move |socket| async move {
59 | let _ = handle_alis_socket(socket, clients_tx).await;
60 | })
61 | }
62 |
63 | async fn handle_alis_socket(
64 | socket: ws::WebSocket,
65 | clients_tx: mpsc::Sender,
66 | ) -> Result<()> {
67 | let (sink, stream) = socket.split();
68 | let drainer = tokio::spawn(stream.map(Ok).forward(sink::drain()));
69 |
70 | let result = session::stream(&clients_tx)
71 | .await?
72 | .filter_map(alis_message)
73 | .chain(stream::once(future::ready(Ok(close_message()))))
74 | .forward(sink)
75 | .await;
76 |
77 | drainer.abort();
78 | result?;
79 |
80 | Ok(())
81 | }
82 |
83 | async fn alis_message(
84 | event: Result,
85 | ) -> Option> {
86 | use session::Event::*;
87 |
88 | match event {
89 | Ok(Init(time, cols, rows, seq, _text)) => Some(Ok(json_message(json!({
90 | "time": time,
91 | "cols": cols,
92 | "rows": rows,
93 | "init": seq,
94 | })))),
95 |
96 | Ok(Output(time, data)) => Some(Ok(json_message(json!([time, "o", data])))),
97 |
98 | Ok(Resize(time, cols, rows)) => Some(Ok(json_message(json!([
99 | time,
100 | "r",
101 | format!("{cols}x{rows}")
102 | ])))),
103 |
104 | Ok(Snapshot(_, _, _, _)) => None,
105 |
106 | Err(e) => Some(Err(axum::Error::new(e))),
107 | }
108 | }
109 |
110 | #[derive(Debug, Deserialize)]
111 | struct EventsParams {
112 | sub: Option,
113 | }
114 |
115 | /// Event stream handler
116 | ///
117 | /// This endpoint allows the client to subscribe to selected events and have them delivered as they occur.
118 | /// Query param `sub` should be set to a comma-separated list desired of events.
119 | /// See above for a list of supported events.
120 | async fn event_stream_handler(
121 | ws: ws::WebSocketUpgrade,
122 | Query(params): Query,
123 | ConnectInfo(_addr): ConnectInfo,
124 | State(clients_tx): State>,
125 | ) -> impl IntoResponse {
126 | let sub: Subscription = params.sub.unwrap_or_default().parse().unwrap_or_default();
127 |
128 | ws.on_upgrade(move |socket| async move {
129 | let _ = handle_event_stream_socket(socket, clients_tx, sub).await;
130 | })
131 | }
132 |
133 | async fn handle_event_stream_socket(
134 | socket: ws::WebSocket,
135 | clients_tx: mpsc::Sender,
136 | sub: Subscription,
137 | ) -> Result<()> {
138 | let (sink, stream) = socket.split();
139 | let drainer = tokio::spawn(stream.map(Ok).forward(sink::drain()));
140 |
141 | let result = session::stream(&clients_tx)
142 | .await?
143 | .filter_map(move |e| event_stream_message(e, sub))
144 | .chain(stream::once(future::ready(Ok(close_message()))))
145 | .forward(sink)
146 | .await;
147 |
148 | drainer.abort();
149 | result?;
150 |
151 | Ok(())
152 | }
153 |
154 | async fn event_stream_message(
155 | event: Result,
156 | sub: Subscription,
157 | ) -> Option> {
158 | use session::Event::*;
159 |
160 | match event {
161 | Ok(e @ Init(_, _, _, _, _)) if sub.init => Some(Ok(json_message(e.to_json()))),
162 | Ok(e @ Output(_, _)) if sub.output => Some(Ok(json_message(e.to_json()))),
163 | Ok(e @ Resize(_, _, _)) if sub.resize => Some(Ok(json_message(e.to_json()))),
164 | Ok(e @ Snapshot(_, _, _, _)) if sub.snapshot => Some(Ok(json_message(e.to_json()))),
165 | Ok(_) => None,
166 | Err(e) => Some(Err(axum::Error::new(e))),
167 | }
168 | }
169 |
170 | fn json_message(value: serde_json::Value) -> ws::Message {
171 | ws::Message::Text(value.to_string())
172 | }
173 |
174 | fn close_message() -> ws::Message {
175 | ws::Message::Close(Some(ws::CloseFrame {
176 | code: ws::close_code::NORMAL,
177 | reason: Cow::from("ended"),
178 | }))
179 | }
180 |
181 | async fn static_handler(uri: Uri) -> impl IntoResponse {
182 | let mut path = uri.path().trim_start_matches('/');
183 |
184 | if path.is_empty() {
185 | path = "index.html";
186 | }
187 |
188 | match Assets::get(path) {
189 | Some(content) => {
190 | let mime = mime_guess::from_path(path).first_or_octet_stream();
191 |
192 | ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
193 | }
194 |
195 | None => (StatusCode::NOT_FOUND, "404").into_response(),
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/src/api/stdio.rs:
--------------------------------------------------------------------------------
1 | use super::Subscription;
2 | use crate::command::{self, Command, InputSeq};
3 | use crate::session;
4 | use anyhow::Result;
5 | use serde::{de::DeserializeOwned, Deserialize};
6 | use std::io;
7 | use std::thread;
8 | use tokio::sync::mpsc;
9 | use tokio_stream::StreamExt;
10 |
11 | #[derive(Debug, Deserialize)]
12 | struct InputArgs {
13 | payload: String,
14 | }
15 |
16 | #[derive(Debug, Deserialize)]
17 | struct SendKeysArgs {
18 | keys: Vec,
19 | }
20 |
21 | #[derive(Debug, Deserialize)]
22 | struct ResizeArgs {
23 | cols: usize,
24 | rows: usize,
25 | }
26 |
27 | pub async fn start(
28 | command_tx: mpsc::Sender,
29 | clients_tx: mpsc::Sender,
30 | sub: Subscription,
31 | ) -> Result<()> {
32 | let (input_tx, mut input_rx) = mpsc::unbounded_channel();
33 | thread::spawn(|| read_stdin(input_tx));
34 | let mut events = session::stream(&clients_tx).await?;
35 |
36 | loop {
37 | tokio::select! {
38 | line = input_rx.recv() => {
39 | match line {
40 | Some(line) => {
41 | match parse_line(&line) {
42 | Ok(command) => command_tx.send(command).await?,
43 | Err(e) => eprintln!("command parse error: {e}"),
44 | }
45 | }
46 |
47 | None => break
48 | }
49 | }
50 |
51 | event = events.next() => {
52 | use session::Event::*;
53 |
54 | match event {
55 | Some(Ok(e @ Init(_, _, _, _, _))) if sub.init => {
56 | println!("{}", e.to_json().to_string());
57 | }
58 |
59 | Some(Ok(e @ Output(_, _))) if sub.output => {
60 | println!("{}", e.to_json().to_string());
61 | }
62 |
63 | Some(Ok(e @ Resize(_, _, _))) if sub.resize => {
64 | println!("{}", e.to_json().to_string());
65 | }
66 |
67 | Some(Ok(e @ Snapshot(_, _, _, _))) if sub.snapshot => {
68 | println!("{}", e.to_json().to_string());
69 | }
70 |
71 | Some(_) => (),
72 |
73 | None => break
74 | }
75 | }
76 | }
77 | }
78 |
79 | Ok(())
80 | }
81 |
82 | fn read_stdin(input_tx: mpsc::UnboundedSender) -> Result<()> {
83 | for line in io::stdin().lines() {
84 | input_tx.send(line?)?;
85 | }
86 |
87 | Ok(())
88 | }
89 |
90 | fn parse_line(line: &str) -> Result {
91 | serde_json::from_str::(line)
92 | .map_err(|e| e.to_string())
93 | .and_then(build_command)
94 | }
95 |
96 | fn build_command(value: serde_json::Value) -> Result {
97 | match value["type"].as_str() {
98 | Some("input") => {
99 | let args: InputArgs = args_from_json_value(value)?;
100 | Ok(Command::Input(vec![standard_key(args.payload)]))
101 | }
102 |
103 | Some("sendKeys") => {
104 | let args: SendKeysArgs = args_from_json_value(value)?;
105 | let seqs = args.keys.into_iter().map(parse_key).collect();
106 | Ok(Command::Input(seqs))
107 | }
108 |
109 | Some("resize") => {
110 | let args: ResizeArgs = args_from_json_value(value)?;
111 | Ok(Command::Resize(args.cols, args.rows))
112 | }
113 |
114 | Some("takeSnapshot") => Ok(Command::Snapshot),
115 |
116 | other => Err(format!("invalid command type: {other:?}")),
117 | }
118 | }
119 |
120 | fn args_from_json_value(value: serde_json::Value) -> Result
121 | where
122 | T: DeserializeOwned,
123 | {
124 | serde_json::from_value(value).map_err(|e| e.to_string())
125 | }
126 |
127 | fn standard_key(seq: S) -> InputSeq {
128 | InputSeq::Standard(seq.to_string())
129 | }
130 |
131 | fn cursor_key(seq1: S, seq2: S) -> InputSeq {
132 | InputSeq::Cursor(seq1.to_string(), seq2.to_string())
133 | }
134 |
135 | fn parse_key(key: String) -> InputSeq {
136 | let seq = match key.as_str() {
137 | "C-@" | "C-Space" | "^@" => "\x00",
138 | "C-[" | "Escape" | "^[" => "\x1b",
139 | "C-\\" | "^\\" => "\x1c",
140 | "C-]" | "^]" => "\x1d",
141 | "C-^" | "C-/" => "\x1e",
142 | "C--" | "C-_" => "\x1f",
143 | "Tab" => "\x09", // same as C-i
144 | "Enter" => "\x0d", // same as C-m
145 | "Space" => " ",
146 | "Left" => return cursor_key("\x1b[D", "\x1bOD"),
147 | "Right" => return cursor_key("\x1b[C", "\x1bOC"),
148 | "Up" => return cursor_key("\x1b[A", "\x1bOA"),
149 | "Down" => return cursor_key("\x1b[B", "\x1bOB"),
150 | "C-Left" => "\x1b[1;5D",
151 | "C-Right" => "\x1b[1;5C",
152 | "S-Left" => "\x1b[1;2D",
153 | "S-Right" => "\x1b[1;2C",
154 | "C-Up" => "\x1b[1;5A",
155 | "C-Down" => "\x1b[1;5B",
156 | "S-Up" => "\x1b[1;2A",
157 | "S-Down" => "\x1b[1;2B",
158 | "A-Left" => "\x1b[1;3D",
159 | "A-Right" => "\x1b[1;3C",
160 | "A-Up" => "\x1b[1;3A",
161 | "A-Down" => "\x1b[1;3B",
162 | "C-S-Left" | "S-C-Left" => "\x1b[1;6D",
163 | "C-S-Right" | "S-C-Right" => "\x1b[1;6C",
164 | "C-S-Up" | "S-C-Up" => "\x1b[1;6A",
165 | "C-S-Down" | "S-C-Down" => "\x1b[1;6B",
166 | "C-A-Left" | "A-C-Left" => "\x1b[1;7D",
167 | "C-A-Right" | "A-C-Right" => "\x1b[1;7C",
168 | "C-A-Up" | "A-C-Up" => "\x1b[1;7A",
169 | "C-A-Down" | "A-C-Down" => "\x1b[1;7B",
170 | "A-S-Left" | "S-A-Left" => "\x1b[1;4D",
171 | "A-S-Right" | "S-A-Right" => "\x1b[1;4C",
172 | "A-S-Up" | "S-A-Up" => "\x1b[1;4A",
173 | "A-S-Down" | "S-A-Down" => "\x1b[1;4B",
174 | "C-A-S-Left" | "C-S-A-Left" | "A-C-S-Left" | "S-C-A-Left" | "A-S-C-Left" | "S-A-C-Left" => {
175 | "\x1b[1;8D"
176 | }
177 | "C-A-S-Right" | "C-S-A-Right" | "A-C-S-Right" | "S-C-A-Right" | "A-S-C-Right"
178 | | "S-A-C-Right" => "\x1b[1;8C",
179 | "C-A-S-Up" | "C-S-A-Up" | "A-C-S-Up" | "S-C-A-Up" | "A-S-C-Up" | "S-A-C-Up" => "\x1b[1;8A",
180 | "C-A-S-Down" | "C-S-A-Down" | "A-C-S-Down" | "S-C-A-Down" | "A-S-C-Down" | "S-A-C-Down" => {
181 | "\x1b[1;8B"
182 | }
183 | "F1" => "\x1bOP",
184 | "F2" => "\x1bOQ",
185 | "F3" => "\x1bOR",
186 | "F4" => "\x1bOS",
187 | "F5" => "\x1b[15~",
188 | "F6" => "\x1b[17~",
189 | "F7" => "\x1b[18~",
190 | "F8" => "\x1b[19~",
191 | "F9" => "\x1b[20~",
192 | "F10" => "\x1b[21~",
193 | "F11" => "\x1b[23~",
194 | "F12" => "\x1b[24~",
195 | "C-F1" => "\x1b[1;5P",
196 | "C-F2" => "\x1b[1;5Q",
197 | "C-F3" => "\x1b[1;5R",
198 | "C-F4" => "\x1b[1;5S",
199 | "C-F5" => "\x1b[15;5~",
200 | "C-F6" => "\x1b[17;5~",
201 | "C-F7" => "\x1b[18;5~",
202 | "C-F8" => "\x1b[19;5~",
203 | "C-F9" => "\x1b[20;5~",
204 | "C-F10" => "\x1b[21;5~",
205 | "C-F11" => "\x1b[23;5~",
206 | "C-F12" => "\x1b[24;5~",
207 | "S-F1" => "\x1b[1;2P",
208 | "S-F2" => "\x1b[1;2Q",
209 | "S-F3" => "\x1b[1;2R",
210 | "S-F4" => "\x1b[1;2S",
211 | "S-F5" => "\x1b[15;2~",
212 | "S-F6" => "\x1b[17;2~",
213 | "S-F7" => "\x1b[18;2~",
214 | "S-F8" => "\x1b[19;2~",
215 | "S-F9" => "\x1b[20;2~",
216 | "S-F10" => "\x1b[21;2~",
217 | "S-F11" => "\x1b[23;2~",
218 | "S-F12" => "\x1b[24;2~",
219 | "A-F1" => "\x1b[1;3P",
220 | "A-F2" => "\x1b[1;3Q",
221 | "A-F3" => "\x1b[1;3R",
222 | "A-F4" => "\x1b[1;3S",
223 | "A-F5" => "\x1b[15;3~",
224 | "A-F6" => "\x1b[17;3~",
225 | "A-F7" => "\x1b[18;3~",
226 | "A-F8" => "\x1b[19;3~",
227 | "A-F9" => "\x1b[20;3~",
228 | "A-F10" => "\x1b[21;3~",
229 | "A-F11" => "\x1b[23;3~",
230 | "A-F12" => "\x1b[24;3~",
231 | "Home" => return cursor_key("\x1b[H", "\x1bOH"),
232 | "C-Home" => "\x1b[1;5H",
233 | "S-Home" => "\x1b[1;2H",
234 | "A-Home" => "\x1b[1;3H",
235 | "End" => return cursor_key("\x1b[F", "\x1bOF"),
236 | "C-End" => "\x1b[1;5F",
237 | "S-End" => "\x1b[1;2F",
238 | "A-End" => "\x1b[1;3F",
239 | "PageUp" => "\x1b[5~",
240 | "C-PageUp" => "\x1b[5;5~",
241 | "S-PageUp" => "\x1b[5;2~",
242 | "A-PageUp" => "\x1b[5;3~",
243 | "PageDown" => "\x1b[6~",
244 | "C-PageDown" => "\x1b[6;5~",
245 | "S-PageDown" => "\x1b[6;2~",
246 | "A-PageDown" => "\x1b[6;3~",
247 |
248 | k => {
249 | let chars: Vec = k.chars().collect();
250 |
251 | match chars.as_slice() {
252 | ['C', '-', k @ 'a'..='z'] => {
253 | return standard_key((*k as u8 - 0x60) as char);
254 | }
255 |
256 | ['C', '-', k @ 'A'..='Z'] => {
257 | return standard_key((*k as u8 - 0x40) as char);
258 | }
259 |
260 | ['^', k @ 'a'..='z'] => {
261 | return standard_key((*k as u8 - 0x60) as char);
262 | }
263 |
264 | ['^', k @ 'A'..='Z'] => {
265 | return standard_key((*k as u8 - 0x40) as char);
266 | }
267 |
268 | ['A', '-', k] => {
269 | return standard_key(format!("\x1b{}", k));
270 | }
271 |
272 | _ => &key,
273 | }
274 | }
275 | };
276 |
277 | standard_key(seq)
278 | }
279 |
280 | #[cfg(test)]
281 | mod test {
282 | use super::{cursor_key, parse_line, standard_key, Command};
283 | use crate::command::InputSeq;
284 |
285 | #[test]
286 | fn parse_input() {
287 | let command = parse_line(r#"{ "type": "input", "payload": "hello" }"#).unwrap();
288 | assert!(matches!(command, Command::Input(input) if input == vec![standard_key("hello")]));
289 | }
290 |
291 | #[test]
292 | fn parse_input_missing_args() {
293 | parse_line(r#"{ "type": "input" }"#).expect_err("should fail");
294 | }
295 |
296 | #[test]
297 | fn parse_send_keys() {
298 | let examples = [
299 | ["hello", "hello"],
300 | ["C-@", "\x00"],
301 | ["C-a", "\x01"],
302 | ["C-A", "\x01"],
303 | ["^a", "\x01"],
304 | ["^A", "\x01"],
305 | ["C-z", "\x1a"],
306 | ["C-Z", "\x1a"],
307 | ["C-[", "\x1b"],
308 | ["Space", " "],
309 | ["C-Space", "\x00"],
310 | ["Tab", "\x09"],
311 | ["Enter", "\x0d"],
312 | ["Escape", "\x1b"],
313 | ["^[", "\x1b"],
314 | ["C-Left", "\x1b[1;5D"],
315 | ["C-Right", "\x1b[1;5C"],
316 | ["S-Left", "\x1b[1;2D"],
317 | ["S-Right", "\x1b[1;2C"],
318 | ["C-Up", "\x1b[1;5A"],
319 | ["C-Down", "\x1b[1;5B"],
320 | ["S-Up", "\x1b[1;2A"],
321 | ["S-Down", "\x1b[1;2B"],
322 | ["A-Left", "\x1b[1;3D"],
323 | ["A-Right", "\x1b[1;3C"],
324 | ["A-Up", "\x1b[1;3A"],
325 | ["A-Down", "\x1b[1;3B"],
326 | ["C-S-Left", "\x1b[1;6D"],
327 | ["C-S-Right", "\x1b[1;6C"],
328 | ["C-S-Up", "\x1b[1;6A"],
329 | ["C-S-Down", "\x1b[1;6B"],
330 | ["C-A-Left", "\x1b[1;7D"],
331 | ["C-A-Right", "\x1b[1;7C"],
332 | ["C-A-Up", "\x1b[1;7A"],
333 | ["C-A-Down", "\x1b[1;7B"],
334 | ["S-A-Left", "\x1b[1;4D"],
335 | ["S-A-Right", "\x1b[1;4C"],
336 | ["S-A-Up", "\x1b[1;4A"],
337 | ["S-A-Down", "\x1b[1;4B"],
338 | ["C-A-S-Left", "\x1b[1;8D"],
339 | ["C-A-S-Right", "\x1b[1;8C"],
340 | ["C-A-S-Up", "\x1b[1;8A"],
341 | ["C-A-S-Down", "\x1b[1;8B"],
342 | ["A-a", "\x1ba"],
343 | ["A-A", "\x1bA"],
344 | ["A-z", "\x1bz"],
345 | ["A-Z", "\x1bZ"],
346 | ["A-1", "\x1b1"],
347 | ["A-!", "\x1b!"],
348 | ["F1", "\x1bOP"],
349 | ["F2", "\x1bOQ"],
350 | ["F3", "\x1bOR"],
351 | ["F4", "\x1bOS"],
352 | ["F5", "\x1b[15~"],
353 | ["F6", "\x1b[17~"],
354 | ["F7", "\x1b[18~"],
355 | ["F8", "\x1b[19~"],
356 | ["F9", "\x1b[20~"],
357 | ["F10", "\x1b[21~"],
358 | ["F11", "\x1b[23~"],
359 | ["F12", "\x1b[24~"],
360 | ["C-F1", "\x1b[1;5P"],
361 | ["C-F2", "\x1b[1;5Q"],
362 | ["C-F3", "\x1b[1;5R"],
363 | ["C-F4", "\x1b[1;5S"],
364 | ["C-F5", "\x1b[15;5~"],
365 | ["C-F6", "\x1b[17;5~"],
366 | ["C-F7", "\x1b[18;5~"],
367 | ["C-F8", "\x1b[19;5~"],
368 | ["C-F9", "\x1b[20;5~"],
369 | ["C-F10", "\x1b[21;5~"],
370 | ["C-F11", "\x1b[23;5~"],
371 | ["C-F12", "\x1b[24;5~"],
372 | ["S-F1", "\x1b[1;2P"],
373 | ["S-F2", "\x1b[1;2Q"],
374 | ["S-F3", "\x1b[1;2R"],
375 | ["S-F4", "\x1b[1;2S"],
376 | ["S-F5", "\x1b[15;2~"],
377 | ["S-F6", "\x1b[17;2~"],
378 | ["S-F7", "\x1b[18;2~"],
379 | ["S-F8", "\x1b[19;2~"],
380 | ["S-F9", "\x1b[20;2~"],
381 | ["S-F10", "\x1b[21;2~"],
382 | ["S-F11", "\x1b[23;2~"],
383 | ["S-F12", "\x1b[24;2~"],
384 | ["A-F1", "\x1b[1;3P"],
385 | ["A-F2", "\x1b[1;3Q"],
386 | ["A-F3", "\x1b[1;3R"],
387 | ["A-F4", "\x1b[1;3S"],
388 | ["A-F5", "\x1b[15;3~"],
389 | ["A-F6", "\x1b[17;3~"],
390 | ["A-F7", "\x1b[18;3~"],
391 | ["A-F8", "\x1b[19;3~"],
392 | ["A-F9", "\x1b[20;3~"],
393 | ["A-F10", "\x1b[21;3~"],
394 | ["A-F11", "\x1b[23;3~"],
395 | ["A-F12", "\x1b[24;3~"],
396 | ["C-Home", "\x1b[1;5H"],
397 | ["S-Home", "\x1b[1;2H"],
398 | ["A-Home", "\x1b[1;3H"],
399 | ["C-End", "\x1b[1;5F"],
400 | ["S-End", "\x1b[1;2F"],
401 | ["A-End", "\x1b[1;3F"],
402 | ["PageUp", "\x1b[5~"],
403 | ["C-PageUp", "\x1b[5;5~"],
404 | ["S-PageUp", "\x1b[5;2~"],
405 | ["A-PageUp", "\x1b[5;3~"],
406 | ["PageDown", "\x1b[6~"],
407 | ["C-PageDown", "\x1b[6;5~"],
408 | ["S-PageDown", "\x1b[6;2~"],
409 | ["A-PageDown", "\x1b[6;3~"],
410 | ];
411 |
412 | for [key, chars] in examples {
413 | let command = parse_line(&format!(
414 | "{{ \"type\": \"sendKeys\", \"keys\": [\"{key}\"] }}"
415 | ))
416 | .unwrap();
417 |
418 | assert!(matches!(command, Command::Input(input) if input == vec![standard_key(chars)]));
419 | }
420 |
421 | let command = parse_line(
422 | r#"{ "type": "sendKeys", "keys": ["hello", "Enter", "C-c", "A-^", "Left"] }"#,
423 | )
424 | .unwrap();
425 |
426 | assert!(
427 | matches!(command, Command::Input(input) if input == vec![standard_key("hello"), standard_key("\x0d"), standard_key("\x03"), standard_key("\x1b^"), cursor_key("\x1b[D", "\x1bOD")])
428 | );
429 | }
430 |
431 | #[test]
432 | fn parse_cursor_keys() {
433 | let examples = [
434 | ["Left", "\x1b[D", "\x1bOD"],
435 | ["Right", "\x1b[C", "\x1bOC"],
436 | ["Up", "\x1b[A", "\x1bOA"],
437 | ["Down", "\x1b[B", "\x1bOB"],
438 | ["Home", "\x1b[H", "\x1bOH"],
439 | ["End", "\x1b[F", "\x1bOF"],
440 | ];
441 |
442 | for [key, seq1, seq2] in examples {
443 | let command = parse_line(&format!(
444 | "{{ \"type\": \"sendKeys\", \"keys\": [\"{key}\"] }}"
445 | ))
446 | .unwrap();
447 |
448 | if let Command::Input(seqs) = command {
449 | if let InputSeq::Cursor(seq3, seq4) = &seqs[0] {
450 | if seq1 == seq3 && seq2 == seq4 {
451 | continue;
452 | }
453 |
454 | panic!("expected {:?} {:?}, got {:?} {:?}", seq1, seq2, seq3, seq4);
455 | }
456 | }
457 |
458 | panic!("expected {:?} {:?}", seq1, seq2);
459 | }
460 | }
461 |
462 | #[test]
463 | fn parse_send_keys_missing_args() {
464 | parse_line(r#"{ "type": "sendKeys" }"#).expect_err("should fail");
465 | }
466 |
467 | #[test]
468 | fn parse_resize() {
469 | let command = parse_line(r#"{ "type": "resize", "cols": 80, "rows": 24 }"#).unwrap();
470 | assert!(matches!(command, Command::Resize(80, 24)));
471 | }
472 |
473 | #[test]
474 | fn parse_resize_missing_args() {
475 | parse_line(r#"{ "type": "resize" }"#).expect_err("should fail");
476 | }
477 |
478 | #[test]
479 | fn parse_take_snapshot() {
480 | let command = parse_line(r#"{ "type": "takeSnapshot" }"#).unwrap();
481 | assert!(matches!(command, Command::Snapshot));
482 | }
483 |
484 | #[test]
485 | fn parse_invalid_json() {
486 | parse_line("{").expect_err("should fail");
487 | }
488 | }
489 |
--------------------------------------------------------------------------------
/src/cli.rs:
--------------------------------------------------------------------------------
1 | use crate::api::Subscription;
2 | use anyhow::bail;
3 | use clap::Parser;
4 | use nix::pty;
5 | use std::{fmt::Display, net::SocketAddr, ops::Deref, str::FromStr};
6 |
7 | #[derive(Debug, Parser)]
8 | #[clap(version, about)]
9 | #[command(name = "ht")]
10 | pub struct Cli {
11 | /// Terminal size
12 | #[arg(long, value_name = "COLSxROWS", default_value = Some("120x40"))]
13 | pub size: Size,
14 |
15 | /// Command to run inside the terminal
16 | #[arg(default_value = "bash")]
17 | pub command: Vec,
18 |
19 | /// Enable HTTP server
20 | #[arg(short, long, value_name = "LISTEN_ADDR", default_missing_value = "127.0.0.1:0", num_args = 0..=1)]
21 | pub listen: Option,
22 |
23 | /// Subscribe to events
24 | #[arg(long, value_name = "EVENTS")]
25 | pub subscribe: Option,
26 | }
27 |
28 | impl Cli {
29 | pub fn new() -> Self {
30 | Cli::parse()
31 | }
32 | }
33 |
34 | #[derive(Debug, Clone)]
35 | pub struct Size(pty::Winsize);
36 |
37 | impl Size {
38 | pub fn cols(&self) -> usize {
39 | self.0.ws_col as usize
40 | }
41 |
42 | pub fn rows(&self) -> usize {
43 | self.0.ws_row as usize
44 | }
45 | }
46 |
47 | impl FromStr for Size {
48 | type Err = anyhow::Error;
49 |
50 | fn from_str(s: &str) -> std::prelude::v1::Result {
51 | match s.split_once('x') {
52 | Some((cols, rows)) => {
53 | let cols: u16 = cols.parse()?;
54 | let rows: u16 = rows.parse()?;
55 |
56 | let winsize = pty::Winsize {
57 | ws_col: cols,
58 | ws_row: rows,
59 | ws_xpixel: 0,
60 | ws_ypixel: 0,
61 | };
62 |
63 | Ok(Size(winsize))
64 | }
65 |
66 | None => {
67 | bail!("invalid size format: {s}");
68 | }
69 | }
70 | }
71 | }
72 |
73 | impl Deref for Size {
74 | type Target = pty::Winsize;
75 |
76 | fn deref(&self) -> &Self::Target {
77 | &self.0
78 | }
79 | }
80 |
81 | impl Display for Size {
82 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 | write!(f, "{}x{}", self.0.ws_col, self.0.ws_row)
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/command.rs:
--------------------------------------------------------------------------------
1 | #[derive(Debug)]
2 | pub enum Command {
3 | Input(Vec),
4 | Snapshot,
5 | Resize(usize, usize),
6 | }
7 |
8 | #[derive(Debug, PartialEq)]
9 | pub enum InputSeq {
10 | Standard(String),
11 | Cursor(String, String),
12 | }
13 |
14 | pub fn seqs_to_bytes(seqs: &[InputSeq], app_mode: bool) -> Vec {
15 | let mut bytes = Vec::new();
16 |
17 | for seq in seqs {
18 | bytes.extend_from_slice(seq_as_bytes(seq, app_mode));
19 | }
20 |
21 | bytes
22 | }
23 |
24 | fn seq_as_bytes(seq: &InputSeq, app_mode: bool) -> &[u8] {
25 | match (seq, app_mode) {
26 | (InputSeq::Standard(seq), _) => seq.as_bytes(),
27 | (InputSeq::Cursor(seq1, _seq2), false) => seq1.as_bytes(),
28 | (InputSeq::Cursor(_seq1, seq2), true) => seq2.as_bytes(),
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/locale.rs:
--------------------------------------------------------------------------------
1 | use nix::libc::{self, CODESET, LC_ALL};
2 | use std::env;
3 | use std::ffi::CStr;
4 |
5 | pub fn check_utf8_locale() -> anyhow::Result<()> {
6 | initialize_from_env();
7 |
8 | let encoding = get_encoding();
9 |
10 | if ["US-ASCII", "UTF-8"].contains(&encoding.as_str()) {
11 | Ok(())
12 | } else {
13 | let env = env::var("LC_ALL")
14 | .map(|v| format!("LC_ALL={}", v))
15 | .or(env::var("LC_CTYPE").map(|v| format!("LC_CTYPE={}", v)))
16 | .or(env::var("LANG").map(|v| format!("LANG={}", v)))
17 | .unwrap_or("".to_string());
18 |
19 | Err(anyhow::anyhow!("ASCII or UTF-8 character encoding required. The environment ({}) specifies the character set \"{}\". Check the output of `locale` command.", env, encoding))
20 | }
21 | }
22 |
23 | pub fn initialize_from_env() {
24 | unsafe {
25 | libc::setlocale(LC_ALL, b"\0".as_ptr() as *const libc::c_char);
26 | };
27 | }
28 |
29 | fn get_encoding() -> String {
30 | let codeset = unsafe { CStr::from_ptr(libc::nl_langinfo(CODESET)) };
31 |
32 | let mut encoding = codeset
33 | .to_str()
34 | .expect("Locale codeset name is not a valid UTF-8 string")
35 | .to_owned();
36 |
37 | if encoding == "ANSI_X3.4-1968" {
38 | encoding = "US-ASCII".to_owned();
39 | }
40 |
41 | encoding
42 | }
43 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | mod api;
2 | mod cli;
3 | mod command;
4 | mod locale;
5 | mod nbio;
6 | mod pty;
7 | mod session;
8 | use anyhow::{Context, Result};
9 | use command::Command;
10 | use session::Session;
11 | use std::net::{SocketAddr, TcpListener};
12 | use tokio::{sync::mpsc, task::JoinHandle};
13 |
14 | #[tokio::main]
15 | async fn main() -> Result<()> {
16 | locale::check_utf8_locale()?;
17 | let cli = cli::Cli::new();
18 |
19 | let (input_tx, input_rx) = mpsc::channel(1024);
20 | let (output_tx, output_rx) = mpsc::channel(1024);
21 | let (command_tx, command_rx) = mpsc::channel(1024);
22 | let (clients_tx, clients_rx) = mpsc::channel(1);
23 |
24 | start_http_api(cli.listen, clients_tx.clone()).await?;
25 | let api = start_stdio_api(command_tx, clients_tx, cli.subscribe.unwrap_or_default());
26 | let pty = start_pty(cli.command, &cli.size, input_rx, output_tx)?;
27 | let session = build_session(&cli.size);
28 | run_event_loop(output_rx, input_tx, command_rx, clients_rx, session, api).await?;
29 | pty.await?
30 | }
31 |
32 | fn build_session(size: &cli::Size) -> Session {
33 | Session::new(size.cols(), size.rows())
34 | }
35 |
36 | fn start_stdio_api(
37 | command_tx: mpsc::Sender,
38 | clients_tx: mpsc::Sender,
39 | sub: api::Subscription,
40 | ) -> JoinHandle> {
41 | tokio::spawn(api::stdio::start(command_tx, clients_tx, sub))
42 | }
43 |
44 | fn start_pty(
45 | command: Vec,
46 | size: &cli::Size,
47 | input_rx: mpsc::Receiver>,
48 | output_tx: mpsc::Sender>,
49 | ) -> Result>> {
50 | let command = command.join(" ");
51 | eprintln!("launching \"{}\" in terminal of size {}", command, size);
52 |
53 | Ok(tokio::spawn(pty::spawn(
54 | command, size, input_rx, output_tx,
55 | )?))
56 | }
57 |
58 | async fn start_http_api(
59 | listen_addr: Option,
60 | clients_tx: mpsc::Sender,
61 | ) -> Result<()> {
62 | if let Some(addr) = listen_addr {
63 | let listener = TcpListener::bind(addr).context("cannot start HTTP listener")?;
64 | tokio::spawn(api::http::start(listener, clients_tx).await?);
65 | }
66 |
67 | Ok(())
68 | }
69 |
70 | async fn run_event_loop(
71 | mut output_rx: mpsc::Receiver>,
72 | input_tx: mpsc::Sender>,
73 | mut command_rx: mpsc::Receiver,
74 | mut clients_rx: mpsc::Receiver,
75 | mut session: Session,
76 | mut api_handle: JoinHandle>,
77 | ) -> Result<()> {
78 | let mut serving = true;
79 |
80 | loop {
81 | tokio::select! {
82 | result = output_rx.recv() => {
83 | match result {
84 | Some(data) => {
85 | session.output(String::from_utf8_lossy(&data).to_string());
86 | },
87 |
88 | None => {
89 | eprintln!("process exited, shutting down...");
90 | break;
91 | }
92 | }
93 | }
94 |
95 | command = command_rx.recv() => {
96 | match command {
97 | Some(Command::Input(seqs)) => {
98 | let data = command::seqs_to_bytes(&seqs, session.cursor_key_app_mode());
99 | input_tx.send(data).await?;
100 | }
101 |
102 | Some(Command::Snapshot) => {
103 | session.snapshot();
104 | }
105 |
106 | Some(Command::Resize(cols, rows)) => {
107 | session.resize(cols, rows);
108 | }
109 |
110 | None => {
111 | eprintln!("stdin closed, shutting down...");
112 | break;
113 | }
114 | }
115 | }
116 |
117 | client = clients_rx.recv(), if serving => {
118 | match client {
119 | Some(client) => {
120 | client.accept(session.subscribe());
121 | }
122 |
123 | None => {
124 | serving = false;
125 | }
126 | }
127 | }
128 |
129 | _ = &mut api_handle => {
130 | eprintln!("stdin closed, shutting down...");
131 | break;
132 | }
133 | }
134 | }
135 |
136 | Ok(())
137 | }
138 |
--------------------------------------------------------------------------------
/src/nbio.rs:
--------------------------------------------------------------------------------
1 | use std::io::{self, ErrorKind, Read};
2 | use std::{io::Write, os::fd::RawFd};
3 |
4 | pub fn set_non_blocking(fd: &RawFd) -> Result<(), io::Error> {
5 | use nix::fcntl::{fcntl, FcntlArg::*, OFlag};
6 |
7 | let flags = fcntl(*fd, F_GETFL)?;
8 | let mut oflags = OFlag::from_bits_truncate(flags);
9 | oflags |= OFlag::O_NONBLOCK;
10 | fcntl(*fd, F_SETFL(oflags))?;
11 |
12 | Ok(())
13 | }
14 |
15 | pub fn read(source: &mut R, buf: &mut [u8]) -> io::Result