Fix Ghostty terminal support (#26)
This commit is contained in:
780
Cargo.lock
generated
780
Cargo.lock
generated
@@ -73,12 +73,75 @@ version = "0.7.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bit-set"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
|
||||||
|
dependencies = [
|
||||||
|
"bit-vec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bit-vec"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.10.0"
|
version = "2.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.19.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.24.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -122,7 +185,7 @@ dependencies = [
|
|||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -133,7 +196,7 @@ checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "claude-chill"
|
name = "claude-chill"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -142,8 +205,9 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"memchr",
|
"memchr",
|
||||||
"nix",
|
"nix 0.30.1",
|
||||||
"serde",
|
"serde",
|
||||||
|
"termwiz",
|
||||||
"toml",
|
"toml",
|
||||||
"vt100",
|
"vt100",
|
||||||
]
|
]
|
||||||
@@ -154,6 +218,51 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "csscolorparser"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf"
|
||||||
|
dependencies = [
|
||||||
|
"lab",
|
||||||
|
"phf",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deltae"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs"
|
name = "dirs"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
@@ -204,6 +313,74 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "euclid"
|
||||||
|
version = "0.22.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fancy-regex"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2"
|
||||||
|
dependencies = [
|
||||||
|
"bit-set",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filedescriptor"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "finl_unicode"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fixedbitset"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fnv"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -215,6 +392,18 @@ dependencies = [
|
|||||||
"wasi",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasip2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
@@ -227,6 +416,12 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.13.0"
|
version = "2.13.0"
|
||||||
@@ -270,9 +465,31 @@ checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.85"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lab"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.177"
|
version = "0.2.177"
|
||||||
@@ -285,7 +502,7 @@ version = "0.1.12"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.10.0",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -295,24 +512,104 @@ version = "0.4.29"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mac_address"
|
||||||
|
version = "1.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303"
|
||||||
|
dependencies = [
|
||||||
|
"nix 0.29.0",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.6"
|
version = "2.7.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memmem"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memoffset"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minimal-lexical"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
"memoffset",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.30.1"
|
version = "0.30.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 2.10.0",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "7.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"minimal-lexical",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-derive"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell_polyfill"
|
name = "once_cell_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
@@ -325,6 +622,110 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordered-float"
|
||||||
|
version = "4.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest"
|
||||||
|
version = "2.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"ucd-trie",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_derive"
|
||||||
|
version = "2.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
"pest_generator",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_generator"
|
||||||
|
version = "2.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
"pest_meta",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest_meta"
|
||||||
|
version = "2.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
||||||
|
dependencies = [
|
||||||
|
"phf_macros",
|
||||||
|
"phf_shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_codegen"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator",
|
||||||
|
"phf_shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_generator"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||||
|
dependencies = [
|
||||||
|
"phf_shared",
|
||||||
|
"rand",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_macros"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator",
|
||||||
|
"phf_shared",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_shared"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
||||||
|
dependencies = [
|
||||||
|
"siphasher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portable-atomic"
|
name = "portable-atomic"
|
||||||
version = "1.13.0"
|
version = "1.13.0"
|
||||||
@@ -358,15 +759,36 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_users"
|
name = "redox_users"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom 0.2.17",
|
||||||
"libredox",
|
"libredox",
|
||||||
"thiserror",
|
"thiserror 2.0.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -398,6 +820,12 @@ version = "0.8.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
@@ -425,7 +853,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -437,12 +865,60 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.10.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook"
|
||||||
|
version = "0.3.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||||
|
dependencies = [
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "siphasher"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "1.0.109"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.114"
|
version = "2.0.114"
|
||||||
@@ -454,13 +930,96 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "terminfo"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"nom",
|
||||||
|
"phf",
|
||||||
|
"phf_codegen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termios"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termwiz"
|
||||||
|
version = "0.23.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"base64",
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"fancy-regex",
|
||||||
|
"filedescriptor",
|
||||||
|
"finl_unicode",
|
||||||
|
"fixedbitset",
|
||||||
|
"hex",
|
||||||
|
"lazy_static",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"memmem",
|
||||||
|
"nix 0.29.0",
|
||||||
|
"num-derive",
|
||||||
|
"num-traits",
|
||||||
|
"ordered-float",
|
||||||
|
"pest",
|
||||||
|
"pest_derive",
|
||||||
|
"phf",
|
||||||
|
"sha2",
|
||||||
|
"signal-hook",
|
||||||
|
"siphasher",
|
||||||
|
"terminfo",
|
||||||
|
"termios",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"ucd-trie",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"vtparse",
|
||||||
|
"wezterm-bidi",
|
||||||
|
"wezterm-blob-leases",
|
||||||
|
"wezterm-color-types",
|
||||||
|
"wezterm-dynamic",
|
||||||
|
"wezterm-input-types",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.17"
|
version = "2.0.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl 2.0.17",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -471,7 +1030,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -515,12 +1074,30 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ucd-trie"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -533,6 +1110,24 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "1.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
|
||||||
|
dependencies = [
|
||||||
|
"atomic",
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vt100"
|
name = "vt100"
|
||||||
version = "0.16.2"
|
version = "0.16.2"
|
||||||
@@ -554,12 +1149,169 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vtparse"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.1+wasi-snapshot-preview1"
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip2"
|
||||||
|
version = "1.0.2+wasi-0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.108"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.108"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.108"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.108"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wezterm-bidi"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"wezterm-dynamic",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wezterm-blob-leases"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"mac_address",
|
||||||
|
"sha2",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wezterm-color-types"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296"
|
||||||
|
dependencies = [
|
||||||
|
"csscolorparser",
|
||||||
|
"deltae",
|
||||||
|
"lazy_static",
|
||||||
|
"wezterm-dynamic",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wezterm-dynamic"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"ordered-float",
|
||||||
|
"strsim",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"wezterm-dynamic-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wezterm-dynamic-derive"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wezterm-input-types"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"euclid",
|
||||||
|
"lazy_static",
|
||||||
|
"serde",
|
||||||
|
"wezterm-dynamic",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -583,3 +1335,9 @@ checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["David Beesley"]
|
authors = ["David Beesley"]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[](https://github.com/davidbeesley/claude-chill/actions/workflows/ci.yml)
|
[](https://github.com/davidbeesley/claude-chill/actions/workflows/ci.yml)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|||||||
@@ -26,5 +26,6 @@ libc = "0.2"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
vt100 = "0.16"
|
vt100 = "0.16"
|
||||||
|
termwiz = "0.23"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
|
|||||||
580
crates/claude-chill/src/history_filter.rs
Normal file
580
crates/claude-chill/src/history_filter.rs
Normal file
@@ -0,0 +1,580 @@
|
|||||||
|
//! Whitelist-based filter for terminal escape sequences going into history.
|
||||||
|
//!
|
||||||
|
//! Uses termwiz to parse escape sequences and only allows known-safe
|
||||||
|
//! visual sequences through. This prevents mode-setting sequences
|
||||||
|
//! (focus reporting, mouse tracking, etc.) from being replayed.
|
||||||
|
//!
|
||||||
|
//! IMPORTANT: Every enum variant must be explicitly classified.
|
||||||
|
//! No catch-all fallbacks - we must consciously decide on each case.
|
||||||
|
|
||||||
|
use std::fmt::Write as FmtWrite;
|
||||||
|
use termwiz::escape::Action;
|
||||||
|
use termwiz::escape::csi::CSI;
|
||||||
|
use termwiz::escape::parser::Parser;
|
||||||
|
|
||||||
|
/// Filter that only allows safe visual sequences into history.
|
||||||
|
pub struct HistoryFilter {
|
||||||
|
parser: Parser,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HistoryFilter {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HistoryFilter {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
parser: Parser::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter bytes, returning only safe sequences for history.
|
||||||
|
pub fn filter(&mut self, input: &[u8]) -> Vec<u8> {
|
||||||
|
let actions = self.parser.parse_as_vec(input);
|
||||||
|
let mut output = String::new();
|
||||||
|
|
||||||
|
for action in actions {
|
||||||
|
if is_safe_for_history(&action) {
|
||||||
|
// Re-encode the action
|
||||||
|
let _ = write!(output, "{}", action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.into_bytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Classification result - explicit whitelist or blacklist
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum Classification {
|
||||||
|
/// Safe for history - purely visual, no side effects
|
||||||
|
Whitelist,
|
||||||
|
/// Unsafe for history - triggers responses or enables modes
|
||||||
|
Blacklist,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if an action is safe to include in history.
|
||||||
|
fn is_safe_for_history(action: &Action) -> bool {
|
||||||
|
classify_action(action) == Classification::Whitelist
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Classify an action as whitelist or blacklist.
|
||||||
|
/// Every variant must be explicitly handled - no wildcards.
|
||||||
|
fn classify_action(action: &Action) -> Classification {
|
||||||
|
use Classification::*;
|
||||||
|
|
||||||
|
match action {
|
||||||
|
// === WHITELIST: Text output ===
|
||||||
|
Action::Print(_) => Whitelist,
|
||||||
|
Action::PrintString(_) => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Graphics data ===
|
||||||
|
Action::Sixel(_) => Whitelist,
|
||||||
|
Action::KittyImage(_) => Whitelist,
|
||||||
|
|
||||||
|
// === MIXED: CSI sequences - check subcategory ===
|
||||||
|
Action::CSI(csi) => classify_csi(csi),
|
||||||
|
|
||||||
|
// === MIXED: Control codes - check individually ===
|
||||||
|
Action::Control(code) => classify_control(*code),
|
||||||
|
|
||||||
|
// === MIXED: Escape sequences - check individually ===
|
||||||
|
Action::Esc(esc) => classify_esc(esc),
|
||||||
|
|
||||||
|
// === MIXED: OSC commands - check for queries ===
|
||||||
|
Action::OperatingSystemCommand(osc) => classify_osc(osc),
|
||||||
|
|
||||||
|
// === BLACKLIST: Device control (DCS) - queries and modes ===
|
||||||
|
Action::DeviceControl(_) => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Termcap query ===
|
||||||
|
Action::XtGetTcap(_) => Blacklist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Classify CSI sequences - every variant explicitly handled.
|
||||||
|
fn classify_csi(csi: &CSI) -> Classification {
|
||||||
|
use Classification::*;
|
||||||
|
|
||||||
|
match csi {
|
||||||
|
// === WHITELIST: SGR (colors, styling) ===
|
||||||
|
CSI::Sgr(_) => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Cursor movement ===
|
||||||
|
CSI::Cursor(_) => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Edit operations ===
|
||||||
|
CSI::Edit(_) => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Character path (bidi) ===
|
||||||
|
CSI::SelectCharacterPath(_, _) => Whitelist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Mode setting (DECSET/DECRST) ===
|
||||||
|
// Includes focus tracking, mouse modes, bracketed paste, etc.
|
||||||
|
CSI::Mode(_) => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Device queries ===
|
||||||
|
CSI::Device(_) => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Keyboard protocol ===
|
||||||
|
CSI::Keyboard(_) => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Mouse reports ===
|
||||||
|
// These come IN from terminal, shouldn't be in output anyway
|
||||||
|
CSI::Mouse(_) => Blacklist,
|
||||||
|
|
||||||
|
// === MIXED: Window operations ===
|
||||||
|
CSI::Window(w) => classify_window(w),
|
||||||
|
|
||||||
|
// === BLACKLIST: Unspecified/unknown ===
|
||||||
|
// We can't know if it's safe, so blacklist
|
||||||
|
CSI::Unspecified(_) => Blacklist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Classify window operations - every variant explicitly handled.
|
||||||
|
fn classify_window(window: &termwiz::escape::csi::Window) -> Classification {
|
||||||
|
use Classification::*;
|
||||||
|
use termwiz::escape::csi::Window;
|
||||||
|
|
||||||
|
match window {
|
||||||
|
// === WHITELIST: Window state modifications ===
|
||||||
|
Window::DeIconify => Whitelist,
|
||||||
|
Window::Iconify => Whitelist,
|
||||||
|
Window::MoveWindow { .. } => Whitelist,
|
||||||
|
Window::ResizeWindowPixels { .. } => Whitelist,
|
||||||
|
Window::RaiseWindow => Whitelist,
|
||||||
|
Window::LowerWindow => Whitelist,
|
||||||
|
Window::RefreshWindow => Whitelist,
|
||||||
|
Window::ResizeWindowCells { .. } => Whitelist,
|
||||||
|
Window::RestoreMaximizedWindow => Whitelist,
|
||||||
|
Window::MaximizeWindow => Whitelist,
|
||||||
|
Window::MaximizeWindowVertically => Whitelist,
|
||||||
|
Window::MaximizeWindowHorizontally => Whitelist,
|
||||||
|
Window::UndoFullScreenMode => Whitelist,
|
||||||
|
Window::ChangeToFullScreenMode => Whitelist,
|
||||||
|
Window::ToggleFullScreen => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Title stack operations ===
|
||||||
|
Window::PushIconAndWindowTitle => Whitelist,
|
||||||
|
Window::PushIconTitle => Whitelist,
|
||||||
|
Window::PushWindowTitle => Whitelist,
|
||||||
|
Window::PopIconAndWindowTitle => Whitelist,
|
||||||
|
Window::PopIconTitle => Whitelist,
|
||||||
|
Window::PopWindowTitle => Whitelist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Query operations (terminal sends response) ===
|
||||||
|
Window::ReportWindowState => Blacklist,
|
||||||
|
Window::ReportWindowPosition => Blacklist,
|
||||||
|
Window::ReportTextAreaPosition => Blacklist,
|
||||||
|
Window::ReportTextAreaSizePixels => Blacklist,
|
||||||
|
Window::ReportWindowSizePixels => Blacklist,
|
||||||
|
Window::ReportScreenSizePixels => Blacklist,
|
||||||
|
Window::ReportCellSizePixels => Blacklist,
|
||||||
|
Window::ReportCellSizePixelsResponse { .. } => Blacklist,
|
||||||
|
Window::ReportTextAreaSizeCells => Blacklist,
|
||||||
|
Window::ReportScreenSizeCells => Blacklist,
|
||||||
|
Window::ReportIconLabel => Blacklist,
|
||||||
|
Window::ReportWindowTitle => Blacklist,
|
||||||
|
Window::ChecksumRectangularArea { .. } => Blacklist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Classify control codes - every variant explicitly handled.
|
||||||
|
fn classify_control(code: termwiz::escape::ControlCode) -> Classification {
|
||||||
|
use Classification::*;
|
||||||
|
use termwiz::escape::ControlCode;
|
||||||
|
|
||||||
|
match code {
|
||||||
|
// === WHITELIST: Common formatting controls ===
|
||||||
|
ControlCode::Null => Whitelist,
|
||||||
|
ControlCode::Bell => Whitelist,
|
||||||
|
ControlCode::Backspace => Whitelist,
|
||||||
|
ControlCode::HorizontalTab => Whitelist,
|
||||||
|
ControlCode::LineFeed => Whitelist,
|
||||||
|
ControlCode::VerticalTab => Whitelist,
|
||||||
|
ControlCode::FormFeed => Whitelist,
|
||||||
|
ControlCode::CarriageReturn => Whitelist,
|
||||||
|
ControlCode::ShiftOut => Whitelist,
|
||||||
|
ControlCode::ShiftIn => Whitelist,
|
||||||
|
|
||||||
|
// === BLACKLIST: ENQ triggers terminal response ===
|
||||||
|
ControlCode::Enquiry => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Other C0 controls ===
|
||||||
|
ControlCode::StartOfHeading => Blacklist,
|
||||||
|
ControlCode::StartOfText => Blacklist,
|
||||||
|
ControlCode::EndOfText => Blacklist,
|
||||||
|
ControlCode::EndOfTransmission => Blacklist,
|
||||||
|
ControlCode::Acknowledge => Blacklist,
|
||||||
|
ControlCode::DataLinkEscape => Blacklist,
|
||||||
|
ControlCode::DeviceControlOne => Blacklist,
|
||||||
|
ControlCode::DeviceControlTwo => Blacklist,
|
||||||
|
ControlCode::DeviceControlThree => Blacklist,
|
||||||
|
ControlCode::DeviceControlFour => Blacklist,
|
||||||
|
ControlCode::NegativeAcknowledge => Blacklist,
|
||||||
|
ControlCode::SynchronousIdle => Blacklist,
|
||||||
|
ControlCode::EndOfTransmissionBlock => Blacklist,
|
||||||
|
ControlCode::Cancel => Blacklist,
|
||||||
|
ControlCode::EndOfMedium => Blacklist,
|
||||||
|
ControlCode::Substitute => Blacklist,
|
||||||
|
ControlCode::Escape => Blacklist, // ESC alone shouldn't appear
|
||||||
|
ControlCode::FileSeparator => Blacklist,
|
||||||
|
ControlCode::GroupSeparator => Blacklist,
|
||||||
|
ControlCode::RecordSeparator => Blacklist,
|
||||||
|
ControlCode::UnitSeparator => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: C1 8-bit controls ===
|
||||||
|
ControlCode::BPH => Blacklist,
|
||||||
|
ControlCode::NBH => Blacklist,
|
||||||
|
ControlCode::IND => Blacklist, // Use ESC D instead
|
||||||
|
ControlCode::NEL => Blacklist, // Use ESC E instead
|
||||||
|
ControlCode::SSA => Blacklist,
|
||||||
|
ControlCode::ESA => Blacklist,
|
||||||
|
ControlCode::HTS => Blacklist, // Use ESC H instead
|
||||||
|
ControlCode::HTJ => Blacklist,
|
||||||
|
ControlCode::VTS => Blacklist,
|
||||||
|
ControlCode::PLD => Blacklist,
|
||||||
|
ControlCode::PLU => Blacklist,
|
||||||
|
ControlCode::RI => Blacklist, // Use ESC M instead
|
||||||
|
ControlCode::SS2 => Blacklist,
|
||||||
|
ControlCode::SS3 => Blacklist,
|
||||||
|
ControlCode::DCS => Blacklist, // Device control string
|
||||||
|
ControlCode::PU1 => Blacklist,
|
||||||
|
ControlCode::PU2 => Blacklist,
|
||||||
|
ControlCode::STS => Blacklist,
|
||||||
|
ControlCode::CCH => Blacklist,
|
||||||
|
ControlCode::MW => Blacklist,
|
||||||
|
ControlCode::SPA => Blacklist,
|
||||||
|
ControlCode::EPA => Blacklist,
|
||||||
|
ControlCode::SOS => Blacklist,
|
||||||
|
ControlCode::SCI => Blacklist,
|
||||||
|
ControlCode::CSI => Blacklist, // CSI alone shouldn't appear
|
||||||
|
ControlCode::ST => Blacklist, // String terminator
|
||||||
|
ControlCode::OSC => Blacklist, // OSC alone shouldn't appear
|
||||||
|
ControlCode::PM => Blacklist,
|
||||||
|
ControlCode::APC => Blacklist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Classify escape sequences - every variant explicitly handled.
|
||||||
|
fn classify_esc(esc: &termwiz::escape::Esc) -> Classification {
|
||||||
|
use Classification::*;
|
||||||
|
use termwiz::escape::{Esc, EscCode};
|
||||||
|
|
||||||
|
match esc {
|
||||||
|
Esc::Code(code) => match code {
|
||||||
|
// === WHITELIST: Cursor save/restore ===
|
||||||
|
EscCode::DecSaveCursorPosition => Whitelist,
|
||||||
|
EscCode::DecRestoreCursorPosition => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Character set designation (G0) ===
|
||||||
|
EscCode::DecLineDrawingG0 => Whitelist,
|
||||||
|
EscCode::AsciiCharacterSetG0 => Whitelist,
|
||||||
|
EscCode::UkCharacterSetG0 => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Character set designation (G1) ===
|
||||||
|
EscCode::DecLineDrawingG1 => Whitelist,
|
||||||
|
EscCode::AsciiCharacterSetG1 => Whitelist,
|
||||||
|
EscCode::UkCharacterSetG1 => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Line operations ===
|
||||||
|
EscCode::Index => Whitelist,
|
||||||
|
EscCode::ReverseIndex => Whitelist,
|
||||||
|
EscCode::NextLine => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Tab set ===
|
||||||
|
EscCode::HorizontalTabSet => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Keypad modes ===
|
||||||
|
EscCode::DecApplicationKeyPad => Whitelist,
|
||||||
|
EscCode::DecNormalKeyPad => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: String terminator ===
|
||||||
|
EscCode::StringTerminator => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Full reset ===
|
||||||
|
EscCode::FullReset => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Tmux title ===
|
||||||
|
EscCode::TmuxTitle => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Cursor position ===
|
||||||
|
EscCode::CursorPositionLowerLeft => Whitelist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Single shifts ===
|
||||||
|
EscCode::SingleShiftG2 => Blacklist,
|
||||||
|
EscCode::SingleShiftG3 => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Guarded area ===
|
||||||
|
EscCode::StartOfGuardedArea => Blacklist,
|
||||||
|
EscCode::EndOfGuardedArea => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Start of string ===
|
||||||
|
EscCode::StartOfString => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Return terminal ID (query) ===
|
||||||
|
EscCode::ReturnTerminalId => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Privacy message ===
|
||||||
|
EscCode::PrivacyMessage => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Application program command ===
|
||||||
|
EscCode::ApplicationProgramCommand => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Back index ===
|
||||||
|
EscCode::DecBackIndex => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: DEC line width/height ===
|
||||||
|
EscCode::DecDoubleHeightTopHalfLine => Blacklist,
|
||||||
|
EscCode::DecDoubleHeightBottomHalfLine => Blacklist,
|
||||||
|
EscCode::DecSingleWidthLine => Blacklist,
|
||||||
|
EscCode::DecDoubleWidthLine => Blacklist,
|
||||||
|
EscCode::DecScreenAlignmentDisplay => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Application mode key presses (input, not output) ===
|
||||||
|
EscCode::ApplicationModeArrowUpPress => Blacklist,
|
||||||
|
EscCode::ApplicationModeArrowDownPress => Blacklist,
|
||||||
|
EscCode::ApplicationModeArrowRightPress => Blacklist,
|
||||||
|
EscCode::ApplicationModeArrowLeftPress => Blacklist,
|
||||||
|
EscCode::ApplicationModeHomePress => Blacklist,
|
||||||
|
EscCode::ApplicationModeEndPress => Blacklist,
|
||||||
|
EscCode::F1Press => Blacklist,
|
||||||
|
EscCode::F2Press => Blacklist,
|
||||||
|
EscCode::F3Press => Blacklist,
|
||||||
|
EscCode::F4Press => Blacklist,
|
||||||
|
},
|
||||||
|
|
||||||
|
// === BLACKLIST: Unspecified escape sequences ===
|
||||||
|
Esc::Unspecified { .. } => Blacklist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Classify OSC commands - every variant explicitly handled.
|
||||||
|
fn classify_osc(osc: &termwiz::escape::OperatingSystemCommand) -> Classification {
|
||||||
|
use Classification::*;
|
||||||
|
use termwiz::escape::osc::OperatingSystemCommand;
|
||||||
|
|
||||||
|
match osc {
|
||||||
|
// === WHITELIST: Title setting ===
|
||||||
|
OperatingSystemCommand::SetIconNameAndWindowTitle(_) => Whitelist,
|
||||||
|
OperatingSystemCommand::SetIconName(_) => Whitelist,
|
||||||
|
OperatingSystemCommand::SetIconNameSun(_) => Whitelist,
|
||||||
|
OperatingSystemCommand::SetWindowTitle(_) => Whitelist,
|
||||||
|
OperatingSystemCommand::SetWindowTitleSun(_) => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Hyperlinks ===
|
||||||
|
OperatingSystemCommand::SetHyperlink(_) => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Color palette setting ===
|
||||||
|
OperatingSystemCommand::ChangeColorNumber(_) => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Reset colors ===
|
||||||
|
OperatingSystemCommand::ResetColors(_) => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Reset dynamic color ===
|
||||||
|
OperatingSystemCommand::ResetDynamicColor(_) => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Selection setting (not query) ===
|
||||||
|
OperatingSystemCommand::SetSelection(_, _) => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: System notification ===
|
||||||
|
OperatingSystemCommand::SystemNotification(_) => Whitelist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Selection clear ===
|
||||||
|
OperatingSystemCommand::ClearSelection(_) => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Selection query ===
|
||||||
|
OperatingSystemCommand::QuerySelection(_) => Blacklist,
|
||||||
|
|
||||||
|
// === MIXED: Dynamic colors (check for query) ===
|
||||||
|
OperatingSystemCommand::ChangeDynamicColors(_, colors) => {
|
||||||
|
// Whitelist only if ALL are Color, not Query
|
||||||
|
if colors
|
||||||
|
.iter()
|
||||||
|
.all(|c| matches!(c, termwiz::escape::osc::ColorOrQuery::Color(_)))
|
||||||
|
{
|
||||||
|
Whitelist
|
||||||
|
} else {
|
||||||
|
Blacklist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === MIXED: iTerm proprietary ===
|
||||||
|
OperatingSystemCommand::ITermProprietary(iterm) => classify_iterm(iterm),
|
||||||
|
|
||||||
|
// === WHITELIST: FinalTerm semantic prompts ===
|
||||||
|
OperatingSystemCommand::FinalTermSemanticPrompt(_) => Whitelist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Current directory (pwd) - could leak info ===
|
||||||
|
OperatingSystemCommand::CurrentWorkingDirectory(_) => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Rxvt extension ===
|
||||||
|
OperatingSystemCommand::RxvtExtension(_) => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: ConEmu progress ===
|
||||||
|
OperatingSystemCommand::ConEmuProgress(_) => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Unspecified OSC ===
|
||||||
|
OperatingSystemCommand::Unspecified(_) => Blacklist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Classify iTerm proprietary OSC commands.
|
||||||
|
fn classify_iterm(iterm: &termwiz::escape::osc::ITermProprietary) -> Classification {
|
||||||
|
use Classification::*;
|
||||||
|
use termwiz::escape::osc::ITermProprietary;
|
||||||
|
|
||||||
|
match iterm {
|
||||||
|
// === WHITELIST: File/image data ===
|
||||||
|
ITermProprietary::File(_) => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Marks ===
|
||||||
|
ITermProprietary::SetMark => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: User variables ===
|
||||||
|
ITermProprietary::SetUserVar { .. } => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Badge ===
|
||||||
|
ITermProprietary::SetBadgeFormat(_) => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Profile ===
|
||||||
|
ITermProprietary::SetProfile(_) => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Copy to pasteboard ===
|
||||||
|
ITermProprietary::CopyToClipboard(_) => Whitelist,
|
||||||
|
|
||||||
|
// === WHITELIST: Copy string ===
|
||||||
|
ITermProprietary::Copy(_) => Whitelist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Current directory ===
|
||||||
|
ITermProprietary::CurrentDir(_) => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Cell size request (triggers response) ===
|
||||||
|
ITermProprietary::RequestCellSize => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Cell size response ===
|
||||||
|
ITermProprietary::ReportCellSize { .. } => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Report variable (triggers response) ===
|
||||||
|
ITermProprietary::ReportVariable(_) => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Unicode version ===
|
||||||
|
ITermProprietary::UnicodeVersion(_) => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Stealing focus ===
|
||||||
|
ITermProprietary::StealFocus => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Clear scrollback ===
|
||||||
|
ITermProprietary::ClearScrollback => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: End copy ===
|
||||||
|
ITermProprietary::EndCopy => Blacklist,
|
||||||
|
|
||||||
|
// === BLACKLIST: Highlight cursor line ===
|
||||||
|
ITermProprietary::HighlightCursorLine(_) => Blacklist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_plain_text_passes() {
|
||||||
|
let mut filter = HistoryFilter::new();
|
||||||
|
let output = filter.filter(b"Hello, World!");
|
||||||
|
assert_eq!(output, b"Hello, World!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sgr_passes() {
|
||||||
|
let mut filter = HistoryFilter::new();
|
||||||
|
// Bold red text
|
||||||
|
let input = b"\x1b[1;31mRed\x1b[0m";
|
||||||
|
let output = filter.filter(input);
|
||||||
|
// termwiz re-encodes, so check it contains the key parts
|
||||||
|
assert!(!output.is_empty());
|
||||||
|
let output_str = String::from_utf8_lossy(&output);
|
||||||
|
assert!(output_str.contains("Red"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cursor_movement_passes() {
|
||||||
|
let mut filter = HistoryFilter::new();
|
||||||
|
// Move cursor to position 1,1
|
||||||
|
let input = b"\x1b[1;1H";
|
||||||
|
let output = filter.filter(input);
|
||||||
|
assert!(!output.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_focus_tracking_filtered() {
|
||||||
|
let mut filter = HistoryFilter::new();
|
||||||
|
// Enable focus reporting (CSI ? 1004 h)
|
||||||
|
let input = b"\x1b[?1004h";
|
||||||
|
let output = filter.filter(input);
|
||||||
|
assert!(output.is_empty(), "Focus tracking should be filtered");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mouse_mode_filtered() {
|
||||||
|
let mut filter = HistoryFilter::new();
|
||||||
|
// Enable mouse tracking (CSI ? 1000 h)
|
||||||
|
let input = b"\x1b[?1000h";
|
||||||
|
let output = filter.filter(input);
|
||||||
|
assert!(output.is_empty(), "Mouse tracking should be filtered");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bracketed_paste_filtered() {
|
||||||
|
let mut filter = HistoryFilter::new();
|
||||||
|
// Enable bracketed paste (CSI ? 2004 h)
|
||||||
|
let input = b"\x1b[?2004h";
|
||||||
|
let output = filter.filter(input);
|
||||||
|
assert!(output.is_empty(), "Bracketed paste should be filtered");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_device_attributes_filtered() {
|
||||||
|
let mut filter = HistoryFilter::new();
|
||||||
|
// Primary DA query
|
||||||
|
let input = b"\x1b[c";
|
||||||
|
let output = filter.filter(input);
|
||||||
|
assert!(output.is_empty(), "DA query should be filtered");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mixed_content() {
|
||||||
|
let mut filter = HistoryFilter::new();
|
||||||
|
// Mix of safe and unsafe
|
||||||
|
let input = b"Hello\x1b[?1004hWorld\x1b[31mRed\x1b[0m";
|
||||||
|
let output = filter.filter(input);
|
||||||
|
let output_str = String::from_utf8_lossy(&output);
|
||||||
|
assert!(output_str.contains("Hello"));
|
||||||
|
assert!(output_str.contains("World"));
|
||||||
|
assert!(output_str.contains("Red"));
|
||||||
|
// Should not contain the focus tracking sequence
|
||||||
|
assert!(!output_str.contains("1004"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_osc_title_passes() {
|
||||||
|
let mut filter = HistoryFilter::new();
|
||||||
|
// Set window title
|
||||||
|
let input = b"\x1b]0;My Title\x07";
|
||||||
|
let output = filter.filter(input);
|
||||||
|
assert!(!output.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_osc_query_filtered() {
|
||||||
|
let mut filter = HistoryFilter::new();
|
||||||
|
// Query foreground color
|
||||||
|
let input = b"\x1b]10;?\x07";
|
||||||
|
let output = filter.filter(input);
|
||||||
|
assert!(output.is_empty(), "OSC query should be filtered");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod escape_filter;
|
pub mod escape_filter;
|
||||||
pub mod escape_sequences;
|
pub mod escape_sequences;
|
||||||
|
pub mod history_filter;
|
||||||
pub mod key_parser;
|
pub mod key_parser;
|
||||||
pub mod line_buffer;
|
pub mod line_buffer;
|
||||||
pub mod proxy;
|
pub mod proxy;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use crate::escape_filter::TerminalQueryFilter;
|
|
||||||
use crate::escape_sequences::{
|
use crate::escape_sequences::{
|
||||||
ALT_SCREEN_ENTER, ALT_SCREEN_ENTER_LEGACY, ALT_SCREEN_EXIT, ALT_SCREEN_EXIT_LEGACY,
|
ALT_SCREEN_ENTER, ALT_SCREEN_ENTER_LEGACY, ALT_SCREEN_EXIT, ALT_SCREEN_EXIT_LEGACY,
|
||||||
CLEAR_SCREEN, CURSOR_HOME, INPUT_BUFFER_CAPACITY, OUTPUT_BUFFER_CAPACITY, SYNC_BUFFER_CAPACITY,
|
CLEAR_SCREEN, CURSOR_HOME, INPUT_BUFFER_CAPACITY, OUTPUT_BUFFER_CAPACITY, SYNC_BUFFER_CAPACITY,
|
||||||
SYNC_END, SYNC_START,
|
SYNC_END, SYNC_START,
|
||||||
};
|
};
|
||||||
|
use crate::history_filter::HistoryFilter;
|
||||||
use crate::line_buffer::LineBuffer;
|
use crate::line_buffer::LineBuffer;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
@@ -21,6 +21,9 @@ use std::os::unix::process::CommandExt;
|
|||||||
use std::process::{Child, Command, ExitStatus};
|
use std::process::{Child, Command, ExitStatus};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
use termwiz::escape::Action;
|
||||||
|
use termwiz::escape::csi::{CSI, Keyboard};
|
||||||
|
use termwiz::escape::parser::Parser as TermwizParser;
|
||||||
|
|
||||||
static SIGWINCH_RECEIVED: AtomicBool = AtomicBool::new(false);
|
static SIGWINCH_RECEIVED: AtomicBool = AtomicBool::new(false);
|
||||||
static SIGINT_RECEIVED: AtomicBool = AtomicBool::new(false);
|
static SIGINT_RECEIVED: AtomicBool = AtomicBool::new(false);
|
||||||
@@ -97,7 +100,7 @@ pub struct Proxy {
|
|||||||
child: Child,
|
child: Child,
|
||||||
original_termios: Option<Termios>,
|
original_termios: Option<Termios>,
|
||||||
history: LineBuffer,
|
history: LineBuffer,
|
||||||
history_filter: TerminalQueryFilter,
|
history_filter: HistoryFilter,
|
||||||
vt_parser: vt100::Parser,
|
vt_parser: vt100::Parser,
|
||||||
vt_prev_screen: Option<vt100::Screen>,
|
vt_prev_screen: Option<vt100::Screen>,
|
||||||
last_output_time: Option<Instant>,
|
last_output_time: Option<Instant>,
|
||||||
@@ -110,9 +113,8 @@ pub struct Proxy {
|
|||||||
in_lookback_mode: bool,
|
in_lookback_mode: bool,
|
||||||
in_alternate_screen: bool,
|
in_alternate_screen: bool,
|
||||||
kitty_mode_supported: bool,
|
kitty_mode_supported: bool,
|
||||||
kitty_mode_enabled: bool,
|
kitty_mode_stack: u32,
|
||||||
kitty_input_buffer: Vec<u8>,
|
kitty_output_parser: TermwizParser,
|
||||||
kitty_output_buffer: Vec<u8>,
|
|
||||||
vt_render_pending: bool,
|
vt_render_pending: bool,
|
||||||
lookback_cache: Vec<u8>,
|
lookback_cache: Vec<u8>,
|
||||||
lookback_input_buffer: Vec<u8>,
|
lookback_input_buffer: Vec<u8>,
|
||||||
@@ -127,70 +129,86 @@ pub struct Proxy {
|
|||||||
alt_screen_exit_legacy_finder: memmem::Finder<'static>,
|
alt_screen_exit_legacy_finder: memmem::Finder<'static>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_KITTY_SEQ_LEN: usize = 16;
|
/// Returns (supported, initial_flags) - if flags > 0, terminal is already in Kitty mode
|
||||||
|
fn detect_kitty_support() -> (bool, u32) {
|
||||||
|
use std::io::Write;
|
||||||
|
use termwiz::escape::csi::Device;
|
||||||
|
|
||||||
fn is_complete_kitty_output_seq(data: &[u8]) -> bool {
|
// Query sequences:
|
||||||
if data.len() < 4 || data[0] != 0x1b || data[1] != b'[' {
|
// CSI ? u - Kitty keyboard protocol query
|
||||||
return false;
|
// CSI c - Primary Device Attributes (all terminals respond)
|
||||||
}
|
const KITTY_QUERY: &[u8] = b"\x1b[?u";
|
||||||
if data[2] == b'<' && data[3] == b'u' {
|
const DA_QUERY: &[u8] = b"\x1b[c";
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if data[2] == b'>' {
|
|
||||||
let mut j = 3;
|
|
||||||
while j < data.len() && data[j].is_ascii_digit() {
|
|
||||||
j += 1;
|
|
||||||
}
|
|
||||||
if j > 3 && j < data.len() && data[j] == b'u' {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_complete_kitty_input_seq(data: &[u8]) -> bool {
|
// Send both queries
|
||||||
if data.len() < 5 || data[0] != 0x1b || data[1] != b'[' || data[2] != b'?' {
|
let stdout = std::io::stdout();
|
||||||
return false;
|
let mut stdout_lock = stdout.lock();
|
||||||
|
if stdout_lock.write_all(KITTY_QUERY).is_err() {
|
||||||
|
return (false, 0);
|
||||||
}
|
}
|
||||||
let mut j = 3;
|
if stdout_lock.write_all(DA_QUERY).is_err() {
|
||||||
while j < data.len() && data[j].is_ascii_digit() {
|
return (false, 0);
|
||||||
j += 1;
|
|
||||||
}
|
}
|
||||||
j > 3 && j < data.len() && data[j] == b'u'
|
if stdout_lock.flush().is_err() {
|
||||||
}
|
return (false, 0);
|
||||||
|
}
|
||||||
|
drop(stdout_lock);
|
||||||
|
|
||||||
fn kitty_output_split_point(buffer: &[u8]) -> usize {
|
// Read responses with timeout using termwiz parser
|
||||||
let last_esc = buffer.iter().rposition(|&b| b == 0x1b);
|
let stdin = std::io::stdin();
|
||||||
|
let mut parser = TermwizParser::new();
|
||||||
|
let mut buf = [0u8; 256];
|
||||||
|
let mut kitty_supported = false;
|
||||||
|
let mut kitty_flags: u32 = 0;
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let timeout = std::time::Duration::from_millis(500);
|
||||||
|
let poll_interval = PollTimeout::from(50u16);
|
||||||
|
|
||||||
match last_esc {
|
while start.elapsed() < timeout {
|
||||||
None => buffer.len(),
|
let mut poll_fd = [PollFd::new(stdin.as_fd(), PollFlags::POLLIN)];
|
||||||
Some(pos) if pos + MAX_KITTY_SEQ_LEN <= buffer.len() => buffer.len(),
|
|
||||||
Some(pos) => {
|
|
||||||
let tail = &buffer[pos..];
|
|
||||||
if is_complete_kitty_output_seq(tail) {
|
|
||||||
buffer.len()
|
|
||||||
} else {
|
|
||||||
pos
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn kitty_input_split_point(buffer: &[u8]) -> usize {
|
match poll(&mut poll_fd, poll_interval) {
|
||||||
let last_esc = buffer.iter().rposition(|&b| b == 0x1b);
|
Ok(0) => continue,
|
||||||
|
Ok(_) => {
|
||||||
|
match read(stdin.as_fd(), &mut buf) {
|
||||||
|
Ok(0) => continue,
|
||||||
|
Ok(n) => {
|
||||||
|
let actions = parser.parse_as_vec(&buf[..n]);
|
||||||
|
for action in actions {
|
||||||
|
if let Action::CSI(csi) = action {
|
||||||
|
match csi {
|
||||||
|
CSI::Keyboard(Keyboard::ReportKittyState(flags)) => {
|
||||||
|
kitty_supported = true;
|
||||||
|
kitty_flags = u32::from(flags.bits());
|
||||||
|
}
|
||||||
|
CSI::Device(dev)
|
||||||
|
if matches!(*dev, Device::DeviceAttributes(_)) =>
|
||||||
|
{
|
||||||
|
// DA response means all responses received
|
||||||
|
debug!(
|
||||||
|
"Kitty detection complete: supported={} flags={}",
|
||||||
|
kitty_supported, kitty_flags
|
||||||
|
);
|
||||||
|
return (kitty_supported, kitty_flags);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) if e == Errno::EAGAIN || e == Errno::EWOULDBLOCK => continue,
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match last_esc {
|
debug!(
|
||||||
None => buffer.len(),
|
"Kitty detection timed out, supported={} flags={}",
|
||||||
Some(pos) if pos + MAX_KITTY_SEQ_LEN <= buffer.len() => buffer.len(),
|
kitty_supported, kitty_flags
|
||||||
Some(pos) => {
|
);
|
||||||
let tail = &buffer[pos..];
|
(kitty_supported, kitty_flags)
|
||||||
if is_complete_kitty_input_seq(tail) {
|
|
||||||
buffer.len()
|
|
||||||
} else {
|
|
||||||
pos
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Proxy {
|
impl Proxy {
|
||||||
@@ -201,6 +219,11 @@ impl Proxy {
|
|||||||
let terminal_guard = TerminalGuard::new()?;
|
let terminal_guard = TerminalGuard::new()?;
|
||||||
setup_signal_handlers()?;
|
setup_signal_handlers()?;
|
||||||
|
|
||||||
|
// Detect Kitty support before spawning child
|
||||||
|
// If flags > 0, terminal is already in Kitty mode (inherited from parent)
|
||||||
|
let (kitty_supported, kitty_initial_flags) = detect_kitty_support();
|
||||||
|
let kitty_initial_stack = if kitty_initial_flags > 0 { 1 } else { 0 };
|
||||||
|
|
||||||
let slave_fd = pty.slave.as_raw_fd();
|
let slave_fd = pty.slave.as_raw_fd();
|
||||||
|
|
||||||
let child = unsafe {
|
let child = unsafe {
|
||||||
@@ -247,7 +270,7 @@ impl Proxy {
|
|||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
history,
|
history,
|
||||||
history_filter: TerminalQueryFilter::new(),
|
history_filter: HistoryFilter::new(),
|
||||||
config,
|
config,
|
||||||
pty_master: pty.master,
|
pty_master: pty.master,
|
||||||
child,
|
child,
|
||||||
@@ -263,10 +286,9 @@ impl Proxy {
|
|||||||
in_sync_block: false,
|
in_sync_block: false,
|
||||||
in_lookback_mode: false,
|
in_lookback_mode: false,
|
||||||
in_alternate_screen: false,
|
in_alternate_screen: false,
|
||||||
kitty_mode_supported: false,
|
kitty_mode_supported: kitty_supported,
|
||||||
kitty_mode_enabled: false,
|
kitty_mode_stack: kitty_initial_stack,
|
||||||
kitty_input_buffer: Vec::with_capacity(16),
|
kitty_output_parser: TermwizParser::new(),
|
||||||
kitty_output_buffer: Vec::with_capacity(16),
|
|
||||||
vt_render_pending: false,
|
vt_render_pending: false,
|
||||||
lookback_cache: Vec::new(),
|
lookback_cache: Vec::new(),
|
||||||
lookback_input_buffer: Vec::with_capacity(INPUT_BUFFER_CAPACITY),
|
lookback_input_buffer: Vec::with_capacity(INPUT_BUFFER_CAPACITY),
|
||||||
@@ -378,14 +400,11 @@ impl Proxy {
|
|||||||
feed_vt
|
feed_vt
|
||||||
);
|
);
|
||||||
|
|
||||||
// Track Kitty keyboard protocol state from output
|
|
||||||
self.update_kitty_mode_from_output(data);
|
|
||||||
|
|
||||||
if self.in_alternate_screen {
|
if self.in_alternate_screen {
|
||||||
// Still feed VT and history while in alt screen so they stay in sync
|
// Feed VT but NOT history while in alt screen
|
||||||
|
// Alt screen content (TUI editors, etc.) shouldn't be in lookback history
|
||||||
if feed_vt {
|
if feed_vt {
|
||||||
self.vt_parser.process(data);
|
self.vt_parser.process(data);
|
||||||
self.push_to_history(data);
|
|
||||||
}
|
}
|
||||||
return self.process_output_alt_screen(data, stdout_fd);
|
return self.process_output_alt_screen(data, stdout_fd);
|
||||||
}
|
}
|
||||||
@@ -425,7 +444,7 @@ impl Proxy {
|
|||||||
self.in_alternate_screen = true;
|
self.in_alternate_screen = true;
|
||||||
let seq_len = self.alt_screen_enter_len(&data[pos + alt_pos..]);
|
let seq_len = self.alt_screen_enter_len(&data[pos + alt_pos..]);
|
||||||
// Write alt screen enter directly
|
// Write alt screen enter directly
|
||||||
write_all(stdout_fd, &data[pos + alt_pos..pos + alt_pos + seq_len])?;
|
self.write_to_terminal(stdout_fd, &data[pos + alt_pos..pos + alt_pos + seq_len])?;
|
||||||
return self.process_output_alt_screen(&data[pos + alt_pos + seq_len..], stdout_fd);
|
return self.process_output_alt_screen(&data[pos + alt_pos + seq_len..], stdout_fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,9 +486,9 @@ impl Proxy {
|
|||||||
"process_output_alt_screen: ALT_SCREEN_EXIT detected at pos={}",
|
"process_output_alt_screen: ALT_SCREEN_EXIT detected at pos={}",
|
||||||
exit_pos
|
exit_pos
|
||||||
);
|
);
|
||||||
write_all(stdout_fd, &data[..exit_pos])?;
|
self.write_to_terminal(stdout_fd, &data[..exit_pos])?;
|
||||||
let seq_len = self.alt_screen_exit_len(&data[exit_pos..]);
|
let seq_len = self.alt_screen_exit_len(&data[exit_pos..]);
|
||||||
write_all(stdout_fd, &data[exit_pos..exit_pos + seq_len])?;
|
self.write_to_terminal(stdout_fd, &data[exit_pos..exit_pos + seq_len])?;
|
||||||
self.in_alternate_screen = false;
|
self.in_alternate_screen = false;
|
||||||
|
|
||||||
// Force full VT render to restore main screen content
|
// Force full VT render to restore main screen content
|
||||||
@@ -489,7 +508,7 @@ impl Proxy {
|
|||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
write_all(stdout_fd, data)
|
self.write_to_terminal(stdout_fd, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check for alt screen transitions without re-feeding VT/history
|
/// Check for alt screen transitions without re-feeding VT/history
|
||||||
@@ -501,7 +520,7 @@ impl Proxy {
|
|||||||
);
|
);
|
||||||
self.in_alternate_screen = true;
|
self.in_alternate_screen = true;
|
||||||
let seq_len = self.alt_screen_enter_len(&data[alt_pos..]);
|
let seq_len = self.alt_screen_enter_len(&data[alt_pos..]);
|
||||||
write_all(stdout_fd, &data[alt_pos..alt_pos + seq_len])?;
|
self.write_to_terminal(stdout_fd, &data[alt_pos..alt_pos + seq_len])?;
|
||||||
return self.process_output_alt_screen(&data[alt_pos + seq_len..], stdout_fd);
|
return self.process_output_alt_screen(&data[alt_pos + seq_len..], stdout_fd);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -545,90 +564,62 @@ impl Proxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_kitty_mode_from_output(&mut self, data: &[u8]) {
|
fn kitty_mode_enabled(&self) -> bool {
|
||||||
self.kitty_output_buffer.extend_from_slice(data);
|
self.kitty_mode_stack > 0
|
||||||
|
|
||||||
let split_point = kitty_output_split_point(&self.kitty_output_buffer);
|
|
||||||
|
|
||||||
if split_point > 0 {
|
|
||||||
let to_parse: Vec<u8> = self.kitty_output_buffer.drain(..split_point).collect();
|
|
||||||
self.parse_kitty_output(&to_parse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safety cap on carryover buffer
|
/// Write data to the terminal and track Kitty keyboard protocol state
|
||||||
if self.kitty_output_buffer.len() > 64 {
|
fn write_to_terminal<F: AsFd>(&mut self, stdout_fd: &F, data: &[u8]) -> Result<()> {
|
||||||
let overflow: Vec<u8> = self.kitty_output_buffer.drain(..).collect();
|
Self::update_kitty_mode_helper(
|
||||||
self.parse_kitty_output(&overflow);
|
&mut self.kitty_output_parser,
|
||||||
}
|
&mut self.kitty_mode_stack,
|
||||||
|
self.kitty_mode_supported,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
write_all(stdout_fd, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_kitty_output(&mut self, data: &[u8]) {
|
fn update_kitty_mode_helper(
|
||||||
let mut i = 0;
|
parser: &mut TermwizParser,
|
||||||
while i < data.len() {
|
stack: &mut u32,
|
||||||
if data[i] == 0x1b && i + 3 < data.len() && data[i + 1] == b'[' {
|
supported: bool,
|
||||||
if data[i + 2] == b'<' && data[i + 3] == b'u' {
|
data: &[u8],
|
||||||
debug!("Kitty keyboard protocol disabled");
|
) {
|
||||||
self.kitty_mode_enabled = false;
|
let actions = parser.parse_as_vec(data);
|
||||||
i += 4;
|
for action in actions {
|
||||||
continue;
|
if let Action::CSI(csi) = action {
|
||||||
}
|
match csi {
|
||||||
if data[i + 2] == b'>' && self.kitty_mode_supported {
|
CSI::Keyboard(Keyboard::PushKittyState { flags, .. }) => {
|
||||||
let mut j = i + 3;
|
if supported {
|
||||||
while j < data.len() && data[j].is_ascii_digit() {
|
*stack = stack.saturating_add(1);
|
||||||
j += 1;
|
debug!(
|
||||||
}
|
"Kitty keyboard protocol push (flags={:?}, stack={})",
|
||||||
if j > i + 3 && j < data.len() && data[j] == b'u' {
|
flags, stack
|
||||||
debug!("Kitty keyboard protocol enabled");
|
);
|
||||||
self.kitty_mode_enabled = true;
|
|
||||||
i = j + 1;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
CSI::Keyboard(Keyboard::SetKittyState { flags, .. }) => {
|
||||||
i += 1;
|
if supported && !flags.is_empty() && *stack == 0 {
|
||||||
|
*stack = 1;
|
||||||
|
debug!(
|
||||||
|
"Kitty keyboard protocol set (flags={:?}, stack={})",
|
||||||
|
flags, stack
|
||||||
|
);
|
||||||
|
} else if flags.is_empty() && *stack > 0 {
|
||||||
|
debug!("Kitty keyboard protocol set empty flags (stack={})", stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
CSI::Keyboard(Keyboard::PopKittyState(n)) => {
|
||||||
fn update_kitty_support_from_input(&mut self, data: &[u8]) {
|
let prev = *stack;
|
||||||
if self.kitty_mode_supported {
|
*stack = stack.saturating_sub(n);
|
||||||
return;
|
debug!(
|
||||||
|
"Kitty keyboard protocol pop {} (stack {} -> {})",
|
||||||
|
n, prev, stack
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
self.kitty_input_buffer.extend_from_slice(data);
|
|
||||||
|
|
||||||
let split_point = kitty_input_split_point(&self.kitty_input_buffer);
|
|
||||||
|
|
||||||
if split_point > 0 {
|
|
||||||
let to_parse: Vec<u8> = self.kitty_input_buffer.drain(..split_point).collect();
|
|
||||||
self.parse_kitty_input(&to_parse);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Safety cap on carryover buffer
|
|
||||||
if self.kitty_input_buffer.len() > 64 {
|
|
||||||
let overflow: Vec<u8> = self.kitty_input_buffer.drain(..).collect();
|
|
||||||
self.parse_kitty_input(&overflow);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_kitty_input(&mut self, data: &[u8]) {
|
|
||||||
if self.kitty_mode_supported {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut i = 0;
|
|
||||||
while i < data.len() {
|
|
||||||
if data[i] == 0x1b && i + 4 < data.len() && data[i + 1] == b'[' && data[i + 2] == b'?' {
|
|
||||||
let mut j = i + 3;
|
|
||||||
while j < data.len() && data[j].is_ascii_digit() {
|
|
||||||
j += 1;
|
|
||||||
}
|
|
||||||
if j > i + 3 && j < data.len() && data[j] == b'u' {
|
|
||||||
debug!("Kitty keyboard protocol supported (saw query response)");
|
|
||||||
self.kitty_mode_supported = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,6 +726,14 @@ impl Proxy {
|
|||||||
is_diff,
|
is_diff,
|
||||||
self.output_buffer.len()
|
self.output_buffer.len()
|
||||||
);
|
);
|
||||||
|
// Can't use write_to_terminal here due to borrow checker - can't pass
|
||||||
|
// &self.output_buffer while also taking &mut self
|
||||||
|
Self::update_kitty_mode_helper(
|
||||||
|
&mut self.kitty_output_parser,
|
||||||
|
&mut self.kitty_mode_stack,
|
||||||
|
self.kitty_mode_supported,
|
||||||
|
&self.output_buffer,
|
||||||
|
);
|
||||||
write_all(stdout_fd, &self.output_buffer)?;
|
write_all(stdout_fd, &self.output_buffer)?;
|
||||||
|
|
||||||
// Store current screen for next diff
|
// Store current screen for next diff
|
||||||
@@ -795,8 +794,23 @@ impl Proxy {
|
|||||||
self.output_buffer.clear();
|
self.output_buffer.clear();
|
||||||
self.history.append_all(&mut self.output_buffer);
|
self.history.append_all(&mut self.output_buffer);
|
||||||
|
|
||||||
write_all(stdout_fd, CLEAR_SCREEN)?;
|
// Debug: write history to file if CLAUDE_CHILL_HISTORY_FILE is set
|
||||||
write_all(stdout_fd, CURSOR_HOME)?;
|
if let Ok(path) = std::env::var("CLAUDE_CHILL_HISTORY_FILE")
|
||||||
|
&& let Err(e) = std::fs::write(&path, &self.output_buffer)
|
||||||
|
{
|
||||||
|
debug!("Failed to write history file: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.write_to_terminal(stdout_fd, CLEAR_SCREEN)?;
|
||||||
|
self.write_to_terminal(stdout_fd, CURSOR_HOME)?;
|
||||||
|
// Can't use write_to_terminal here due to borrow checker - can't pass
|
||||||
|
// &self.output_buffer while also taking &mut self
|
||||||
|
Self::update_kitty_mode_helper(
|
||||||
|
&mut self.kitty_output_parser,
|
||||||
|
&mut self.kitty_mode_stack,
|
||||||
|
self.kitty_mode_supported,
|
||||||
|
&self.output_buffer,
|
||||||
|
);
|
||||||
write_all(stdout_fd, &self.output_buffer)?;
|
write_all(stdout_fd, &self.output_buffer)?;
|
||||||
|
|
||||||
// Force full VT render on next output since terminal now shows history
|
// Force full VT render on next output since terminal now shows history
|
||||||
@@ -807,13 +821,13 @@ impl Proxy {
|
|||||||
fn process_input<F: AsFd>(&mut self, data: &[u8], stdout_fd: &F) -> Result<()> {
|
fn process_input<F: AsFd>(&mut self, data: &[u8], stdout_fd: &F) -> Result<()> {
|
||||||
self.last_stdin_time = Some(Instant::now());
|
self.last_stdin_time = Some(Instant::now());
|
||||||
|
|
||||||
self.update_kitty_support_from_input(data);
|
debug!("process_input: stdin={:?}", data);
|
||||||
|
|
||||||
if self.in_alternate_screen {
|
if self.in_alternate_screen {
|
||||||
return write_all(&self.pty_master, data);
|
return write_all(&self.pty_master, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
let lookback_sequence = if self.kitty_mode_enabled {
|
let lookback_sequence = if self.kitty_mode_enabled() {
|
||||||
self.config.lookback_sequence_kitty.clone()
|
self.config.lookback_sequence_kitty.clone()
|
||||||
} else {
|
} else {
|
||||||
self.config.lookback_sequence_legacy.clone()
|
self.config.lookback_sequence_legacy.clone()
|
||||||
@@ -849,18 +863,19 @@ impl Proxy {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
SequenceMatch::Partial => {}
|
SequenceMatch::Partial => {
|
||||||
|
// Still might be lookback sequence, don't forward yet
|
||||||
|
continue;
|
||||||
|
}
|
||||||
SequenceMatch::None => {
|
SequenceMatch::None => {
|
||||||
if !lookback_sequence.starts_with(&self.lookback_input_buffer) {
|
// Not a lookback sequence - forward all buffered bytes
|
||||||
|
if !self.in_lookback_mode {
|
||||||
|
write_all(&self.pty_master, &self.lookback_input_buffer)?;
|
||||||
|
}
|
||||||
self.lookback_input_buffer.clear();
|
self.lookback_input_buffer.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if lookback_action == SequenceMatch::None && !self.in_lookback_mode {
|
|
||||||
write_all(&self.pty_master, &[byte])?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1076,212 +1091,259 @@ fn nix_read<F: AsFd>(fd: &F, buf: &mut [u8]) -> Result<usize, Errno> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
// Tests for is_complete_kitty_output_seq
|
// Helper to test Kitty tracking using the real update_kitty_mode_helper
|
||||||
|
struct KittyTracker {
|
||||||
|
parser: TermwizParser,
|
||||||
|
mode_supported: bool,
|
||||||
|
mode_stack: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KittyTracker {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
parser: TermwizParser::new(),
|
||||||
|
mode_supported: false,
|
||||||
|
mode_stack: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mode_enabled(&self) -> bool {
|
||||||
|
self.mode_stack > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_output(&mut self, data: &[u8]) {
|
||||||
|
// Uses the real production function
|
||||||
|
Proxy::update_kitty_mode_helper(
|
||||||
|
&mut self.parser,
|
||||||
|
&mut self.mode_stack,
|
||||||
|
self.mode_supported,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_input(&mut self, data: &[u8]) {
|
||||||
|
// Kitty support detection from query response (CSI ? flags u)
|
||||||
|
// This is done separately in detect_kitty_support() at startup
|
||||||
|
if self.mode_supported {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let actions = self.parser.parse_as_vec(data);
|
||||||
|
for action in actions {
|
||||||
|
if let Action::CSI(CSI::Keyboard(Keyboard::ReportKittyState(_))) = action {
|
||||||
|
self.mode_supported = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests for Kitty keyboard protocol tracking
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_output_seq_disable_complete() {
|
fn test_kitty_initially_disabled() {
|
||||||
assert!(is_complete_kitty_output_seq(b"\x1b[<u"));
|
let tracker = KittyTracker::new();
|
||||||
|
assert!(!tracker.mode_enabled());
|
||||||
|
assert!(!tracker.mode_supported);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_output_seq_disable_with_trailing() {
|
fn test_kitty_support_detected_from_query_response() {
|
||||||
assert!(is_complete_kitty_output_seq(b"\x1b[<uhello"));
|
let mut tracker = KittyTracker::new();
|
||||||
|
// Terminal responds to query with CSI ? flags u
|
||||||
|
tracker.process_input(b"\x1b[?1u");
|
||||||
|
assert!(tracker.mode_supported);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_output_seq_enable_complete() {
|
fn test_kitty_push_increments_stack() {
|
||||||
assert!(is_complete_kitty_output_seq(b"\x1b[>1u"));
|
let mut tracker = KittyTracker::new();
|
||||||
|
tracker.mode_supported = true;
|
||||||
|
// CSI > 1 u = push with flags
|
||||||
|
tracker.process_output(b"\x1b[>1u");
|
||||||
|
assert_eq!(tracker.mode_stack, 1);
|
||||||
|
assert!(tracker.mode_enabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_output_seq_enable_multi_digit() {
|
fn test_kitty_push_requires_support() {
|
||||||
assert!(is_complete_kitty_output_seq(b"\x1b[>31u"));
|
let mut tracker = KittyTracker::new();
|
||||||
|
// Push without support detection - should be ignored
|
||||||
|
tracker.process_output(b"\x1b[>1u");
|
||||||
|
assert_eq!(tracker.mode_stack, 0);
|
||||||
|
assert!(!tracker.mode_enabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_output_seq_enable_with_trailing() {
|
fn test_kitty_pop_decrements_stack() {
|
||||||
assert!(is_complete_kitty_output_seq(b"\x1b[>1umore"));
|
let mut tracker = KittyTracker::new();
|
||||||
|
tracker.mode_supported = true;
|
||||||
|
tracker.process_output(b"\x1b[>1u"); // push
|
||||||
|
tracker.process_output(b"\x1b[<u"); // pop 1
|
||||||
|
assert_eq!(tracker.mode_stack, 0);
|
||||||
|
assert!(!tracker.mode_enabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_output_seq_incomplete_esc_only() {
|
fn test_kitty_pop_with_count() {
|
||||||
assert!(!is_complete_kitty_output_seq(b"\x1b"));
|
let mut tracker = KittyTracker::new();
|
||||||
|
tracker.mode_supported = true;
|
||||||
|
tracker.process_output(b"\x1b[>1u"); // push
|
||||||
|
tracker.process_output(b"\x1b[>1u"); // push
|
||||||
|
tracker.process_output(b"\x1b[>1u"); // push
|
||||||
|
assert_eq!(tracker.mode_stack, 3);
|
||||||
|
tracker.process_output(b"\x1b[<2u"); // pop 2
|
||||||
|
assert_eq!(tracker.mode_stack, 1);
|
||||||
|
assert!(tracker.mode_enabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_output_seq_incomplete_esc_bracket() {
|
fn test_kitty_pop_saturates_at_zero() {
|
||||||
assert!(!is_complete_kitty_output_seq(b"\x1b["));
|
let mut tracker = KittyTracker::new();
|
||||||
|
tracker.mode_supported = true;
|
||||||
|
tracker.process_output(b"\x1b[>1u"); // push
|
||||||
|
tracker.process_output(b"\x1b[<5u"); // pop 5 (more than we have)
|
||||||
|
assert_eq!(tracker.mode_stack, 0);
|
||||||
|
assert!(!tracker.mode_enabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_output_seq_incomplete_disable() {
|
fn test_kitty_split_sequence_across_buffers() {
|
||||||
assert!(!is_complete_kitty_output_seq(b"\x1b[<"));
|
let mut tracker = KittyTracker::new();
|
||||||
|
tracker.mode_supported = true;
|
||||||
|
// Feed the sequence in parts
|
||||||
|
tracker.process_output(b"\x1b[>");
|
||||||
|
tracker.process_output(b"1u");
|
||||||
|
assert_eq!(tracker.mode_stack, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_output_seq_incomplete_enable_no_digit() {
|
fn test_kitty_multiple_sequences_in_one_buffer() {
|
||||||
assert!(!is_complete_kitty_output_seq(b"\x1b[>"));
|
let mut tracker = KittyTracker::new();
|
||||||
|
tracker.mode_supported = true;
|
||||||
|
// Push twice, pop once, all in one buffer
|
||||||
|
tracker.process_output(b"\x1b[>1u\x1b[>1u\x1b[<u");
|
||||||
|
assert_eq!(tracker.mode_stack, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_output_seq_incomplete_enable_digit_no_u() {
|
fn test_kitty_mixed_with_other_sequences() {
|
||||||
assert!(!is_complete_kitty_output_seq(b"\x1b[>1"));
|
let mut tracker = KittyTracker::new();
|
||||||
|
tracker.mode_supported = true;
|
||||||
|
// Kitty push mixed with cursor moves and SGR
|
||||||
|
tracker.process_output(b"\x1b[H\x1b[>1u\x1b[31m\x1b[2J");
|
||||||
|
assert_eq!(tracker.mode_stack, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_output_seq_not_kitty() {
|
fn test_kitty_typical_session_flow() {
|
||||||
assert!(!is_complete_kitty_output_seq(b"\x1b[H"));
|
let mut tracker = KittyTracker::new();
|
||||||
assert!(!is_complete_kitty_output_seq(b"\x1b[2J"));
|
// 1. Terminal responds to query
|
||||||
|
tracker.process_input(b"\x1b[?1u");
|
||||||
|
assert!(tracker.mode_supported);
|
||||||
|
assert!(!tracker.mode_enabled());
|
||||||
|
// 2. App pushes keyboard mode
|
||||||
|
tracker.process_output(b"\x1b[>1u");
|
||||||
|
assert!(tracker.mode_enabled());
|
||||||
|
// 3. App pops keyboard mode on exit
|
||||||
|
tracker.process_output(b"\x1b[<u");
|
||||||
|
assert!(!tracker.mode_enabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests for sequence matching (used for lookback key detection)
|
||||||
|
|
||||||
|
fn check_sequence(buffer: &[u8], byte: u8, sequence: &[u8]) -> SequenceMatch {
|
||||||
|
let mut buf = buffer.to_vec();
|
||||||
|
buf.push(byte);
|
||||||
|
if buf.len() > sequence.len() {
|
||||||
|
let excess = buf.len() - sequence.len();
|
||||||
|
buf.drain(..excess);
|
||||||
|
}
|
||||||
|
if buf.as_slice() == sequence {
|
||||||
|
SequenceMatch::Complete
|
||||||
|
} else if sequence.starts_with(&buf) {
|
||||||
|
SequenceMatch::Partial
|
||||||
|
} else {
|
||||||
|
SequenceMatch::None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_output_seq_empty() {
|
fn test_sequence_match_complete_single_byte() {
|
||||||
assert!(!is_complete_kitty_output_seq(b""));
|
// Single byte sequence (legacy Ctrl+6 = 0x1E)
|
||||||
|
let sequence = &[0x1E];
|
||||||
|
assert_eq!(check_sequence(&[], 0x1E, sequence), SequenceMatch::Complete);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_output_seq_no_esc() {
|
fn test_sequence_match_complete_multi_byte() {
|
||||||
assert!(!is_complete_kitty_output_seq(b"hello"));
|
// Multi-byte Kitty sequence: ESC [ 5 4 ; 5 u
|
||||||
|
let sequence = b"\x1b[54;5u";
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
for &byte in &sequence[..sequence.len() - 1] {
|
||||||
|
let result = check_sequence(&buffer, byte, sequence);
|
||||||
|
assert_eq!(result, SequenceMatch::Partial);
|
||||||
|
buffer.push(byte);
|
||||||
|
if buffer.len() > sequence.len() {
|
||||||
|
buffer.drain(..buffer.len() - sequence.len());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Tests for is_complete_kitty_input_seq
|
// Final byte completes the sequence
|
||||||
|
assert_eq!(
|
||||||
#[test]
|
check_sequence(&buffer, sequence[sequence.len() - 1], sequence),
|
||||||
fn test_input_seq_query_response_complete() {
|
SequenceMatch::Complete
|
||||||
assert!(is_complete_kitty_input_seq(b"\x1b[?1u"));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_input_seq_query_response_multi_digit() {
|
fn test_sequence_match_partial() {
|
||||||
assert!(is_complete_kitty_input_seq(b"\x1b[?31u"));
|
let sequence = b"\x1b[54;5u";
|
||||||
|
assert_eq!(check_sequence(&[], 0x1b, sequence), SequenceMatch::Partial);
|
||||||
|
assert_eq!(
|
||||||
|
check_sequence(&[0x1b], b'[', sequence),
|
||||||
|
SequenceMatch::Partial
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
check_sequence(&[0x1b, b'['], b'5', sequence),
|
||||||
|
SequenceMatch::Partial
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_input_seq_query_response_with_trailing() {
|
fn test_sequence_match_none_wrong_byte() {
|
||||||
assert!(is_complete_kitty_input_seq(b"\x1b[?1umore"));
|
let sequence = b"\x1b[54;5u";
|
||||||
|
// Start with wrong byte
|
||||||
|
assert_eq!(check_sequence(&[], b'a', sequence), SequenceMatch::None);
|
||||||
|
// Wrong byte after partial match
|
||||||
|
assert_eq!(check_sequence(&[0x1b], b'O', sequence), SequenceMatch::None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_input_seq_incomplete_esc_only() {
|
fn test_sequence_match_buffer_rolling() {
|
||||||
assert!(!is_complete_kitty_input_seq(b"\x1b"));
|
// Test that the rolling buffer properly handles the case where
|
||||||
|
// random bytes precede the actual sequence. The buffer keeps
|
||||||
|
// only the last N bytes where N = sequence.len()
|
||||||
|
let sequence = b"\x1b[54;5u"; // 7 bytes
|
||||||
|
// User types random chars - no match
|
||||||
|
assert_eq!(check_sequence(&[], b'a', sequence), SequenceMatch::None);
|
||||||
|
assert_eq!(check_sequence(b"a", b'b', sequence), SequenceMatch::None);
|
||||||
|
// Buffer [a, b, ESC] doesn't start sequence (sequence starts with ESC)
|
||||||
|
assert_eq!(check_sequence(b"ab", 0x1b, sequence), SequenceMatch::None);
|
||||||
|
// After more typing, old bytes get trimmed from buffer
|
||||||
|
// When buffer finally contains just ESC at the right position, it matches
|
||||||
|
// But with rolling buffer, we need the EXACT prefix
|
||||||
|
// Fresh start: ESC alone is a partial match
|
||||||
|
assert_eq!(check_sequence(&[], 0x1b, sequence), SequenceMatch::Partial);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_input_seq_incomplete_esc_bracket() {
|
fn test_sequence_match_interleaved_typing() {
|
||||||
assert!(!is_complete_kitty_input_seq(b"\x1b["));
|
// User types "ab" then the lookback sequence
|
||||||
}
|
let sequence = &[0x1E];
|
||||||
|
assert_eq!(check_sequence(&[], b'a', sequence), SequenceMatch::None);
|
||||||
#[test]
|
assert_eq!(check_sequence(b"a", b'b', sequence), SequenceMatch::None);
|
||||||
fn test_input_seq_incomplete_esc_bracket_question() {
|
assert_eq!(
|
||||||
assert!(!is_complete_kitty_input_seq(b"\x1b[?"));
|
check_sequence(b"ab", 0x1E, sequence),
|
||||||
}
|
SequenceMatch::Complete
|
||||||
|
);
|
||||||
#[test]
|
|
||||||
fn test_input_seq_incomplete_no_u() {
|
|
||||||
assert!(!is_complete_kitty_input_seq(b"\x1b[?1"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_input_seq_not_kitty() {
|
|
||||||
assert!(!is_complete_kitty_input_seq(b"\x1b[H"));
|
|
||||||
assert!(!is_complete_kitty_input_seq(b"\x1b[?25h"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_input_seq_empty() {
|
|
||||||
assert!(!is_complete_kitty_input_seq(b""));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests for kitty_output_split_point
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_output_split_no_esc() {
|
|
||||||
assert_eq!(kitty_output_split_point(b"hello world"), 11);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_output_split_empty() {
|
|
||||||
assert_eq!(kitty_output_split_point(b""), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_output_split_complete_disable() {
|
|
||||||
// Complete sequence - split at end (process all)
|
|
||||||
assert_eq!(kitty_output_split_point(b"\x1b[<u"), 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_output_split_complete_enable() {
|
|
||||||
assert_eq!(kitty_output_split_point(b"\x1b[>1u"), 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_output_split_incomplete_esc_only() {
|
|
||||||
// Just ESC - incomplete, split at 0 (keep all)
|
|
||||||
assert_eq!(kitty_output_split_point(b"\x1b"), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_output_split_incomplete_enable() {
|
|
||||||
// Incomplete enable - split at ESC position
|
|
||||||
assert_eq!(kitty_output_split_point(b"\x1b[>"), 0);
|
|
||||||
assert_eq!(kitty_output_split_point(b"\x1b[>1"), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_output_split_data_then_incomplete() {
|
|
||||||
// Data followed by incomplete - split before ESC
|
|
||||||
let data = b"hello\x1b[>";
|
|
||||||
assert_eq!(kitty_output_split_point(data), 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_output_split_complete_then_incomplete() {
|
|
||||||
// Complete disable followed by incomplete enable
|
|
||||||
let data = b"\x1b[<u\x1b[>";
|
|
||||||
assert_eq!(kitty_output_split_point(data), 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_output_split_esc_far_from_end() {
|
|
||||||
// ESC more than 16 bytes from end - process all
|
|
||||||
let mut data = b"\x1b[<u".to_vec();
|
|
||||||
data.extend_from_slice(b"this is padding longer than 16 bytes!");
|
|
||||||
assert_eq!(kitty_output_split_point(&data), data.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests for kitty_input_split_point
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_input_split_no_esc() {
|
|
||||||
assert_eq!(kitty_input_split_point(b"hello world"), 11);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_input_split_empty() {
|
|
||||||
assert_eq!(kitty_input_split_point(b""), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_input_split_complete_query_response() {
|
|
||||||
assert_eq!(kitty_input_split_point(b"\x1b[?1u"), 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_input_split_incomplete() {
|
|
||||||
assert_eq!(kitty_input_split_point(b"\x1b[?"), 0);
|
|
||||||
assert_eq!(kitty_input_split_point(b"\x1b[?1"), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_input_split_data_then_incomplete() {
|
|
||||||
let data = b"hello\x1b[?";
|
|
||||||
assert_eq!(kitty_input_split_point(data), 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_input_split_complete_then_incomplete() {
|
|
||||||
let data = b"\x1b[?1u\x1b[?";
|
|
||||||
assert_eq!(kitty_input_split_point(data), 5);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user