├── .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 | screenshot of raw terminal output vs ht output 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> { 16 | match source.read(buf) { 17 | Ok(n) => Ok(Some(n)), 18 | 19 | Err(e) => { 20 | if e.kind() == ErrorKind::WouldBlock { 21 | Ok(None) 22 | } else if e.raw_os_error().is_some_and(|code| code == 5) { 23 | Ok(Some(0)) 24 | } else { 25 | return Err(e); 26 | } 27 | } 28 | } 29 | } 30 | 31 | pub fn write(sink: &mut W, buf: &[u8]) -> io::Result> { 32 | match sink.write(buf) { 33 | Ok(n) => Ok(Some(n)), 34 | 35 | Err(e) => { 36 | if e.kind() == ErrorKind::WouldBlock { 37 | Ok(None) 38 | } else if e.raw_os_error().is_some_and(|code| code == 5) { 39 | Ok(Some(0)) 40 | } else { 41 | return Err(e); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/pty.rs: -------------------------------------------------------------------------------- 1 | use crate::nbio; 2 | use anyhow::Result; 3 | use nix::libc; 4 | use nix::pty; 5 | use nix::sys::signal::{self, SigHandler, Signal}; 6 | use nix::sys::wait; 7 | use nix::unistd::{self, ForkResult, Pid}; 8 | use std::env; 9 | use std::ffi::{CString, NulError}; 10 | use std::fs::File; 11 | use std::future::Future; 12 | use std::io; 13 | use std::os::fd::FromRawFd; 14 | use std::os::fd::{AsRawFd, OwnedFd}; 15 | use tokio::io::unix::AsyncFd; 16 | use tokio::sync::mpsc; 17 | 18 | pub fn spawn( 19 | command: String, 20 | winsize: &pty::Winsize, 21 | input_rx: mpsc::Receiver>, 22 | output_tx: mpsc::Sender>, 23 | ) -> Result>> { 24 | let result = unsafe { pty::forkpty(Some(winsize), None) }?; 25 | 26 | match result.fork_result { 27 | ForkResult::Parent { child } => Ok(drive_child(child, result.master, input_rx, output_tx)), 28 | 29 | ForkResult::Child => { 30 | exec(command)?; 31 | unreachable!(); 32 | } 33 | } 34 | } 35 | 36 | async fn drive_child( 37 | child: Pid, 38 | master: OwnedFd, 39 | input_rx: mpsc::Receiver>, 40 | output_tx: mpsc::Sender>, 41 | ) -> Result<()> { 42 | let result = do_drive_child(master, input_rx, output_tx).await; 43 | eprintln!("sending HUP signal to the child process"); 44 | unsafe { libc::kill(child.as_raw(), libc::SIGHUP) }; 45 | eprintln!("waiting for the child process to exit"); 46 | 47 | tokio::task::spawn_blocking(move || { 48 | let _ = wait::waitpid(child, None); 49 | }) 50 | .await 51 | .unwrap(); 52 | 53 | result 54 | } 55 | 56 | const READ_BUF_SIZE: usize = 128 * 1024; 57 | 58 | async fn do_drive_child( 59 | master: OwnedFd, 60 | mut input_rx: mpsc::Receiver>, 61 | output_tx: mpsc::Sender>, 62 | ) -> Result<()> { 63 | let mut buf = [0u8; READ_BUF_SIZE]; 64 | let mut input: Vec = Vec::with_capacity(READ_BUF_SIZE); 65 | nbio::set_non_blocking(&master.as_raw_fd())?; 66 | let mut master_file = unsafe { File::from_raw_fd(master.as_raw_fd()) }; 67 | let master_fd = AsyncFd::new(master)?; 68 | 69 | loop { 70 | tokio::select! { 71 | result = input_rx.recv() => { 72 | match result { 73 | Some(data) => { 74 | input.extend_from_slice(&data); 75 | } 76 | 77 | None => { 78 | return Ok(()); 79 | } 80 | } 81 | } 82 | 83 | result = master_fd.readable() => { 84 | let mut guard = result?; 85 | 86 | loop { 87 | match nbio::read(&mut master_file, &mut buf)? { 88 | Some(0) => { 89 | return Ok(()); 90 | } 91 | 92 | Some(n) => { 93 | output_tx.send(buf[0..n].to_vec()).await?; 94 | } 95 | 96 | None => { 97 | guard.clear_ready(); 98 | break; 99 | } 100 | } 101 | } 102 | } 103 | 104 | result = master_fd.writable(), if !input.is_empty() => { 105 | let mut guard = result?; 106 | let mut buf: &[u8] = input.as_ref(); 107 | 108 | loop { 109 | match nbio::write(&mut master_file, buf)? { 110 | Some(0) => { 111 | return Ok(()); 112 | } 113 | 114 | Some(n) => { 115 | buf = &buf[n..]; 116 | 117 | if buf.is_empty() { 118 | break; 119 | } 120 | } 121 | 122 | None => { 123 | guard.clear_ready(); 124 | break; 125 | } 126 | } 127 | } 128 | 129 | let left = buf.len(); 130 | 131 | if left == 0 { 132 | input.clear(); 133 | } else { 134 | input.drain(..input.len() - left); 135 | } 136 | } 137 | } 138 | } 139 | } 140 | 141 | fn exec(command: String) -> io::Result<()> { 142 | let command = ["/bin/sh".to_owned(), "-c".to_owned(), command] 143 | .iter() 144 | .map(|s| CString::new(s.as_bytes())) 145 | .collect::, NulError>>()?; 146 | 147 | env::set_var("TERM", "xterm-256color"); 148 | unsafe { signal::signal(Signal::SIGPIPE, SigHandler::SigDfl) }?; 149 | unistd::execvp(&command[0], &command)?; 150 | unsafe { libc::_exit(1) } 151 | } 152 | -------------------------------------------------------------------------------- /src/session.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use futures_util::{stream, Stream, StreamExt}; 3 | use serde_json::json; 4 | use std::future; 5 | use std::time::{Duration, Instant}; 6 | use tokio::sync::{broadcast, mpsc, oneshot}; 7 | use tokio_stream::wrappers::{errors::BroadcastStreamRecvError, BroadcastStream}; 8 | 9 | pub struct Session { 10 | vt: avt::Vt, 11 | broadcast_tx: broadcast::Sender, 12 | stream_time: f64, 13 | start_time: Instant, 14 | last_event_time: Instant, 15 | } 16 | 17 | #[derive(Clone)] 18 | pub enum Event { 19 | Init(f64, usize, usize, String, String), 20 | Output(f64, String), 21 | Resize(f64, usize, usize), 22 | Snapshot(usize, usize, String, String), 23 | } 24 | 25 | pub struct Client(oneshot::Sender); 26 | 27 | pub struct Subscription { 28 | init: Event, 29 | broadcast_rx: broadcast::Receiver, 30 | } 31 | 32 | impl Session { 33 | pub fn new(cols: usize, rows: usize) -> Self { 34 | let (broadcast_tx, _) = broadcast::channel(1024); 35 | let now = Instant::now(); 36 | 37 | Self { 38 | vt: build_vt(cols, rows), 39 | broadcast_tx, 40 | stream_time: 0.0, 41 | start_time: now, 42 | last_event_time: now, 43 | } 44 | } 45 | 46 | pub fn output(&mut self, data: String) { 47 | self.vt.feed_str(&data); 48 | let time = self.start_time.elapsed().as_secs_f64(); 49 | let _ = self.broadcast_tx.send(Event::Output(time, data)); 50 | self.stream_time = time; 51 | self.last_event_time = Instant::now(); 52 | } 53 | 54 | pub fn resize(&mut self, cols: usize, rows: usize) { 55 | resize_vt(&mut self.vt, cols, rows); 56 | let time = self.start_time.elapsed().as_secs_f64(); 57 | let _ = self.broadcast_tx.send(Event::Resize(time, cols, rows)); 58 | self.stream_time = time; 59 | self.last_event_time = Instant::now(); 60 | } 61 | 62 | pub fn snapshot(&self) { 63 | let (cols, rows) = self.vt.size(); 64 | 65 | let _ = self.broadcast_tx.send(Event::Snapshot( 66 | cols, 67 | rows, 68 | self.vt.dump(), 69 | self.text_view(), 70 | )); 71 | } 72 | 73 | pub fn cursor_key_app_mode(&self) -> bool { 74 | self.vt.arrow_key_app_mode() 75 | } 76 | 77 | pub fn subscribe(&self) -> Subscription { 78 | let (cols, rows) = self.vt.size(); 79 | 80 | let init = Event::Init( 81 | self.elapsed_time(), 82 | cols, 83 | rows, 84 | self.vt.dump(), 85 | self.text_view(), 86 | ); 87 | 88 | let broadcast_rx = self.broadcast_tx.subscribe(); 89 | 90 | Subscription { init, broadcast_rx } 91 | } 92 | 93 | fn elapsed_time(&self) -> f64 { 94 | self.stream_time + self.last_event_time.elapsed().as_secs_f64() 95 | } 96 | 97 | fn text_view(&self) -> String { 98 | self.vt 99 | .view() 100 | .iter() 101 | .map(|l| l.text()) 102 | .collect::>() 103 | .join("\n") 104 | } 105 | } 106 | 107 | impl Event { 108 | pub fn to_json(&self) -> serde_json::Value { 109 | match self { 110 | Event::Init(_time, cols, rows, seq, text) => json!({ 111 | "type": "init", 112 | "data": json!({ 113 | "cols": cols, 114 | "rows": rows, 115 | "seq": seq, 116 | "text": text, 117 | }) 118 | }), 119 | 120 | Event::Output(_time, seq) => json!({ 121 | "type": "output", 122 | "data": json!({ 123 | "seq": seq 124 | }) 125 | }), 126 | 127 | Event::Resize(_time, cols, rows) => json!({ 128 | "type": "resize", 129 | "data": json!({ 130 | "cols": cols, 131 | "rows": rows, 132 | }) 133 | }), 134 | 135 | Event::Snapshot(cols, rows, seq, text) => json!({ 136 | "type": "snapshot", 137 | "data": json!({ 138 | "cols": cols, 139 | "rows": rows, 140 | "seq": seq, 141 | "text": text, 142 | }) 143 | }), 144 | } 145 | } 146 | } 147 | 148 | fn build_vt(cols: usize, rows: usize) -> avt::Vt { 149 | avt::Vt::builder().size(cols, rows).resizable(true).build() 150 | } 151 | 152 | fn resize_vt(vt: &mut avt::Vt, cols: usize, rows: usize) { 153 | vt.feed_str(&format!("\x1b[8;{rows};{cols}t")); 154 | } 155 | 156 | impl Client { 157 | pub fn accept(self, subscription: Subscription) { 158 | let _ = self.0.send(subscription); 159 | } 160 | } 161 | 162 | pub async fn stream( 163 | clients_tx: &mpsc::Sender, 164 | ) -> Result>> { 165 | let (sub_tx, sub_rx) = oneshot::channel(); 166 | clients_tx.send(Client(sub_tx)).await?; 167 | let sub = tokio::time::timeout(Duration::from_secs(5), sub_rx).await??; 168 | let init = stream::once(future::ready(Ok(sub.init))); 169 | let events = BroadcastStream::new(sub.broadcast_rx); 170 | 171 | Ok(init.chain(events)) 172 | } 173 | --------------------------------------------------------------------------------