feat(vt): port Ghostty's VT parsing layer
Ports the parsing half of ZIG/src/terminal into the ghostty-vt crate: - Parser state machine with a compile-time generated parse table and the UTF-8 decoder - OSC parser incl. all sub-parsers (titles, colors, clipboard, hyperlinks, semantic prompts/OSC 133, OSC 9 / ConEmu, iTerm2, kitty clipboard, color and text-sizing protocols, context signals) - CSI, SGR, modes table with save/restore, charsets, ansi, device status, mouse shapes, APC, DCS (incl. XTGETTCAP), tmux control-mode parser - color palette / X11 color names, kitty color and graphics command parser Deviations from Zig (documented in docs/rewrite/notes/ghostty-vt-W*.md): parser actions own their payloads instead of borrowing parser buffers; OSC strings are Vec<u8>; non-exhaustive Zig enums gain an Other(raw) variant; tmux regexes replaced by equivalent hand-written matchers. Known follow-up: semantic_prompt.rs carries local copies of the printf-q and percent decoders that belong in ghostty-os. Test Plan: - cargo test -p ghostty-vt: 395 passed, 1 ignored (skipped in Zig too) - cargo clippy -p ghostty-vt --all-targets -- -D warnings: clean
This commit is contained in:
1 parent
987f2957ad
commit
bf84326c2f
44 files changed
+15040
-6
No files matched your search
Generated
+1501
-3
@@ -2,6 +2,507 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayref"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayvec"
|
||||||
|
version = "0.7.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-broadcast"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
|
||||||
|
dependencies = [
|
||||||
|
"event-listener",
|
||||||
|
"event-listener-strategy",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-channel"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
|
||||||
|
dependencies = [
|
||||||
|
"concurrent-queue",
|
||||||
|
"event-listener-strategy",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-executor"
|
||||||
|
version = "1.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
|
||||||
|
dependencies = [
|
||||||
|
"async-task",
|
||||||
|
"concurrent-queue",
|
||||||
|
"fastrand",
|
||||||
|
"futures-lite",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-io"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"cfg-if",
|
||||||
|
"concurrent-queue",
|
||||||
|
"futures-io",
|
||||||
|
"futures-lite",
|
||||||
|
"parking",
|
||||||
|
"polling",
|
||||||
|
"rustix",
|
||||||
|
"slab",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-lock"
|
||||||
|
version = "3.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
|
||||||
|
dependencies = [
|
||||||
|
"event-listener",
|
||||||
|
"event-listener-strategy",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-process"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
|
||||||
|
dependencies = [
|
||||||
|
"async-channel",
|
||||||
|
"async-io",
|
||||||
|
"async-lock",
|
||||||
|
"async-signal",
|
||||||
|
"async-task",
|
||||||
|
"blocking",
|
||||||
|
"cfg-if",
|
||||||
|
"event-listener",
|
||||||
|
"futures-lite",
|
||||||
|
"rustix",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-recursion"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.8",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-signal"
|
||||||
|
version = "0.2.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
|
||||||
|
dependencies = [
|
||||||
|
"async-io",
|
||||||
|
"async-lock",
|
||||||
|
"atomic-waker",
|
||||||
|
"cfg-if",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"rustix",
|
||||||
|
"signal-hook-registry",
|
||||||
|
"slab",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-task"
|
||||||
|
version = "4.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-trait"
|
||||||
|
version = "0.1.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.8",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic-waker"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.23.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bindgen"
|
||||||
|
version = "0.72.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cexpr",
|
||||||
|
"clang-sys",
|
||||||
|
"itertools",
|
||||||
|
"log",
|
||||||
|
"prettyplease",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"rustc-hash",
|
||||||
|
"shlex 1.3.0",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blocking"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa"
|
||||||
|
dependencies = [
|
||||||
|
"async-channel",
|
||||||
|
"async-task",
|
||||||
|
"futures-io",
|
||||||
|
"futures-lite",
|
||||||
|
"piper",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.25.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838"
|
||||||
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
|
"shlex 2.0.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cexpr"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clang-sys"
|
||||||
|
version = "1.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
|
||||||
|
dependencies = [
|
||||||
|
"glob",
|
||||||
|
"libc",
|
||||||
|
"libloading",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "concurrent-queue"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const_format"
|
||||||
|
version = "0.2.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e"
|
||||||
|
dependencies = [
|
||||||
|
"const_format_proc_macros",
|
||||||
|
"konst",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const_format_proc_macros"
|
||||||
|
version = "0.2.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc32fast"
|
||||||
|
version = "1.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crunchy"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "endi"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enumflags2"
|
||||||
|
version = "0.7.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
|
||||||
|
dependencies = [
|
||||||
|
"enumflags2_derive",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enumflags2_derive"
|
||||||
|
version = "0.7.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
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 = "event-listener"
|
||||||
|
version = "5.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
|
||||||
|
dependencies = [
|
||||||
|
"parking",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "event-listener-strategy"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||||
|
dependencies = [
|
||||||
|
"event-listener",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fdeflate"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||||
|
dependencies = [
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "find-msvc-tools"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"miniz_oxide 0.9.1",
|
||||||
|
"zlib-rs",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-io"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-lite"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"parking",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gettext-rs"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9df5e3fa8c077b15fdfa8ce130e2bc73d663b8aaa6d51b71421b8659980b1d80"
|
||||||
|
dependencies = [
|
||||||
|
"gettext-sys",
|
||||||
|
"locale_config",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gettext-sys"
|
||||||
|
version = "0.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4caa7e4460b196107cc1bb2629488e8b6dbb7f34f6683ea6753c4ac22f590785"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"temp-dir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ghostty-apprt-gtk"
|
name = "ghostty-apprt-gtk"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -15,9 +516,12 @@ dependencies = [
|
|||||||
name = "ghostty-config"
|
name = "ghostty-config"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"const_format",
|
||||||
"ghostty-input",
|
"ghostty-input",
|
||||||
"ghostty-os",
|
"ghostty-os",
|
||||||
"ghostty-vt",
|
"ghostty-vt",
|
||||||
|
"log",
|
||||||
|
"onig",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -37,14 +541,19 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ghostty-datastruct"
|
name = "ghostty-datastruct"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"half",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ghostty-font"
|
name = "ghostty-font"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ghostty-config",
|
"log",
|
||||||
"ghostty-terminal",
|
"png",
|
||||||
"ghostty-unicode",
|
"thiserror",
|
||||||
|
"tiny-skia",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -52,11 +561,22 @@ name = "ghostty-input"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ghostty-vt",
|
"ghostty-vt",
|
||||||
|
"indexmap",
|
||||||
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ghostty-os"
|
name = "ghostty-os"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"gettext-rs",
|
||||||
|
"log",
|
||||||
|
"nix",
|
||||||
|
"rustix",
|
||||||
|
"temp-env",
|
||||||
|
"thiserror",
|
||||||
|
"zbus",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ghostty-renderer"
|
name = "ghostty-renderer"
|
||||||
@@ -101,3 +621,981 @@ version = "0.1.0"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ghostty-vt"
|
name = "ghostty-vt"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"log",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glob"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "half"
|
||||||
|
version = "2.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"crunchy",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.17.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.14.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "konst"
|
||||||
|
version = "0.2.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb"
|
||||||
|
dependencies = [
|
||||||
|
"konst_macro_rules",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "konst_macro_rules"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.190"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libloading"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "locale_config"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08d2c35b16f4483f6c26f0e4e9550717a2f6575bcd6f12a53ff0c490a94a6934"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"objc",
|
||||||
|
"objc-foundation",
|
||||||
|
"regex",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "malloc_buf"
|
||||||
|
version = "0.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
|
[[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 = "miniz_oxide"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.31.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"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 = "objc"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
|
||||||
|
dependencies = [
|
||||||
|
"malloc_buf",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc-foundation"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
|
||||||
|
dependencies = [
|
||||||
|
"block",
|
||||||
|
"objc",
|
||||||
|
"objc_id",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc_id"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
|
||||||
|
dependencies = [
|
||||||
|
"objc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "onig"
|
||||||
|
version = "6.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"onig_sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "onig_sys"
|
||||||
|
version = "69.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7"
|
||||||
|
dependencies = [
|
||||||
|
"bindgen",
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordered-stream"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking"
|
||||||
|
version = "2.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.12.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.9.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "piper"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"fastrand",
|
||||||
|
"futures-io",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkg-config"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "png"
|
||||||
|
version = "0.18.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"crc32fast",
|
||||||
|
"fdeflate",
|
||||||
|
"flate2",
|
||||||
|
"miniz_oxide 0.8.9",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "polling"
|
||||||
|
version = "3.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"concurrent-queue",
|
||||||
|
"hermit-abi",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prettyplease"
|
||||||
|
version = "0.2.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-crate"
|
||||||
|
version = "3.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||||
|
dependencies = [
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.107"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.47"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.8",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_repr"
|
||||||
|
version = "0.1.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.8",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||||
|
|
||||||
|
[[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 = "simd-adler32"
|
||||||
|
version = "0.3.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.16.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strict-num"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.119"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "01016da373cd8f7ef12624f796309f5c31ba8d646dd08856c02cd741d823c622"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "temp-dir"
|
||||||
|
version = "0.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83176759e9416cf81ee66cb6508dbfe9c96f20b8b56265a39917551c23c70964"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "temp-env"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050"
|
||||||
|
dependencies = [
|
||||||
|
"parking_lot",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.8",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tiny-skia"
|
||||||
|
version = "0.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea"
|
||||||
|
dependencies = [
|
||||||
|
"arrayref",
|
||||||
|
"arrayvec",
|
||||||
|
"bytemuck",
|
||||||
|
"cfg-if",
|
||||||
|
"log",
|
||||||
|
"png",
|
||||||
|
"tiny-skia-path",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tiny-skia-path"
|
||||||
|
version = "0.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9"
|
||||||
|
dependencies = [
|
||||||
|
"arrayref",
|
||||||
|
"bytemuck",
|
||||||
|
"strict-num",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "1.1.2+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.25.17+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_parser",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_parser"
|
||||||
|
version = "1.1.5+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
|
||||||
|
dependencies = [
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing"
|
||||||
|
version = "0.1.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
|
dependencies = [
|
||||||
|
"pin-project-lite",
|
||||||
|
"tracing-attributes",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-attributes"
|
||||||
|
version = "0.1.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-core"
|
||||||
|
version = "0.1.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uds_windows"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||||
|
dependencies = [
|
||||||
|
"memoffset",
|
||||||
|
"tempfile",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "1.28.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"serde_core",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.129"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.129"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.129"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.8",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.129"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zbus"
|
||||||
|
version = "5.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907"
|
||||||
|
dependencies = [
|
||||||
|
"async-broadcast",
|
||||||
|
"async-executor",
|
||||||
|
"async-io",
|
||||||
|
"async-lock",
|
||||||
|
"async-process",
|
||||||
|
"async-recursion",
|
||||||
|
"async-task",
|
||||||
|
"async-trait",
|
||||||
|
"blocking",
|
||||||
|
"enumflags2",
|
||||||
|
"event-listener",
|
||||||
|
"futures-core",
|
||||||
|
"futures-lite",
|
||||||
|
"hex",
|
||||||
|
"libc",
|
||||||
|
"ordered-stream",
|
||||||
|
"rustix",
|
||||||
|
"serde",
|
||||||
|
"serde_repr",
|
||||||
|
"tracing",
|
||||||
|
"uds_windows",
|
||||||
|
"uuid",
|
||||||
|
"windows-sys",
|
||||||
|
"winnow",
|
||||||
|
"zbus_macros",
|
||||||
|
"zbus_names",
|
||||||
|
"zvariant",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zbus_macros"
|
||||||
|
version = "5.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.8",
|
||||||
|
"zbus_names",
|
||||||
|
"zvariant",
|
||||||
|
"zvariant_utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zbus_names"
|
||||||
|
version = "4.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"winnow",
|
||||||
|
"zvariant",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zcheapstr"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.62"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.62"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zlib-rs"
|
||||||
|
version = "0.6.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zvariant"
|
||||||
|
version = "5.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147"
|
||||||
|
dependencies = [
|
||||||
|
"endi",
|
||||||
|
"enumflags2",
|
||||||
|
"serde",
|
||||||
|
"winnow",
|
||||||
|
"zcheapstr",
|
||||||
|
"zvariant_derive",
|
||||||
|
"zvariant_utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zvariant_derive"
|
||||||
|
version = "5.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.8",
|
||||||
|
"zvariant_utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zvariant_utils"
|
||||||
|
version = "4.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"serde",
|
||||||
|
"syn 3.0.8",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
@@ -5,6 +5,9 @@ edition.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
base64 = "0.23.1"
|
||||||
|
log = "0.4.34"
|
||||||
|
thiserror = "2.0.21"
|
||||||
|
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
//! ANSI types (ZIG/src/terminal/ansi.zig).
|
||||||
|
|
||||||
|
/// C0 (7-bit) control characters from ANSI.
|
||||||
|
///
|
||||||
|
/// This is not complete, control characters are only added to this
|
||||||
|
/// as the terminal emulator handles them.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum C0 {
|
||||||
|
/// Null
|
||||||
|
Nul = 0x00,
|
||||||
|
/// Start of heading
|
||||||
|
Soh = 0x01,
|
||||||
|
/// Start of text
|
||||||
|
Stx = 0x02,
|
||||||
|
/// Enquiry
|
||||||
|
Enq = 0x05,
|
||||||
|
/// Bell
|
||||||
|
Bel = 0x07,
|
||||||
|
/// Backspace
|
||||||
|
Bs = 0x08,
|
||||||
|
/// Horizontal tab
|
||||||
|
Ht = 0x09,
|
||||||
|
/// Line feed
|
||||||
|
Lf = 0x0A,
|
||||||
|
/// Vertical Tab
|
||||||
|
Vt = 0x0B,
|
||||||
|
/// Form feed
|
||||||
|
Ff = 0x0C,
|
||||||
|
/// Carriage return
|
||||||
|
Cr = 0x0D,
|
||||||
|
/// Shift out
|
||||||
|
So = 0x0E,
|
||||||
|
/// Shift in
|
||||||
|
Si = 0x0F,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl C0 {
|
||||||
|
/// Convert from a raw byte; unknown values return `None` (the Zig enum
|
||||||
|
/// is non-exhaustive).
|
||||||
|
pub fn from_int(v: u8) -> Option<Self> {
|
||||||
|
Some(match v {
|
||||||
|
0x00 => Self::Nul,
|
||||||
|
0x01 => Self::Soh,
|
||||||
|
0x02 => Self::Stx,
|
||||||
|
0x05 => Self::Enq,
|
||||||
|
0x07 => Self::Bel,
|
||||||
|
0x08 => Self::Bs,
|
||||||
|
0x09 => Self::Ht,
|
||||||
|
0x0A => Self::Lf,
|
||||||
|
0x0B => Self::Vt,
|
||||||
|
0x0C => Self::Ff,
|
||||||
|
0x0D => Self::Cr,
|
||||||
|
0x0E => Self::So,
|
||||||
|
0x0F => Self::Si,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The SGR rendition aspects that can be set, sometimes known as attributes.
|
||||||
|
/// The value corresponds to the parameter value for the SGR command (ESC [ m).
|
||||||
|
/// Non-exhaustive in Zig; `Other` carries unknown values.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum RenditionAspect {
|
||||||
|
Default,
|
||||||
|
Bold,
|
||||||
|
DefaultFg,
|
||||||
|
DefaultBg,
|
||||||
|
Other(u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenditionAspect {
|
||||||
|
pub fn from_int(v: u16) -> Self {
|
||||||
|
match v {
|
||||||
|
0 => Self::Default,
|
||||||
|
1 => Self::Bold,
|
||||||
|
39 => Self::DefaultFg,
|
||||||
|
49 => Self::DefaultBg,
|
||||||
|
v => Self::Other(v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The device attribute request type (ESC [ c).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum DeviceAttributeReq {
|
||||||
|
/// Blank
|
||||||
|
Primary,
|
||||||
|
/// >
|
||||||
|
Secondary,
|
||||||
|
/// =
|
||||||
|
Tertiary,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Possible cursor styles (ESC [ q)
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum CursorStyle {
|
||||||
|
Default,
|
||||||
|
BlinkingBlock,
|
||||||
|
SteadyBlock,
|
||||||
|
BlinkingUnderline,
|
||||||
|
SteadyUnderline,
|
||||||
|
BlinkingBar,
|
||||||
|
SteadyBar,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The status line type for DECSSDT. Non-exhaustive in Zig.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum StatusLineType {
|
||||||
|
None,
|
||||||
|
Indicator,
|
||||||
|
HostWritable,
|
||||||
|
Other(u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatusLineType {
|
||||||
|
pub fn from_int(v: u16) -> Self {
|
||||||
|
match v {
|
||||||
|
0 => Self::None,
|
||||||
|
1 => Self::Indicator,
|
||||||
|
2 => Self::HostWritable,
|
||||||
|
v => Self::Other(v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The display to target for status updates (DECSASD).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum StatusDisplay {
|
||||||
|
Main,
|
||||||
|
StatusLine,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The possible modify key formats to ESC[>{a};{b}m
|
||||||
|
/// Note: this is not complete, we should add more as we support more
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ModifyKeyFormat {
|
||||||
|
Legacy,
|
||||||
|
CursorKeys,
|
||||||
|
FunctionKeys,
|
||||||
|
OtherKeysNone,
|
||||||
|
OtherKeysNumericExcept,
|
||||||
|
OtherKeysNumeric,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The protection modes that can be set for the terminal. See DECSCA and
|
||||||
|
/// ESC V, W.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ProtectedMode {
|
||||||
|
Off,
|
||||||
|
/// ESC V, W
|
||||||
|
Iso,
|
||||||
|
/// CSI Ps " q
|
||||||
|
Dec,
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
//! APC command handler (ZIG/src/terminal/apc.zig). This should be hooked
|
||||||
|
//! into a stream. The start/feed/end functions are meant to be called from
|
||||||
|
//! the stream's apcStart, apcPut, and apcEnd functions, respectively.
|
||||||
|
|
||||||
|
use crate::kitty::graphics_command as kitty_gfx;
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub enum State {
|
||||||
|
/// We're not in the middle of an APC command yet.
|
||||||
|
#[default]
|
||||||
|
Inactive,
|
||||||
|
|
||||||
|
/// We got an unrecognized APC sequence or the APC sequence we
|
||||||
|
/// recognized became invalid. We're just dropping bytes.
|
||||||
|
Ignore,
|
||||||
|
|
||||||
|
/// We're waiting to identify the APC sequence. This is done by
|
||||||
|
/// inspecting the first byte of the sequence.
|
||||||
|
Identify,
|
||||||
|
|
||||||
|
/// Kitty graphics protocol
|
||||||
|
Kitty(kitty_gfx::Parser),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Possible APC commands.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Command {
|
||||||
|
Kitty(kitty_gfx::Command),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Handler {
|
||||||
|
pub state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&mut self) {
|
||||||
|
self.state = State::Identify;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn feed(&mut self, byte: u8) {
|
||||||
|
match &mut self.state {
|
||||||
|
State::Inactive => unreachable!("APC feed while inactive"),
|
||||||
|
|
||||||
|
// We're ignoring this APC command, likely because we don't
|
||||||
|
// recognize it so there is no need to store the data in memory.
|
||||||
|
State::Ignore => {}
|
||||||
|
|
||||||
|
// We identify the APC command by the first byte.
|
||||||
|
State::Identify => {
|
||||||
|
self.state = match byte {
|
||||||
|
// Kitty graphics protocol
|
||||||
|
b'G' => State::Kitty(kitty_gfx::Parser::new()),
|
||||||
|
|
||||||
|
// Unknown
|
||||||
|
_ => State::Ignore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
State::Kitty(p) => {
|
||||||
|
if let Err(err) = p.feed(byte) {
|
||||||
|
log::warn!("kitty graphics protocol error: {err}");
|
||||||
|
self.state = State::Ignore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end(&mut self) -> Option<Command> {
|
||||||
|
let state = std::mem::take(&mut self.state);
|
||||||
|
match state {
|
||||||
|
State::Inactive => unreachable!("APC end while inactive"),
|
||||||
|
State::Ignore | State::Identify => None,
|
||||||
|
State::Kitty(mut p) => match p.complete() {
|
||||||
|
Ok(command) => Some(Command::Kitty(command)),
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("kitty graphics protocol error: {err}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn run(input: &str) -> Option<Command> {
|
||||||
|
let mut h = Handler::new();
|
||||||
|
h.start();
|
||||||
|
for c in input.bytes() {
|
||||||
|
h.feed(c);
|
||||||
|
}
|
||||||
|
h.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_apc_command() {
|
||||||
|
assert!(run("Xabcdef1234").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn garbage_kitty_command() {
|
||||||
|
assert!(run("Gabcdef1234").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kitty_command_with_overflow_u32() {
|
||||||
|
assert!(run("Ga=p,i=10000000000").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kitty_command_with_overflow_i32() {
|
||||||
|
assert!(run("Ga=p,i=1,z=-9999999999").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn valid_kitty_command() {
|
||||||
|
let cmd = run("Gf=24,s=10,v=20,hello=world").unwrap();
|
||||||
|
assert!(matches!(cmd, Command::Kitty(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn end_resets_to_inactive() {
|
||||||
|
let mut h = Handler::new();
|
||||||
|
h.start();
|
||||||
|
h.feed(b'X');
|
||||||
|
assert!(h.end().is_none());
|
||||||
|
assert!(matches!(h.state, State::Inactive));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
//! Character sets (ZIG/src/terminal/charsets.zig).
|
||||||
|
|
||||||
|
/// The available charset slots for a terminal.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum Slots {
|
||||||
|
G0,
|
||||||
|
G1,
|
||||||
|
G2,
|
||||||
|
G3,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The name of the active slots.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum ActiveSlot {
|
||||||
|
GL,
|
||||||
|
GR,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of supported character sets and their associated tables.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum Charset {
|
||||||
|
Utf8,
|
||||||
|
Ascii,
|
||||||
|
British,
|
||||||
|
DecSpecial,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Our table length is 256 so we can contain all ASCII chars.
|
||||||
|
pub const TABLE_LEN: usize = 256;
|
||||||
|
|
||||||
|
/// The table for the given charset. The returned table is guaranteed to be
|
||||||
|
/// 256 entries that can be used to map ASCII to the given charset.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics for `Charset::Utf8`: utf8 is not a table, callers should
|
||||||
|
/// double-check if the charset is utf8 and NOT use tables.
|
||||||
|
pub fn table(set: Charset) -> &'static [u16; TABLE_LEN] {
|
||||||
|
match set {
|
||||||
|
Charset::British => &BRITISH,
|
||||||
|
Charset::DecSpecial => &DEC_SPECIAL,
|
||||||
|
Charset::Utf8 => unreachable!("utf8 has no table"),
|
||||||
|
Charset::Ascii => &ASCII,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a table that maps ASCII to ASCII as a getting started point.
|
||||||
|
const fn init_table() -> [u16; TABLE_LEN] {
|
||||||
|
let mut result = [0u16; TABLE_LEN];
|
||||||
|
let mut i = 0;
|
||||||
|
while i < TABLE_LEN {
|
||||||
|
result[i] = i as u16;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Just a basic c => c ascii table
|
||||||
|
static ASCII: [u16; TABLE_LEN] = init_table();
|
||||||
|
|
||||||
|
/// https://vt100.net/docs/vt220-rm/chapter2.html
|
||||||
|
static BRITISH: [u16; TABLE_LEN] = {
|
||||||
|
let mut t = init_table();
|
||||||
|
t[0x23] = 0x00a3;
|
||||||
|
t
|
||||||
|
};
|
||||||
|
|
||||||
|
/// https://en.wikipedia.org/wiki/DEC_Special_Graphics
|
||||||
|
static DEC_SPECIAL: [u16; TABLE_LEN] = {
|
||||||
|
let mut t = init_table();
|
||||||
|
t[0x60] = 0x25C6;
|
||||||
|
t[0x61] = 0x2592;
|
||||||
|
t[0x62] = 0x2409;
|
||||||
|
t[0x63] = 0x240C;
|
||||||
|
t[0x64] = 0x240D;
|
||||||
|
t[0x65] = 0x240A;
|
||||||
|
t[0x66] = 0x00B0;
|
||||||
|
t[0x67] = 0x00B1;
|
||||||
|
t[0x68] = 0x2424;
|
||||||
|
t[0x69] = 0x240B;
|
||||||
|
t[0x6a] = 0x2518;
|
||||||
|
t[0x6b] = 0x2510;
|
||||||
|
t[0x6c] = 0x250C;
|
||||||
|
t[0x6d] = 0x2514;
|
||||||
|
t[0x6e] = 0x253C;
|
||||||
|
t[0x6f] = 0x23BA;
|
||||||
|
t[0x70] = 0x23BB;
|
||||||
|
t[0x71] = 0x2500;
|
||||||
|
t[0x72] = 0x23BC;
|
||||||
|
t[0x73] = 0x23BD;
|
||||||
|
t[0x74] = 0x251C;
|
||||||
|
t[0x75] = 0x2524;
|
||||||
|
t[0x76] = 0x2534;
|
||||||
|
t[0x77] = 0x252C;
|
||||||
|
t[0x78] = 0x2502;
|
||||||
|
t[0x79] = 0x2264;
|
||||||
|
t[0x7a] = 0x2265;
|
||||||
|
t[0x7b] = 0x03C0;
|
||||||
|
t[0x7c] = 0x2260;
|
||||||
|
t[0x7d] = 0x00A3;
|
||||||
|
t[0x7e] = 0x00B7;
|
||||||
|
t
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_lengths() {
|
||||||
|
for set in [Charset::Ascii, Charset::British, Charset::DecSpecial] {
|
||||||
|
assert_eq!(table(set).len(), 256);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_contents() {
|
||||||
|
assert_eq!(table(Charset::British)[0x23], 0xa3);
|
||||||
|
assert_eq!(table(Charset::Ascii)[0x41], 0x41);
|
||||||
|
assert_eq!(table(Charset::DecSpecial)[0x71], 0x2500);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,1220 @@
|
|||||||
|
//! Terminal colors (ZIG/src/terminal/color.zig).
|
||||||
|
|
||||||
|
use crate::x11_color;
|
||||||
|
|
||||||
|
/// The default palette.
|
||||||
|
pub const DEFAULT: Palette = default_palette();
|
||||||
|
|
||||||
|
const fn default_palette() -> Palette {
|
||||||
|
let mut result = [Rgb { r: 0, g: 0, b: 0 }; 256];
|
||||||
|
|
||||||
|
// Named values
|
||||||
|
let mut i: usize = 0;
|
||||||
|
while i < 16 {
|
||||||
|
result[i] = match Name(i as u8).default() {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => unreachable!(),
|
||||||
|
};
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cube
|
||||||
|
assert!(i == 16);
|
||||||
|
let mut r: u8 = 0;
|
||||||
|
while r < 6 {
|
||||||
|
let mut g: u8 = 0;
|
||||||
|
while g < 6 {
|
||||||
|
let mut b: u8 = 0;
|
||||||
|
while b < 6 {
|
||||||
|
result[i] = Rgb {
|
||||||
|
r: if r == 0 { 0 } else { r * 40 + 55 },
|
||||||
|
g: if g == 0 { 0 } else { g * 40 + 55 },
|
||||||
|
b: if b == 0 { 0 } else { b * 40 + 55 },
|
||||||
|
};
|
||||||
|
i += 1;
|
||||||
|
b += 1;
|
||||||
|
}
|
||||||
|
g += 1;
|
||||||
|
}
|
||||||
|
r += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gray ramp
|
||||||
|
assert!(i == 232);
|
||||||
|
while i < 256 {
|
||||||
|
let value = ((i as u8 - 232) * 10) + 8;
|
||||||
|
result[i] = Rgb {
|
||||||
|
r: value,
|
||||||
|
g: value,
|
||||||
|
b: value,
|
||||||
|
};
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Palette is the 256 color palette.
|
||||||
|
pub type Palette = [Rgb; 256];
|
||||||
|
|
||||||
|
/// Mask that can be used to set which palette indexes were set.
|
||||||
|
/// (Zig `std.StaticBitSet(256)`.)
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub struct PaletteMask([u64; 4]);
|
||||||
|
|
||||||
|
impl PaletteMask {
|
||||||
|
pub const fn init_empty() -> Self {
|
||||||
|
Self([0; 4])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_set(&self, idx: usize) -> bool {
|
||||||
|
self.0[idx / 64] & (1 << (idx % 64)) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&mut self, idx: usize) {
|
||||||
|
self.0[idx / 64] |= 1 << (idx % 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unset(&mut self, idx: usize) {
|
||||||
|
self.0[idx / 64] &= !(1 << (idx % 64));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn count(&self) -> usize {
|
||||||
|
self.0.iter().map(|w| w.count_ones() as usize).sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over set indexes in ascending order.
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = usize> + '_ {
|
||||||
|
(0..256).filter(|&i| self.is_set(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate the 256-color palette from the user's base16 theme colors,
|
||||||
|
/// terminal background, and terminal foreground.
|
||||||
|
///
|
||||||
|
/// Motivation: The default 256-color palette uses fixed, fully-saturated
|
||||||
|
/// colors that clash with custom base16 themes, have poor readability in
|
||||||
|
/// dark shades (the first non-black shade jumps to 37% intensity instead
|
||||||
|
/// of the expected 20%), and exhibit inconsistent perceived brightness
|
||||||
|
/// across hues of the same shade (e.g., blue appears darker than green).
|
||||||
|
/// By generating the extended palette from the user's chosen colors,
|
||||||
|
/// programs can use the richer 256-color range without requiring their
|
||||||
|
/// own theme configuration, and light/dark switching works automatically.
|
||||||
|
///
|
||||||
|
/// The 216-color cube (indices 16–231) is built via trilinear
|
||||||
|
/// interpolation in CIELAB space over the 8 base colors. The base16
|
||||||
|
/// palette maps to the 8 corners of a 6×6×6 RGB cube as follows:
|
||||||
|
///
|
||||||
|
/// R=0 edge: bg → base[1] (red)
|
||||||
|
/// R=5 edge: base[6] → fg
|
||||||
|
/// G=0 edge: bg/base[6] (via R) → base[2]/base[4] (green/blue via R)
|
||||||
|
/// G=5 edge: base[1]/fg (via R) → base[3]/base[5] (yellow/magenta via R)
|
||||||
|
///
|
||||||
|
/// For each R slice, four corner colors (c0–c3) are interpolated along
|
||||||
|
/// the R axis, then for each G row two edge colors (c4–c5) are
|
||||||
|
/// interpolated along G, and finally each B cell is interpolated along B
|
||||||
|
/// to produce the final color. CIELAB interpolation ensures perceptually
|
||||||
|
/// uniform brightness transitions across different hues.
|
||||||
|
///
|
||||||
|
/// The 24-step grayscale ramp (indices 232–255) is a simple linear
|
||||||
|
/// interpolation in CIELAB from the background to the foreground,
|
||||||
|
/// excluding pure black and white (available in the cube at (0,0,0)
|
||||||
|
/// and (5,5,5)). The interpolation parameter runs from 1/25 to 24/25.
|
||||||
|
///
|
||||||
|
/// Fill `skip` with user-defined color indexes to avoid replacing them.
|
||||||
|
pub fn generate_256_color(
|
||||||
|
base: Palette,
|
||||||
|
skip: PaletteMask,
|
||||||
|
bg: Rgb,
|
||||||
|
fg: Rgb,
|
||||||
|
harmonious: bool,
|
||||||
|
) -> Palette {
|
||||||
|
// Convert the background, foreground, and 8 base theme colors into
|
||||||
|
// CIELAB space so that all interpolation is perceptually uniform.
|
||||||
|
let base8_lab: [Lab; 8] = {
|
||||||
|
let mut base8 = [
|
||||||
|
Lab::from_rgb(bg),
|
||||||
|
Lab::from_rgb(base[1]),
|
||||||
|
Lab::from_rgb(base[2]),
|
||||||
|
Lab::from_rgb(base[3]),
|
||||||
|
Lab::from_rgb(base[4]),
|
||||||
|
Lab::from_rgb(base[5]),
|
||||||
|
Lab::from_rgb(base[6]),
|
||||||
|
Lab::from_rgb(fg),
|
||||||
|
];
|
||||||
|
|
||||||
|
// For light themes (where the foreground is darker than the
|
||||||
|
// background), the cube's dark-to-light orientation is inverted
|
||||||
|
// relative to the base color mapping. When `harmonious` is false,
|
||||||
|
// swap bg and fg so the cube still runs from black (16) to
|
||||||
|
// white (231).
|
||||||
|
let is_light_theme = base8[7].l < base8[0].l;
|
||||||
|
let invert = is_light_theme && !harmonious;
|
||||||
|
if invert {
|
||||||
|
base8.swap(0, 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
base8
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start from the base palette so indices 0-15 are preserved as-is.
|
||||||
|
let mut result = base;
|
||||||
|
|
||||||
|
// Build the 216-color cube (indices 16-231) via trilinear interpolation
|
||||||
|
// in CIELAB. The three nested loops correspond to the R, G, and B axes
|
||||||
|
// of a 6x6x6 cube.
|
||||||
|
let mut idx: usize = 16;
|
||||||
|
for ri in 0..6 {
|
||||||
|
// R-axis corners: blend base colors along the red dimension.
|
||||||
|
let tr = ri as f32 / 5.0;
|
||||||
|
let c0 = Lab::lerp(tr, base8_lab[0], base8_lab[1]);
|
||||||
|
let c1 = Lab::lerp(tr, base8_lab[2], base8_lab[3]);
|
||||||
|
let c2 = Lab::lerp(tr, base8_lab[4], base8_lab[5]);
|
||||||
|
let c3 = Lab::lerp(tr, base8_lab[6], base8_lab[7]);
|
||||||
|
for gi in 0..6 {
|
||||||
|
// G-axis edges: blend the R-interpolated corners along green.
|
||||||
|
let tg = gi as f32 / 5.0;
|
||||||
|
let c4 = Lab::lerp(tg, c0, c1);
|
||||||
|
let c5 = Lab::lerp(tg, c2, c3);
|
||||||
|
for bi in 0..6 {
|
||||||
|
// B-axis: final interpolation along blue, then convert back to RGB.
|
||||||
|
if !skip.is_set(idx) {
|
||||||
|
let c6 = Lab::lerp(bi as f32 / 5.0, c4, c5);
|
||||||
|
result[idx] = c6.to_rgb();
|
||||||
|
}
|
||||||
|
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the 24-step grayscale ramp (indices 232-255) by linearly
|
||||||
|
// interpolating in CIELAB from background to foreground. The parameter
|
||||||
|
// runs from 1/25 to 24/25, excluding the endpoints which are already
|
||||||
|
// available in the cube at (0,0,0) and (5,5,5).
|
||||||
|
for i in 0..24 {
|
||||||
|
let t = (i + 1) as f32 / 25.0;
|
||||||
|
if !skip.is_set(idx) {
|
||||||
|
let c = Lab::lerp(t, base8_lab[0], base8_lab[7]);
|
||||||
|
result[idx] = c.to_rgb();
|
||||||
|
}
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A palette that can have its colors changed and reset. Purposely built
|
||||||
|
/// for terminal color operations.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct DynamicPalette {
|
||||||
|
/// The current palette including any user modifications.
|
||||||
|
pub current: Palette,
|
||||||
|
|
||||||
|
/// The original/default palette values.
|
||||||
|
pub original: Palette,
|
||||||
|
|
||||||
|
/// A bitset where each bit represents whether the corresponding
|
||||||
|
/// palette index has been modified from its default value.
|
||||||
|
pub mask: PaletteMask,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DynamicPalette {
|
||||||
|
pub const DEFAULT: DynamicPalette = DynamicPalette::init(DEFAULT);
|
||||||
|
|
||||||
|
/// Initialize a dynamic palette with a default palette.
|
||||||
|
pub const fn init(def: Palette) -> DynamicPalette {
|
||||||
|
DynamicPalette {
|
||||||
|
current: def,
|
||||||
|
original: def,
|
||||||
|
mask: PaletteMask::init_empty(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a custom color at the given palette index.
|
||||||
|
pub fn set(&mut self, idx: u8, color: Rgb) {
|
||||||
|
self.current[idx as usize] = color;
|
||||||
|
self.mask.set(idx as usize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset the color at the given palette index to its original value.
|
||||||
|
pub fn reset(&mut self, idx: u8) {
|
||||||
|
self.current[idx as usize] = self.original[idx as usize];
|
||||||
|
self.mask.unset(idx as usize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset all colors to their original values.
|
||||||
|
pub fn reset_all(&mut self) {
|
||||||
|
*self = Self::init(self.original);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the default palette, but preserve the changed values.
|
||||||
|
pub fn change_default(&mut self, def: Palette) {
|
||||||
|
self.original = def;
|
||||||
|
|
||||||
|
// Fast path, the palette is usually not changed.
|
||||||
|
if self.mask.count() == 0 {
|
||||||
|
self.current = self.original;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are usually less set than unset, so iterate over the changed
|
||||||
|
// values and override them.
|
||||||
|
let mut current = def;
|
||||||
|
for idx in self.mask.iter() {
|
||||||
|
current[idx] = self.current[idx];
|
||||||
|
}
|
||||||
|
self.current = current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RGB value that can be changed and reset. This can also be totally unset
|
||||||
|
/// in every way, in which case the caller can determine their own ultimate
|
||||||
|
/// default.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub struct DynamicRgb {
|
||||||
|
pub override_: Option<Rgb>,
|
||||||
|
pub default: Option<Rgb>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DynamicRgb {
|
||||||
|
pub const UNSET: DynamicRgb = DynamicRgb {
|
||||||
|
override_: None,
|
||||||
|
default: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(def: Rgb) -> DynamicRgb {
|
||||||
|
DynamicRgb {
|
||||||
|
override_: None,
|
||||||
|
default: Some(def),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self) -> Option<Rgb> {
|
||||||
|
self.override_.or(self.default)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&mut self, color: Rgb) {
|
||||||
|
self.override_ = Some(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.override_ = self.default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Color names in the standard 8 or 16 color palette. Remainders (any other
|
||||||
|
/// `u8`) are valid unnamed values in the 256 color palette, so this is a
|
||||||
|
/// newtype rather than an enum.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct Name(pub u8);
|
||||||
|
|
||||||
|
/// Error returned by [`Name::default`] for unnamed palette values.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct NoDefaultValue;
|
||||||
|
|
||||||
|
impl Name {
|
||||||
|
pub const BLACK: Name = Name(0);
|
||||||
|
pub const RED: Name = Name(1);
|
||||||
|
pub const GREEN: Name = Name(2);
|
||||||
|
pub const YELLOW: Name = Name(3);
|
||||||
|
pub const BLUE: Name = Name(4);
|
||||||
|
pub const MAGENTA: Name = Name(5);
|
||||||
|
pub const CYAN: Name = Name(6);
|
||||||
|
pub const WHITE: Name = Name(7);
|
||||||
|
|
||||||
|
pub const BRIGHT_BLACK: Name = Name(8);
|
||||||
|
pub const BRIGHT_RED: Name = Name(9);
|
||||||
|
pub const BRIGHT_GREEN: Name = Name(10);
|
||||||
|
pub const BRIGHT_YELLOW: Name = Name(11);
|
||||||
|
pub const BRIGHT_BLUE: Name = Name(12);
|
||||||
|
pub const BRIGHT_MAGENTA: Name = Name(13);
|
||||||
|
pub const BRIGHT_CYAN: Name = Name(14);
|
||||||
|
pub const BRIGHT_WHITE: Name = Name(15);
|
||||||
|
|
||||||
|
/// Default colors for tagged values.
|
||||||
|
pub const fn default(self) -> Result<Rgb, NoDefaultValue> {
|
||||||
|
let (r, g, b) = match self.0 {
|
||||||
|
0 => (0x1D, 0x1F, 0x21),
|
||||||
|
1 => (0xCC, 0x66, 0x66),
|
||||||
|
2 => (0xB5, 0xBD, 0x68),
|
||||||
|
3 => (0xF0, 0xC6, 0x74),
|
||||||
|
4 => (0x81, 0xA2, 0xBE),
|
||||||
|
5 => (0xB2, 0x94, 0xBB),
|
||||||
|
6 => (0x8A, 0xBE, 0xB7),
|
||||||
|
7 => (0xC5, 0xC8, 0xC6),
|
||||||
|
|
||||||
|
8 => (0x66, 0x66, 0x66),
|
||||||
|
9 => (0xD5, 0x4E, 0x53),
|
||||||
|
10 => (0xB9, 0xCA, 0x4A),
|
||||||
|
11 => (0xE7, 0xC5, 0x47),
|
||||||
|
12 => (0x7A, 0xA6, 0xDA),
|
||||||
|
13 => (0xC3, 0x97, 0xD8),
|
||||||
|
14 => (0x70, 0xC0, 0xB1),
|
||||||
|
15 => (0xEA, 0xEA, 0xEA),
|
||||||
|
|
||||||
|
_ => return Err(NoDefaultValue),
|
||||||
|
};
|
||||||
|
Ok(Rgb { r, g, b })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The "special colors" as denoted by xterm. These can be set via
|
||||||
|
/// OSC 5 or via OSC 4 by adding the palette length to it.
|
||||||
|
///
|
||||||
|
/// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum Special {
|
||||||
|
Bold = 0,
|
||||||
|
Underline = 1,
|
||||||
|
Blink = 2,
|
||||||
|
Reverse = 3,
|
||||||
|
Italic = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Special {
|
||||||
|
pub fn osc4(self) -> u16 {
|
||||||
|
// "The special colors can also be set by adding the maximum
|
||||||
|
// number of colors (e.g., 88 or 256) to these codes in an
|
||||||
|
// OSC 4 control" - xterm ctlseqs
|
||||||
|
self as u16 + 256
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert from the raw value (Zig `@enumFromInt` / `intToEnum`).
|
||||||
|
pub fn from_u8(v: u8) -> Option<Special> {
|
||||||
|
Some(match v {
|
||||||
|
0 => Special::Bold,
|
||||||
|
1 => Special::Underline,
|
||||||
|
2 => Special::Blink,
|
||||||
|
3 => Special::Reverse,
|
||||||
|
4 => Special::Italic,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The "dynamic colors" as denoted by xterm. These can be set via
|
||||||
|
/// OSC 10 through 19.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum Dynamic {
|
||||||
|
Foreground = 10,
|
||||||
|
Background = 11,
|
||||||
|
Cursor = 12,
|
||||||
|
PointerForeground = 13,
|
||||||
|
PointerBackground = 14,
|
||||||
|
TektronixForeground = 15,
|
||||||
|
TektronixBackground = 16,
|
||||||
|
HighlightBackground = 17,
|
||||||
|
TektronixCursor = 18,
|
||||||
|
HighlightForeground = 19,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dynamic {
|
||||||
|
/// Convert from the raw value (10..=19).
|
||||||
|
pub fn from_u8(v: u8) -> Option<Dynamic> {
|
||||||
|
Some(match v {
|
||||||
|
10 => Dynamic::Foreground,
|
||||||
|
11 => Dynamic::Background,
|
||||||
|
12 => Dynamic::Cursor,
|
||||||
|
13 => Dynamic::PointerForeground,
|
||||||
|
14 => Dynamic::PointerBackground,
|
||||||
|
15 => Dynamic::TektronixForeground,
|
||||||
|
16 => Dynamic::TektronixBackground,
|
||||||
|
17 => Dynamic::HighlightBackground,
|
||||||
|
18 => Dynamic::TektronixCursor,
|
||||||
|
19 => Dynamic::HighlightForeground,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The next dynamic color sequentially. This is required because
|
||||||
|
/// specifying colors sequentially without their index will automatically
|
||||||
|
/// use the next dynamic color.
|
||||||
|
///
|
||||||
|
/// "Each successive parameter changes the next color in the list. The
|
||||||
|
/// value of Ps tells the starting point in the list."
|
||||||
|
pub fn next(self) -> Option<Dynamic> {
|
||||||
|
Dynamic::from_u8(self as u8 + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error returned when parsing a color specification fails.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct InvalidFormat;
|
||||||
|
|
||||||
|
impl std::fmt::Display for InvalidFormat {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("invalid color format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for InvalidFormat {}
|
||||||
|
|
||||||
|
/// RGB
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||||
|
pub struct Rgb {
|
||||||
|
pub r: u8,
|
||||||
|
pub g: u8,
|
||||||
|
pub b: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alias matching the Zig name.
|
||||||
|
pub type RGB = Rgb;
|
||||||
|
|
||||||
|
/// Zig `std.fmt.parseUnsigned(u16, value, 16)` semantics (optional leading
|
||||||
|
/// `+`; a leading `-` is only valid if the value is zero; no underscores).
|
||||||
|
fn parse_unsigned_hex_u16(value: &[u8]) -> Option<u16> {
|
||||||
|
let (neg, digits) = match value.first()? {
|
||||||
|
b'+' => (false, &value[1..]),
|
||||||
|
b'-' => (true, &value[1..]),
|
||||||
|
_ => (false, value),
|
||||||
|
};
|
||||||
|
if digits.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut acc: u32 = 0;
|
||||||
|
for &c in digits {
|
||||||
|
let d = (c as char).to_digit(16)?;
|
||||||
|
acc = acc * 16 + d;
|
||||||
|
if acc > u16::MAX as u32 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if neg && acc != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(acc as u16)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rgb {
|
||||||
|
pub fn eql(self, other: Rgb) -> bool {
|
||||||
|
self.r == other.r && self.g == other.g && self.b == other.b
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the contrast ratio between two colors. The contrast
|
||||||
|
/// ration is a value between 1 and 21 where 1 is the lowest contrast
|
||||||
|
/// and 21 is the highest contrast.
|
||||||
|
///
|
||||||
|
/// https://www.w3.org/TR/WCAG20/#contrast-ratiodef
|
||||||
|
pub fn contrast(self, other: Rgb) -> f64 {
|
||||||
|
// pair[0] = lighter, pair[1] = darker
|
||||||
|
let self_lum = self.luminance();
|
||||||
|
let other_lum = other.luminance();
|
||||||
|
let pair = if self_lum > other_lum {
|
||||||
|
[self_lum, other_lum]
|
||||||
|
} else {
|
||||||
|
[other_lum, self_lum]
|
||||||
|
};
|
||||||
|
|
||||||
|
(pair[0] + 0.05) / (pair[1] + 0.05)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates luminance based on the W3C formula. This returns a
|
||||||
|
/// normalized value between 0 and 1 where 0 is black and 1 is white.
|
||||||
|
///
|
||||||
|
/// https://www.w3.org/TR/WCAG20/#relativeluminancedef
|
||||||
|
pub fn luminance(self) -> f64 {
|
||||||
|
let r_lum = component_luminance(self.r);
|
||||||
|
let g_lum = component_luminance(self.g);
|
||||||
|
let b_lum = component_luminance(self.b);
|
||||||
|
0.2126 * r_lum + 0.7152 * g_lum + 0.0722 * b_lum
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates "perceived luminance" which is better for determining
|
||||||
|
/// light vs dark.
|
||||||
|
///
|
||||||
|
/// Source: https://www.w3.org/TR/AERT/#color-contrast
|
||||||
|
pub fn perceived_luminance(self) -> f64 {
|
||||||
|
let r = self.r as f64;
|
||||||
|
let g = self.g as f64;
|
||||||
|
let b = self.b as f64;
|
||||||
|
0.299 * (r / 255.0) + 0.587 * (g / 255.0) + 0.114 * (b / 255.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a color from a floating point intensity value.
|
||||||
|
///
|
||||||
|
/// The value should be between 0.0 and 1.0, inclusive.
|
||||||
|
fn from_intensity(value: &[u8]) -> Result<u8, InvalidFormat> {
|
||||||
|
let s = std::str::from_utf8(value).map_err(|_| InvalidFormat)?;
|
||||||
|
let i: f64 = s.parse().map_err(|_| InvalidFormat)?;
|
||||||
|
if !(0.0..=1.0).contains(&i) && !i.is_nan() {
|
||||||
|
return Err(InvalidFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((i * 255.0) as u8)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a color from a string of hexadecimal digits
|
||||||
|
///
|
||||||
|
/// The string can contain 1, 2, 3, or 4 characters and represents the color
|
||||||
|
/// value scaled in 4, 8, 12, or 16 bits, respectively.
|
||||||
|
fn from_hex(value: &[u8]) -> Result<u8, InvalidFormat> {
|
||||||
|
if value.is_empty() || value.len() > 4 {
|
||||||
|
return Err(InvalidFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
let color = parse_unsigned_hex_u16(value).ok_or(InvalidFormat)?;
|
||||||
|
|
||||||
|
let divisor: usize = match value.len() {
|
||||||
|
1 => 0xf,
|
||||||
|
2 => 0xff,
|
||||||
|
3 => 0xfff,
|
||||||
|
4 => 0xffff,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((color as usize * 255 / divisor) as u8)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a color specification.
|
||||||
|
///
|
||||||
|
/// Any of the following forms are accepted:
|
||||||
|
///
|
||||||
|
/// 1. rgb:<red>/<green>/<blue>
|
||||||
|
///
|
||||||
|
/// <red>, <green>, <blue> := h | hh | hhh | hhhh
|
||||||
|
///
|
||||||
|
/// where `h` is a single hexadecimal digit.
|
||||||
|
///
|
||||||
|
/// 2. rgbi:<red>/<green>/<blue>
|
||||||
|
///
|
||||||
|
/// where <red>, <green>, and <blue> are floating point values between
|
||||||
|
/// 0.0 and 1.0 (inclusive).
|
||||||
|
///
|
||||||
|
/// 3. #rgb, #rrggbb, #rrrgggbbb #rrrrggggbbbb
|
||||||
|
///
|
||||||
|
/// where `r`, `g`, and `b` are a single hexadecimal digit.
|
||||||
|
/// These specify a color with 4, 8, 12, and 16 bits of precision
|
||||||
|
/// per color channel.
|
||||||
|
pub fn parse(value: &str) -> Result<Rgb, InvalidFormat> {
|
||||||
|
Self::parse_bytes(value.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Byte-slice variant of [`Rgb::parse`].
|
||||||
|
pub fn parse_bytes(value: &[u8]) -> Result<Rgb, InvalidFormat> {
|
||||||
|
if value.is_empty() {
|
||||||
|
return Err(InvalidFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
if value[0] == b'#' {
|
||||||
|
return match value.len() {
|
||||||
|
4 => Ok(Rgb {
|
||||||
|
r: Rgb::from_hex(&value[1..2])?,
|
||||||
|
g: Rgb::from_hex(&value[2..3])?,
|
||||||
|
b: Rgb::from_hex(&value[3..4])?,
|
||||||
|
}),
|
||||||
|
7 => Ok(Rgb {
|
||||||
|
r: Rgb::from_hex(&value[1..3])?,
|
||||||
|
g: Rgb::from_hex(&value[3..5])?,
|
||||||
|
b: Rgb::from_hex(&value[5..7])?,
|
||||||
|
}),
|
||||||
|
10 => Ok(Rgb {
|
||||||
|
r: Rgb::from_hex(&value[1..4])?,
|
||||||
|
g: Rgb::from_hex(&value[4..7])?,
|
||||||
|
b: Rgb::from_hex(&value[7..10])?,
|
||||||
|
}),
|
||||||
|
13 => Ok(Rgb {
|
||||||
|
r: Rgb::from_hex(&value[1..5])?,
|
||||||
|
g: Rgb::from_hex(&value[5..9])?,
|
||||||
|
b: Rgb::from_hex(&value[9..13])?,
|
||||||
|
}),
|
||||||
|
_ => Err(InvalidFormat),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for X11 named colors. We allow whitespace around the edges
|
||||||
|
// of the color because Kitty allows whitespace. This is not part of
|
||||||
|
// any spec I could find.
|
||||||
|
let trimmed = {
|
||||||
|
let mut s = value;
|
||||||
|
while let [b' ', rest @ ..] = s {
|
||||||
|
s = rest;
|
||||||
|
}
|
||||||
|
while let [rest @ .., b' '] = s {
|
||||||
|
s = rest;
|
||||||
|
}
|
||||||
|
s
|
||||||
|
};
|
||||||
|
if let Some(rgb) = x11_color::get_bytes(trimmed) {
|
||||||
|
return Ok(rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
if value.len() < "rgb:a/a/a".len() || &value[0..3] != b"rgb" {
|
||||||
|
return Err(InvalidFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut i: usize = 3;
|
||||||
|
|
||||||
|
let use_intensity = if value[i] == b'i' {
|
||||||
|
i += 1;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if value[i] != b':' {
|
||||||
|
return Err(InvalidFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
|
||||||
|
let component = |slice: &[u8]| {
|
||||||
|
if use_intensity {
|
||||||
|
Rgb::from_intensity(slice)
|
||||||
|
} else {
|
||||||
|
Rgb::from_hex(slice)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let r = {
|
||||||
|
let end = value[i..]
|
||||||
|
.iter()
|
||||||
|
.position(|&c| c == b'/')
|
||||||
|
.map(|p| p + i)
|
||||||
|
.ok_or(InvalidFormat)?;
|
||||||
|
let slice = &value[i..end];
|
||||||
|
i += slice.len() + 1;
|
||||||
|
component(slice)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let g = {
|
||||||
|
let end = value[i..]
|
||||||
|
.iter()
|
||||||
|
.position(|&c| c == b'/')
|
||||||
|
.map(|p| p + i)
|
||||||
|
.ok_or(InvalidFormat)?;
|
||||||
|
let slice = &value[i..end];
|
||||||
|
i += slice.len() + 1;
|
||||||
|
component(slice)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let b = component(&value[i..])?;
|
||||||
|
|
||||||
|
Ok(Rgb { r, g, b })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates single-component luminance based on the W3C formula.
|
||||||
|
///
|
||||||
|
/// Expects sRGB color space which at the time of writing we don't
|
||||||
|
/// generally use but it's a good enough approximation until we fix that.
|
||||||
|
/// https://www.w3.org/TR/WCAG20/#relativeluminancedef
|
||||||
|
fn component_luminance(c: u8) -> f64 {
|
||||||
|
let normalized = c as f64 / 255.0;
|
||||||
|
if normalized <= 0.03928 {
|
||||||
|
return normalized / 12.92;
|
||||||
|
}
|
||||||
|
((normalized + 0.055) / 1.055).powf(2.4)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// LAB color space
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
struct Lab {
|
||||||
|
l: f32,
|
||||||
|
a: f32,
|
||||||
|
b: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::excessive_precision)]
|
||||||
|
impl Lab {
|
||||||
|
/// RGB to LAB
|
||||||
|
fn from_rgb(rgb: Rgb) -> Lab {
|
||||||
|
// Step 1: Normalize sRGB channels from [0, 255] to [0.0, 1.0].
|
||||||
|
let mut r = rgb.r as f32 / 255.0;
|
||||||
|
let mut g = rgb.g as f32 / 255.0;
|
||||||
|
let mut b = rgb.b as f32 / 255.0;
|
||||||
|
|
||||||
|
// Step 2: Apply the inverse sRGB companding (gamma correction) to
|
||||||
|
// convert from sRGB to linear RGB.
|
||||||
|
r = if r > 0.04045 {
|
||||||
|
((r + 0.055) / 1.055).powf(2.4)
|
||||||
|
} else {
|
||||||
|
r / 12.92
|
||||||
|
};
|
||||||
|
g = if g > 0.04045 {
|
||||||
|
((g + 0.055) / 1.055).powf(2.4)
|
||||||
|
} else {
|
||||||
|
g / 12.92
|
||||||
|
};
|
||||||
|
b = if b > 0.04045 {
|
||||||
|
((b + 0.055) / 1.055).powf(2.4)
|
||||||
|
} else {
|
||||||
|
b / 12.92
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 3: Convert linear RGB to CIE XYZ using the sRGB to XYZ
|
||||||
|
// transformation matrix (D65 illuminant). The X and Z values are
|
||||||
|
// normalized by the D65 white point reference values (Xn=0.95047,
|
||||||
|
// Zn=1.08883; Yn=1.0 is implicit).
|
||||||
|
let mut x = (r * 0.4124564 + g * 0.3575761 + b * 0.1804375) / 0.95047;
|
||||||
|
let mut y = r * 0.2126729 + g * 0.7151522 + b * 0.0721750;
|
||||||
|
let mut z = (r * 0.0193339 + g * 0.1191920 + b * 0.9503041) / 1.08883;
|
||||||
|
|
||||||
|
// Step 4: Apply the CIE f(t) nonlinear transform to each XYZ
|
||||||
|
// component.
|
||||||
|
x = if x > 0.008856 {
|
||||||
|
x.cbrt()
|
||||||
|
} else {
|
||||||
|
7.787 * x + 16.0 / 116.0
|
||||||
|
};
|
||||||
|
y = if y > 0.008856 {
|
||||||
|
y.cbrt()
|
||||||
|
} else {
|
||||||
|
7.787 * y + 16.0 / 116.0
|
||||||
|
};
|
||||||
|
z = if z > 0.008856 {
|
||||||
|
z.cbrt()
|
||||||
|
} else {
|
||||||
|
7.787 * z + 16.0 / 116.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 5: Compute the final CIELAB values from the transformed XYZ.
|
||||||
|
Lab {
|
||||||
|
l: 116.0 * y - 16.0,
|
||||||
|
a: 500.0 * (x - y),
|
||||||
|
b: 200.0 * (y - z),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// LAB to RGB
|
||||||
|
fn to_rgb(self) -> Rgb {
|
||||||
|
// Step 1: Recover the intermediate f(Y), f(X), f(Z) values from
|
||||||
|
// L*a*b* by inverting the CIELAB formulas.
|
||||||
|
let y = (self.l + 16.0) / 116.0;
|
||||||
|
let x = self.a / 500.0 + y;
|
||||||
|
let z = y - self.b / 200.0;
|
||||||
|
|
||||||
|
// Step 2: Apply the inverse CIE f(t) transform to get back to XYZ.
|
||||||
|
let x3 = x * x * x;
|
||||||
|
let y3 = y * y * y;
|
||||||
|
let z3 = z * z * z;
|
||||||
|
let xf = (if x3 > 0.008856 {
|
||||||
|
x3
|
||||||
|
} else {
|
||||||
|
(x - 16.0 / 116.0) / 7.787
|
||||||
|
}) * 0.95047;
|
||||||
|
let yf = if y3 > 0.008856 {
|
||||||
|
y3
|
||||||
|
} else {
|
||||||
|
(y - 16.0 / 116.0) / 7.787
|
||||||
|
};
|
||||||
|
let zf = (if z3 > 0.008856 {
|
||||||
|
z3
|
||||||
|
} else {
|
||||||
|
(z - 16.0 / 116.0) / 7.787
|
||||||
|
}) * 1.08883;
|
||||||
|
|
||||||
|
// Step 3: Convert CIE XYZ back to linear RGB.
|
||||||
|
let mut r = xf * 3.2404542 - yf * 1.5371385 - zf * 0.4985314;
|
||||||
|
let mut g = -xf * 0.9692660 + yf * 1.8760108 + zf * 0.0415560;
|
||||||
|
let mut b = xf * 0.0556434 - yf * 0.2040259 + zf * 1.0572252;
|
||||||
|
|
||||||
|
// Step 4: Apply sRGB companding (gamma correction).
|
||||||
|
r = if r > 0.0031308 {
|
||||||
|
1.055 * r.powf(1.0 / 2.4) - 0.055
|
||||||
|
} else {
|
||||||
|
12.92 * r
|
||||||
|
};
|
||||||
|
g = if g > 0.0031308 {
|
||||||
|
1.055 * g.powf(1.0 / 2.4) - 0.055
|
||||||
|
} else {
|
||||||
|
12.92 * g
|
||||||
|
};
|
||||||
|
b = if b > 0.0031308 {
|
||||||
|
1.055 * b.powf(1.0 / 2.4) - 0.055
|
||||||
|
} else {
|
||||||
|
12.92 * b
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 5: Clamp to [0.0, 1.0], scale to [0, 255], and round to
|
||||||
|
// the nearest integer to produce the final 8-bit sRGB values.
|
||||||
|
Rgb {
|
||||||
|
r: (r.clamp(0.0, 1.0) * 255.0 + 0.5) as u8,
|
||||||
|
g: (g.clamp(0.0, 1.0) * 255.0 + 0.5) as u8,
|
||||||
|
b: (b.clamp(0.0, 1.0) * 255.0 + 0.5) as u8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Linearly interpolate between two LAB colors component-wise.
|
||||||
|
/// `t` is the interpolation factor in [0, 1]: t=0 returns `a`,
|
||||||
|
/// t=1 returns `b`, and values in between blend proportionally.
|
||||||
|
fn lerp(t: f32, a: Lab, b: Lab) -> Lab {
|
||||||
|
Lab {
|
||||||
|
l: a.l + t * (b.l - a.l),
|
||||||
|
a: a.a + t * (b.a - a.a),
|
||||||
|
b: a.b + t * (b.b - a.b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn rgb(r: u8, g: u8, b: u8) -> Rgb {
|
||||||
|
Rgb { r, g, b }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn special_osc4() {
|
||||||
|
assert_eq!(256, Special::Bold.osc4());
|
||||||
|
assert_eq!(257, Special::Underline.osc4());
|
||||||
|
assert_eq!(258, Special::Blink.osc4());
|
||||||
|
assert_eq!(259, Special::Reverse.osc4());
|
||||||
|
assert_eq!(260, Special::Italic.osc4());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dynamic_next() {
|
||||||
|
use Dynamic::*;
|
||||||
|
assert_eq!(Some(Background), Foreground.next());
|
||||||
|
assert_eq!(Some(Cursor), Background.next());
|
||||||
|
assert_eq!(Some(PointerForeground), Cursor.next());
|
||||||
|
assert_eq!(Some(PointerBackground), PointerForeground.next());
|
||||||
|
assert_eq!(Some(TektronixForeground), PointerBackground.next());
|
||||||
|
assert_eq!(Some(TektronixBackground), TektronixForeground.next());
|
||||||
|
assert_eq!(Some(HighlightBackground), TektronixBackground.next());
|
||||||
|
assert_eq!(Some(TektronixCursor), HighlightBackground.next());
|
||||||
|
assert_eq!(Some(HighlightForeground), TektronixCursor.next());
|
||||||
|
assert_eq!(None, HighlightForeground.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn palette_default() {
|
||||||
|
for i in 0..16u8 {
|
||||||
|
assert_eq!(Name(i).default().unwrap(), DEFAULT[i as usize]);
|
||||||
|
}
|
||||||
|
// Spot checks of the cube and ramp.
|
||||||
|
assert_eq!(DEFAULT[16], rgb(0, 0, 0));
|
||||||
|
assert_eq!(DEFAULT[17], rgb(0, 0, 95));
|
||||||
|
assert_eq!(DEFAULT[231], rgb(255, 255, 255));
|
||||||
|
assert_eq!(DEFAULT[232], rgb(8, 8, 8));
|
||||||
|
assert_eq!(DEFAULT[255], rgb(238, 238, 238));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rgb_parse() {
|
||||||
|
assert_eq!(rgb(255, 0, 0), Rgb::parse("rgbi:1.0/0/0").unwrap());
|
||||||
|
assert_eq!(rgb(127, 160, 0), Rgb::parse("rgb:7f/a0a0/0").unwrap());
|
||||||
|
assert_eq!(rgb(255, 255, 255), Rgb::parse("rgb:f/ff/fff").unwrap());
|
||||||
|
assert_eq!(rgb(255, 255, 255), Rgb::parse("#ffffff").unwrap());
|
||||||
|
assert_eq!(rgb(255, 255, 255), Rgb::parse("#fff").unwrap());
|
||||||
|
assert_eq!(rgb(255, 255, 255), Rgb::parse("#fffffffff").unwrap());
|
||||||
|
assert_eq!(rgb(255, 255, 255), Rgb::parse("#ffffffffffff").unwrap());
|
||||||
|
assert_eq!(rgb(255, 0, 16), Rgb::parse("#ff0010").unwrap());
|
||||||
|
|
||||||
|
assert_eq!(rgb(0, 0, 0), Rgb::parse("black").unwrap());
|
||||||
|
assert_eq!(rgb(255, 0, 0), Rgb::parse("red").unwrap());
|
||||||
|
assert_eq!(rgb(0, 255, 0), Rgb::parse("green").unwrap());
|
||||||
|
assert_eq!(rgb(0, 0, 255), Rgb::parse("blue").unwrap());
|
||||||
|
assert_eq!(rgb(255, 255, 255), Rgb::parse("white").unwrap());
|
||||||
|
|
||||||
|
assert_eq!(rgb(124, 252, 0), Rgb::parse("LawnGreen").unwrap());
|
||||||
|
assert_eq!(rgb(0, 250, 154), Rgb::parse("medium spring green").unwrap());
|
||||||
|
assert_eq!(rgb(34, 139, 34), Rgb::parse(" Forest Green ").unwrap());
|
||||||
|
|
||||||
|
// Invalid format
|
||||||
|
for bad in [
|
||||||
|
"rgb;",
|
||||||
|
"rgb:",
|
||||||
|
":a/a/a",
|
||||||
|
"a/a/a",
|
||||||
|
"rgb:a/a/a/",
|
||||||
|
"rgb:00000///",
|
||||||
|
"rgb:000/",
|
||||||
|
"rgbi:a/a/a",
|
||||||
|
"rgb:0.5/0.0/1.0",
|
||||||
|
"rgb:not/hex/zz",
|
||||||
|
"#",
|
||||||
|
"#ff",
|
||||||
|
"#ffff",
|
||||||
|
"#fffff",
|
||||||
|
"#gggggg",
|
||||||
|
] {
|
||||||
|
assert_eq!(Err(InvalidFormat), Rgb::parse(bad), "{bad}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dynamic_palette_init() {
|
||||||
|
let p = DynamicPalette::init(DEFAULT);
|
||||||
|
assert_eq!(DEFAULT, p.current);
|
||||||
|
assert_eq!(DEFAULT, p.original);
|
||||||
|
assert_eq!(0, p.mask.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dynamic_palette_set() {
|
||||||
|
let mut p = DynamicPalette::init(DEFAULT);
|
||||||
|
let new_color = rgb(255, 0, 0);
|
||||||
|
|
||||||
|
p.set(0, new_color);
|
||||||
|
assert_eq!(new_color, p.current[0]);
|
||||||
|
assert!(p.mask.is_set(0));
|
||||||
|
assert_eq!(1, p.mask.count());
|
||||||
|
|
||||||
|
assert_eq!(DEFAULT[0], p.original[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dynamic_palette_reset() {
|
||||||
|
let mut p = DynamicPalette::init(DEFAULT);
|
||||||
|
p.set(0, rgb(255, 0, 0));
|
||||||
|
assert!(p.mask.is_set(0));
|
||||||
|
|
||||||
|
p.reset(0);
|
||||||
|
assert_eq!(DEFAULT[0], p.current[0]);
|
||||||
|
assert!(!p.mask.is_set(0));
|
||||||
|
assert_eq!(0, p.mask.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dynamic_palette_reset_all() {
|
||||||
|
let mut p = DynamicPalette::init(DEFAULT);
|
||||||
|
let c = rgb(255, 0, 0);
|
||||||
|
p.set(0, c);
|
||||||
|
p.set(5, c);
|
||||||
|
p.set(10, c);
|
||||||
|
assert_eq!(3, p.mask.count());
|
||||||
|
|
||||||
|
p.reset_all();
|
||||||
|
assert_eq!(DEFAULT, p.current);
|
||||||
|
assert_eq!(DEFAULT, p.original);
|
||||||
|
assert_eq!(0, p.mask.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dynamic_palette_change_default_with_no_changes() {
|
||||||
|
let mut p = DynamicPalette::init(DEFAULT);
|
||||||
|
let mut new_palette = DEFAULT;
|
||||||
|
new_palette[0] = rgb(100, 100, 100);
|
||||||
|
|
||||||
|
p.change_default(new_palette);
|
||||||
|
assert_eq!(new_palette, p.original);
|
||||||
|
assert_eq!(new_palette, p.current);
|
||||||
|
assert_eq!(0, p.mask.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dynamic_palette_change_default_preserves_changes() {
|
||||||
|
let mut p = DynamicPalette::init(DEFAULT);
|
||||||
|
let custom = rgb(255, 0, 0);
|
||||||
|
|
||||||
|
p.set(5, custom);
|
||||||
|
assert!(p.mask.is_set(5));
|
||||||
|
|
||||||
|
let mut new_palette = DEFAULT;
|
||||||
|
new_palette[0] = rgb(100, 100, 100);
|
||||||
|
new_palette[5] = rgb(50, 50, 50);
|
||||||
|
|
||||||
|
p.change_default(new_palette);
|
||||||
|
|
||||||
|
assert_eq!(new_palette, p.original);
|
||||||
|
assert_eq!(new_palette[0], p.current[0]);
|
||||||
|
assert_eq!(custom, p.current[5]);
|
||||||
|
assert!(p.mask.is_set(5));
|
||||||
|
assert_eq!(1, p.mask.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dynamic_palette_change_default_with_multiple_changes() {
|
||||||
|
let mut p = DynamicPalette::init(DEFAULT);
|
||||||
|
let red = rgb(255, 0, 0);
|
||||||
|
let green = rgb(0, 255, 0);
|
||||||
|
let blue = rgb(0, 0, 255);
|
||||||
|
|
||||||
|
p.set(1, red);
|
||||||
|
p.set(2, green);
|
||||||
|
p.set(3, blue);
|
||||||
|
|
||||||
|
let mut new_palette = DEFAULT;
|
||||||
|
new_palette[0] = rgb(50, 50, 50);
|
||||||
|
new_palette[1] = rgb(60, 60, 60);
|
||||||
|
|
||||||
|
p.change_default(new_palette);
|
||||||
|
|
||||||
|
assert_eq!(new_palette[0], p.current[0]);
|
||||||
|
assert_eq!(red, p.current[1]);
|
||||||
|
assert_eq!(green, p.current[2]);
|
||||||
|
assert_eq!(blue, p.current[3]);
|
||||||
|
assert_eq!(3, p.mask.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn approx(expected: f32, actual: f32) {
|
||||||
|
assert!((expected - actual).abs() <= 0.5, "{expected} vs {actual}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lab_from_rgb() {
|
||||||
|
let white = Lab::from_rgb(rgb(255, 255, 255));
|
||||||
|
approx(100.0, white.l);
|
||||||
|
approx(0.0, white.a);
|
||||||
|
approx(0.0, white.b);
|
||||||
|
|
||||||
|
let black = Lab::from_rgb(rgb(0, 0, 0));
|
||||||
|
approx(0.0, black.l);
|
||||||
|
approx(0.0, black.a);
|
||||||
|
approx(0.0, black.b);
|
||||||
|
|
||||||
|
let red = Lab::from_rgb(rgb(255, 0, 0));
|
||||||
|
approx(53.23, red.l);
|
||||||
|
approx(80.11, red.a);
|
||||||
|
approx(67.22, red.b);
|
||||||
|
|
||||||
|
let green = Lab::from_rgb(rgb(0, 128, 0));
|
||||||
|
approx(46.23, green.l);
|
||||||
|
approx(-51.70, green.a);
|
||||||
|
approx(49.90, green.b);
|
||||||
|
|
||||||
|
let blue = Lab::from_rgb(rgb(0, 0, 255));
|
||||||
|
approx(32.30, blue.l);
|
||||||
|
approx(79.20, blue.a);
|
||||||
|
approx(-107.86, blue.b);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate256_color_base16_preserved() {
|
||||||
|
let bg = rgb(0, 0, 0);
|
||||||
|
let fg = rgb(255, 255, 255);
|
||||||
|
let palette = generate_256_color(DEFAULT, PaletteMask::init_empty(), bg, fg, false);
|
||||||
|
for i in 0..16 {
|
||||||
|
assert_eq!(DEFAULT[i], palette[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate256_color_cube_corners_match_base_colors() {
|
||||||
|
let bg = rgb(0, 0, 0);
|
||||||
|
let fg = rgb(255, 255, 255);
|
||||||
|
let palette = generate_256_color(DEFAULT, PaletteMask::init_empty(), bg, fg, false);
|
||||||
|
assert_eq!(bg, palette[16]);
|
||||||
|
assert_eq!(fg, palette[231]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate256_color_cube_corners_black_white_with_harmonious_false() {
|
||||||
|
let black = rgb(0, 0, 0);
|
||||||
|
let white = rgb(255, 255, 255);
|
||||||
|
|
||||||
|
let dark = generate_256_color(DEFAULT, PaletteMask::init_empty(), black, white, false);
|
||||||
|
assert_eq!(black, dark[16]);
|
||||||
|
assert_eq!(white, dark[231]);
|
||||||
|
|
||||||
|
let light = generate_256_color(DEFAULT, PaletteMask::init_empty(), white, black, false);
|
||||||
|
assert_eq!(black, light[16]);
|
||||||
|
assert_eq!(white, light[231]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate256_color_light_theme_cube_corners_with_harmonious_true() {
|
||||||
|
let white = rgb(255, 255, 255);
|
||||||
|
let black = rgb(0, 0, 0);
|
||||||
|
let palette = generate_256_color(DEFAULT, PaletteMask::init_empty(), white, black, true);
|
||||||
|
assert_eq!(white, palette[16]);
|
||||||
|
assert_eq!(black, palette[231]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate256_color_grayscale_ramp_monotonic_luminance() {
|
||||||
|
let bg = rgb(0, 0, 0);
|
||||||
|
let fg = rgb(255, 255, 255);
|
||||||
|
let palette = generate_256_color(DEFAULT, PaletteMask::init_empty(), bg, fg, false);
|
||||||
|
let mut prev_lum = 0.0;
|
||||||
|
for c in &palette[232..256] {
|
||||||
|
let lum = c.luminance();
|
||||||
|
assert!(lum >= prev_lum);
|
||||||
|
prev_lum = lum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate256_color_skip_mask_preserves_original_colors() {
|
||||||
|
let bg = rgb(0, 0, 0);
|
||||||
|
let fg = rgb(255, 255, 255);
|
||||||
|
|
||||||
|
let mut skip = PaletteMask::init_empty();
|
||||||
|
skip.set(20);
|
||||||
|
skip.set(100);
|
||||||
|
skip.set(240);
|
||||||
|
|
||||||
|
let palette = generate_256_color(DEFAULT, skip, bg, fg, false);
|
||||||
|
assert_eq!(DEFAULT[20], palette[20]);
|
||||||
|
assert_eq!(DEFAULT[100], palette[100]);
|
||||||
|
assert_eq!(DEFAULT[240], palette[240]);
|
||||||
|
|
||||||
|
assert!(!palette[21].eql(DEFAULT[21]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate256_color_dark_theme_harmonious_has_no_effect() {
|
||||||
|
let bg = rgb(0, 0, 0);
|
||||||
|
let fg = rgb(255, 255, 255);
|
||||||
|
let normal = generate_256_color(DEFAULT, PaletteMask::init_empty(), bg, fg, false);
|
||||||
|
let harmonious = generate_256_color(DEFAULT, PaletteMask::init_empty(), bg, fg, true);
|
||||||
|
for i in 16..256 {
|
||||||
|
assert_eq!(normal[i], harmonious[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate256_color_light_theme_harmonious_skips_inversion() {
|
||||||
|
let bg = rgb(255, 255, 255);
|
||||||
|
let fg = rgb(0, 0, 0);
|
||||||
|
let inverted = generate_256_color(DEFAULT, PaletteMask::init_empty(), bg, fg, false);
|
||||||
|
let harmonious = generate_256_color(DEFAULT, PaletteMask::init_empty(), bg, fg, true);
|
||||||
|
|
||||||
|
assert_eq!(bg, harmonious[16]);
|
||||||
|
assert!(!inverted[16].eql(bg));
|
||||||
|
|
||||||
|
let differ = (16..232)
|
||||||
|
.filter(|&i| !inverted[i].eql(harmonious[i]))
|
||||||
|
.count();
|
||||||
|
assert!(differ > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate256_color_light_theme_harmonious_grayscale_ramp() {
|
||||||
|
let bg = rgb(255, 255, 255);
|
||||||
|
let fg = rgb(0, 0, 0);
|
||||||
|
|
||||||
|
{
|
||||||
|
let palette = generate_256_color(DEFAULT, PaletteMask::init_empty(), bg, fg, false);
|
||||||
|
let mut prev_lum = 0.0;
|
||||||
|
for c in &palette[232..256] {
|
||||||
|
let lum = c.luminance();
|
||||||
|
assert!(lum >= prev_lum);
|
||||||
|
prev_lum = lum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let palette = generate_256_color(DEFAULT, PaletteMask::init_empty(), bg, fg, true);
|
||||||
|
let mut prev_lum = 1.0;
|
||||||
|
for c in &palette[232..256] {
|
||||||
|
let lum = c.luminance();
|
||||||
|
assert!(lum <= prev_lum);
|
||||||
|
prev_lum = lum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lab_to_rgb() {
|
||||||
|
let cases = [
|
||||||
|
rgb(255, 255, 255),
|
||||||
|
rgb(0, 0, 0),
|
||||||
|
rgb(255, 0, 0),
|
||||||
|
rgb(0, 128, 0),
|
||||||
|
rgb(0, 0, 255),
|
||||||
|
rgb(128, 128, 128),
|
||||||
|
rgb(64, 224, 208),
|
||||||
|
];
|
||||||
|
for expected in cases {
|
||||||
|
let actual = Lab::from_rgb(expected).to_rgb();
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
//! CSI command enums (ZIG/src/terminal/csi.zig).
|
||||||
|
|
||||||
|
/// Modes for the ED CSI command.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum EraseDisplay {
|
||||||
|
Below = 0,
|
||||||
|
Above = 1,
|
||||||
|
Complete = 2,
|
||||||
|
Scrollback = 3,
|
||||||
|
/// This is an extension added by Kitty to move the viewport into the
|
||||||
|
/// scrollback and then erase the display.
|
||||||
|
ScrollComplete = 22,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EraseDisplay {
|
||||||
|
/// Non-exhaustive in spirit: unknown values yield `None`.
|
||||||
|
pub fn from_int(v: u16) -> Option<Self> {
|
||||||
|
Some(match v {
|
||||||
|
0 => Self::Below,
|
||||||
|
1 => Self::Above,
|
||||||
|
2 => Self::Complete,
|
||||||
|
3 => Self::Scrollback,
|
||||||
|
22 => Self::ScrollComplete,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modes for the EL CSI command. Non-exhaustive in Zig so that conversion
|
||||||
|
/// never fails since the inputs are user-generated; `Other` carries the raw value.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum EraseLine {
|
||||||
|
Right,
|
||||||
|
Left,
|
||||||
|
Complete,
|
||||||
|
RightUnlessPendingWrap,
|
||||||
|
Other(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EraseLine {
|
||||||
|
pub fn from_int(v: u8) -> Self {
|
||||||
|
match v {
|
||||||
|
0 => Self::Right,
|
||||||
|
1 => Self::Left,
|
||||||
|
2 => Self::Complete,
|
||||||
|
4 => Self::RightUnlessPendingWrap,
|
||||||
|
v => Self::Other(v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modes for the TBC (tab clear) command. Non-exhaustive like [`EraseLine`].
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum TabClear {
|
||||||
|
Current,
|
||||||
|
All,
|
||||||
|
Other(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TabClear {
|
||||||
|
pub fn from_int(v: u8) -> Self {
|
||||||
|
match v {
|
||||||
|
0 => Self::Current,
|
||||||
|
3 => Self::All,
|
||||||
|
v => Self::Other(v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Style formats for terminal size reports (XTWINOPS).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SizeReportStyle {
|
||||||
|
Csi14T,
|
||||||
|
Csi16T,
|
||||||
|
Csi18T,
|
||||||
|
Csi21T,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// XTWINOPS CSI 22/23
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct TitlePushPop {
|
||||||
|
pub op: TitlePushPopOp,
|
||||||
|
pub index: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum TitlePushPopOp {
|
||||||
|
Push,
|
||||||
|
Pop,
|
||||||
|
}
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
//! DCS command handler (ZIG/src/terminal/dcs.zig). This should be hooked
|
||||||
|
//! into a stream. The hook/put/unhook functions are meant to be called from
|
||||||
|
//! the stream's dcsHook, dcsPut, and dcsUnhook functions, respectively.
|
||||||
|
|
||||||
|
use crate::parser::Dcs;
|
||||||
|
use crate::tmux::control::{Notification, Parser as TmuxParser};
|
||||||
|
|
||||||
|
/// DCS command handler.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Handler {
|
||||||
|
pub state: State,
|
||||||
|
|
||||||
|
/// Maximum bytes any DCS command can take. This is to prevent
|
||||||
|
/// malicious input from causing us to allocate too much memory.
|
||||||
|
/// This is arbitrarily set to 1MB today, increase if needed.
|
||||||
|
pub max_bytes: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Handler {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
state: State::Inactive,
|
||||||
|
max_bytes: 1024 * 1024,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Hook {
|
||||||
|
state: State,
|
||||||
|
command: Option<Command>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hook(&mut self, dcs: &Dcs) -> Option<Command> {
|
||||||
|
debug_assert!(matches!(self.state, State::Inactive));
|
||||||
|
|
||||||
|
// Initialize our state to ignore in case of error
|
||||||
|
self.state = State::Ignore;
|
||||||
|
|
||||||
|
// Try to parse the hook.
|
||||||
|
let Some(hk) = self.try_hook(dcs) else {
|
||||||
|
log::info!("unknown DCS hook: {dcs:?}");
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.state = hk.state;
|
||||||
|
hk.command
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_hook(&self, dcs: &Dcs) -> Option<Hook> {
|
||||||
|
let params = dcs.params();
|
||||||
|
match dcs.intermediates() {
|
||||||
|
[] => match dcs.final_ {
|
||||||
|
// Tmux control mode
|
||||||
|
b'p' => {
|
||||||
|
// Tmux control mode must start with ESC P 1000 p
|
||||||
|
if params.len() != 1 || params[0] != 1000 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut parser = TmuxParser::new();
|
||||||
|
parser.max_bytes = self.max_bytes;
|
||||||
|
Some(Hook {
|
||||||
|
state: State::Tmux(Box::new(parser)),
|
||||||
|
command: Some(Command::Tmux(Notification::Enter)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
|
||||||
|
[b'+'] => match dcs.final_ {
|
||||||
|
// XTGETTCAP
|
||||||
|
// https://github.com/mitchellh/ghostty/issues/517
|
||||||
|
b'q' => Some(Hook {
|
||||||
|
state: State::Xtgettcap(Vec::with_capacity(128)),
|
||||||
|
command: None,
|
||||||
|
}),
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
|
||||||
|
[b'$'] => match dcs.final_ {
|
||||||
|
// DECRQSS
|
||||||
|
b'q' => Some(Hook {
|
||||||
|
state: State::Decrqss(DecrqssBuffer::default()),
|
||||||
|
command: None,
|
||||||
|
}),
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Put a byte into the DCS handler. This will return a command
|
||||||
|
/// if a command needs to be executed.
|
||||||
|
pub fn put(&mut self, byte: u8) -> Option<Command> {
|
||||||
|
let Ok(v) = self.try_put(byte) else {
|
||||||
|
// On error we just discard our state and ignore the rest
|
||||||
|
log::info!("error putting byte into DCS handler err=OutOfMemory");
|
||||||
|
self.discard();
|
||||||
|
self.state = State::Ignore;
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_put(&mut self, byte: u8) -> Result<Option<Command>, ()> {
|
||||||
|
match &mut self.state {
|
||||||
|
State::Inactive | State::Ignore => {}
|
||||||
|
|
||||||
|
State::Tmux(tmux) => {
|
||||||
|
return match tmux.put(byte) {
|
||||||
|
Ok(n) => Ok(n.map(Command::Tmux)),
|
||||||
|
Err(_) => Err(()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
State::Xtgettcap(list) => {
|
||||||
|
if list.len() >= self.max_bytes {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
list.push(byte);
|
||||||
|
}
|
||||||
|
|
||||||
|
State::Decrqss(buffer) => {
|
||||||
|
if buffer.len >= buffer.data.len() {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
buffer.data[buffer.len] = byte;
|
||||||
|
buffer.len += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unhook(&mut self) -> Option<Command> {
|
||||||
|
let state = std::mem::replace(&mut self.state, State::Inactive);
|
||||||
|
match state {
|
||||||
|
State::Inactive | State::Ignore => None,
|
||||||
|
|
||||||
|
State::Tmux(_) => Some(Command::Tmux(Notification::Exit)),
|
||||||
|
|
||||||
|
State::Xtgettcap(mut list) => {
|
||||||
|
list.make_ascii_uppercase();
|
||||||
|
Some(Command::Xtgettcap(Xtgettcap { data: list, i: 0 }))
|
||||||
|
}
|
||||||
|
|
||||||
|
State::Decrqss(buffer) => Some(Command::Decrqss(match buffer.len {
|
||||||
|
0 => Decrqss::None,
|
||||||
|
1 => match buffer.data[0] {
|
||||||
|
b'm' => Decrqss::Sgr,
|
||||||
|
b'r' => Decrqss::Decstbm,
|
||||||
|
b's' => Decrqss::Decslrm,
|
||||||
|
_ => Decrqss::None,
|
||||||
|
},
|
||||||
|
2 => match buffer.data[0] {
|
||||||
|
b' ' => match buffer.data[1] {
|
||||||
|
b'q' => Decrqss::Decscusr,
|
||||||
|
_ => Decrqss::None,
|
||||||
|
},
|
||||||
|
_ => Decrqss::None,
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn discard(&mut self) {
|
||||||
|
self.state = State::Inactive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Command {
|
||||||
|
/// XTGETTCAP
|
||||||
|
Xtgettcap(Xtgettcap),
|
||||||
|
|
||||||
|
/// DECRQSS
|
||||||
|
Decrqss(Decrqss),
|
||||||
|
|
||||||
|
/// Tmux control mode
|
||||||
|
Tmux(Notification),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Xtgettcap {
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
pub i: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Xtgettcap {
|
||||||
|
/// Returns the next terminfo key being requested and None
|
||||||
|
/// when there are no more keys. The returned value is NOT hex-decoded
|
||||||
|
/// because we expect to use a comptime lookup table.
|
||||||
|
#[allow(clippy::should_implement_trait)]
|
||||||
|
pub fn next(&mut self) -> Option<&[u8]> {
|
||||||
|
let items = &self.data;
|
||||||
|
if self.i >= items.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let rem = &items[self.i..];
|
||||||
|
let idx = rem.iter().position(|&b| b == b';').unwrap_or(rem.len());
|
||||||
|
|
||||||
|
// Note that if we're at the end, idx + 1 is len + 1 so we're over
|
||||||
|
// the end but that's okay because our check above is >= so we'll
|
||||||
|
// never read.
|
||||||
|
self.i += idx + 1;
|
||||||
|
|
||||||
|
Some(&rem[..idx])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supported DECRQSS settings
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Decrqss {
|
||||||
|
None,
|
||||||
|
Sgr,
|
||||||
|
Decscusr,
|
||||||
|
Decstbm,
|
||||||
|
Decslrm,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct DecrqssBuffer {
|
||||||
|
data: [u8; 2],
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub enum State {
|
||||||
|
/// We're not in a DCS state at the moment.
|
||||||
|
#[default]
|
||||||
|
Inactive,
|
||||||
|
|
||||||
|
/// We're hooked, but its an unknown DCS command or one that went
|
||||||
|
/// invalid due to some bad input, so we're ignoring the rest.
|
||||||
|
Ignore,
|
||||||
|
|
||||||
|
/// XTGETTCAP
|
||||||
|
Xtgettcap(Vec<u8>),
|
||||||
|
|
||||||
|
/// DECRQSS
|
||||||
|
Decrqss(DecrqssBuffer),
|
||||||
|
|
||||||
|
/// Tmux control mode: https://github.com/tmux/tmux/wiki/Control-Mode
|
||||||
|
Tmux(Box<TmuxParser>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn dcs(inter: &[u8], params: &[u16], final_: u8) -> Dcs {
|
||||||
|
Dcs::new(inter, params, final_)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_dcs_command() {
|
||||||
|
let mut h = Handler::new();
|
||||||
|
assert!(h.hook(&dcs(b"", &[], b'A')).is_none());
|
||||||
|
assert!(matches!(h.state, State::Ignore));
|
||||||
|
assert!(h.unhook().is_none());
|
||||||
|
assert!(matches!(h.state, State::Inactive));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xtgettcap(input: &str) -> Xtgettcap {
|
||||||
|
let mut h = Handler::new();
|
||||||
|
assert!(h.hook(&dcs(b"+", &[], b'q')).is_none());
|
||||||
|
for b in input.bytes() {
|
||||||
|
let _ = h.put(b);
|
||||||
|
}
|
||||||
|
match h.unhook().unwrap() {
|
||||||
|
Command::Xtgettcap(x) => x,
|
||||||
|
_ => panic!("expected xtgettcap"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xtgettcap_command() {
|
||||||
|
let mut cmd = xtgettcap("536D756C78");
|
||||||
|
assert_eq!(cmd.next(), Some(&b"536D756C78"[..]));
|
||||||
|
assert_eq!(cmd.next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xtgettcap_mixed_case() {
|
||||||
|
let mut cmd = xtgettcap("536d756C78");
|
||||||
|
assert_eq!(cmd.next(), Some(&b"536D756C78"[..]));
|
||||||
|
assert_eq!(cmd.next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xtgettcap_command_multiple_keys() {
|
||||||
|
let mut cmd = xtgettcap("536D756C78;536D756C78");
|
||||||
|
assert_eq!(cmd.next(), Some(&b"536D756C78"[..]));
|
||||||
|
assert_eq!(cmd.next(), Some(&b"536D756C78"[..]));
|
||||||
|
assert_eq!(cmd.next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xtgettcap_command_invalid_data() {
|
||||||
|
let mut cmd = xtgettcap("who;536D756C78");
|
||||||
|
assert_eq!(cmd.next(), Some(&b"WHO"[..]));
|
||||||
|
assert_eq!(cmd.next(), Some(&b"536D756C78"[..]));
|
||||||
|
assert_eq!(cmd.next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xtgettcap_max_bytes() {
|
||||||
|
let mut h = Handler::new();
|
||||||
|
h.max_bytes = 2;
|
||||||
|
assert!(h.hook(&dcs(b"+", &[], b'q')).is_none());
|
||||||
|
for b in b"abc" {
|
||||||
|
let _ = h.put(*b);
|
||||||
|
}
|
||||||
|
assert!(matches!(h.state, State::Ignore));
|
||||||
|
assert!(h.unhook().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decrqss_command() {
|
||||||
|
let mut h = Handler::new();
|
||||||
|
assert!(h.hook(&dcs(b"$", &[], b'q')).is_none());
|
||||||
|
let _ = h.put(b'm');
|
||||||
|
assert_eq!(h.unhook(), Some(Command::Decrqss(Decrqss::Sgr)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decrqss_invalid_command() {
|
||||||
|
let mut h = Handler::new();
|
||||||
|
assert!(h.hook(&dcs(b"$", &[], b'q')).is_none());
|
||||||
|
let _ = h.put(b'z');
|
||||||
|
assert_eq!(h.unhook(), Some(Command::Decrqss(Decrqss::None)));
|
||||||
|
|
||||||
|
h.discard();
|
||||||
|
|
||||||
|
assert!(h.hook(&dcs(b"$", &[], b'q')).is_none());
|
||||||
|
let _ = h.put(b'"');
|
||||||
|
let _ = h.put(b' ');
|
||||||
|
let _ = h.put(b'q');
|
||||||
|
assert!(h.unhook().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmux_enter_and_implicit_exit() {
|
||||||
|
let mut h = Handler::new();
|
||||||
|
|
||||||
|
let cmd = h.hook(&dcs(b"", &[1000], b'p')).unwrap();
|
||||||
|
assert_eq!(cmd, Command::Tmux(Notification::Enter));
|
||||||
|
|
||||||
|
let cmd = h.unhook().unwrap();
|
||||||
|
assert_eq!(cmd, Command::Tmux(Notification::Exit));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmux_requires_1000_param() {
|
||||||
|
let mut h = Handler::new();
|
||||||
|
assert!(h.hook(&dcs(b"", &[1], b'p')).is_none());
|
||||||
|
assert!(matches!(h.state, State::Ignore));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
//! Device status requests (ZIG/src/terminal/device_status.zig).
|
||||||
|
|
||||||
|
/// The available device status requests. Each is identified by a numeric
|
||||||
|
/// value plus whether it uses the "?" prefix.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum Request {
|
||||||
|
OperatingStatus,
|
||||||
|
CursorPosition,
|
||||||
|
ColorScheme,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
/// The numeric value of the request.
|
||||||
|
pub fn value(self) -> u16 {
|
||||||
|
ENTRIES
|
||||||
|
.iter()
|
||||||
|
.find(|e| e.request == self)
|
||||||
|
.map_or(0, |e| e.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the request uses the "?" prefix.
|
||||||
|
pub fn question(self) -> bool {
|
||||||
|
ENTRIES
|
||||||
|
.iter()
|
||||||
|
.find(|e| e.request == self)
|
||||||
|
.is_some_and(|e| e.question)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
request: Request,
|
||||||
|
value: u16,
|
||||||
|
question: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The full list of device status request entries.
|
||||||
|
const ENTRIES: &[Entry] = &[
|
||||||
|
Entry {
|
||||||
|
request: Request::OperatingStatus,
|
||||||
|
value: 5,
|
||||||
|
question: false,
|
||||||
|
},
|
||||||
|
Entry {
|
||||||
|
request: Request::CursorPosition,
|
||||||
|
value: 6,
|
||||||
|
question: false,
|
||||||
|
},
|
||||||
|
Entry {
|
||||||
|
request: Request::ColorScheme,
|
||||||
|
value: 996,
|
||||||
|
question: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn req_from_int(v: u16, question: bool) -> Option<Request> {
|
||||||
|
ENTRIES
|
||||||
|
.iter()
|
||||||
|
.find(|e| e.value == v && e.question == question)
|
||||||
|
.map(|e| e.request)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn req_from_int_lookup() {
|
||||||
|
assert_eq!(req_from_int(5, false), Some(Request::OperatingStatus));
|
||||||
|
assert_eq!(req_from_int(6, false), Some(Request::CursorPosition));
|
||||||
|
assert_eq!(req_from_int(996, true), Some(Request::ColorScheme));
|
||||||
|
assert_eq!(req_from_int(996, false), None);
|
||||||
|
assert_eq!(req_from_int(5, true), None);
|
||||||
|
assert_eq!(Request::ColorScheme.value(), 996);
|
||||||
|
assert!(Request::ColorScheme.question());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
//! Kitty color protocol (ZIG/src/terminal/kitty/color.zig).
|
||||||
|
|
||||||
|
use crate::color::Rgb;
|
||||||
|
use crate::osc::Terminator;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// OSC 21 payload.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
|
pub struct OSC {
|
||||||
|
/// list of requests
|
||||||
|
pub list: Vec<Request>,
|
||||||
|
|
||||||
|
/// We must reply with the same string terminator (ST) as used in the
|
||||||
|
/// request.
|
||||||
|
pub terminator: Terminator,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Request {
|
||||||
|
Query(Kind),
|
||||||
|
Set { key: Kind, color: Rgb },
|
||||||
|
Reset(Kind),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum Special {
|
||||||
|
Foreground,
|
||||||
|
Background,
|
||||||
|
SelectionForeground,
|
||||||
|
SelectionBackground,
|
||||||
|
Cursor,
|
||||||
|
CursorText,
|
||||||
|
VisualBell,
|
||||||
|
SecondTransparentBackground,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Special {
|
||||||
|
/// The tag name (Zig `@tagName`).
|
||||||
|
pub fn name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Special::Foreground => "foreground",
|
||||||
|
Special::Background => "background",
|
||||||
|
Special::SelectionForeground => "selection_foreground",
|
||||||
|
Special::SelectionBackground => "selection_background",
|
||||||
|
Special::Cursor => "cursor",
|
||||||
|
Special::CursorText => "cursor_text",
|
||||||
|
Special::VisualBell => "visual_bell",
|
||||||
|
Special::SecondTransparentBackground => "second_transparent_background",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zig `std.meta.stringToEnum`.
|
||||||
|
pub fn from_name(s: &[u8]) -> Option<Special> {
|
||||||
|
Some(match s {
|
||||||
|
b"foreground" => Special::Foreground,
|
||||||
|
b"background" => Special::Background,
|
||||||
|
b"selection_foreground" => Special::SelectionForeground,
|
||||||
|
b"selection_background" => Special::SelectionBackground,
|
||||||
|
b"cursor" => Special::Cursor,
|
||||||
|
b"cursor_text" => Special::CursorText,
|
||||||
|
b"visual_bell" => Special::VisualBell,
|
||||||
|
b"second_transparent_background" => Special::SecondTransparentBackground,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum Kind {
|
||||||
|
Palette(u8),
|
||||||
|
Special(Special),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Kind {
|
||||||
|
/// Number of distinct kinds (255 + number of specials).
|
||||||
|
pub const MAX: usize = u8::MAX as usize + 8;
|
||||||
|
|
||||||
|
pub fn parse(key: &[u8]) -> Option<Kind> {
|
||||||
|
if let Some(s) = Special::from_name(key) {
|
||||||
|
return Some(Kind::Special(s));
|
||||||
|
}
|
||||||
|
// Zig parseUnsigned(u8, key, 10): optional '+', digits only.
|
||||||
|
let digits = match key.first()? {
|
||||||
|
b'+' => &key[1..],
|
||||||
|
_ => key,
|
||||||
|
};
|
||||||
|
if digits.is_empty() || !digits.iter().all(u8::is_ascii_digit) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let s = std::str::from_utf8(digits).ok()?;
|
||||||
|
s.parse::<u8>().ok().map(Kind::Palette)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Kind {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Kind::Palette(p) => write!(f, "{p}"),
|
||||||
|
Kind::Special(s) => f.write_str(s.name()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_kitty_color_protocol_kind_string() {
|
||||||
|
assert_eq!("foreground", Kind::Special(Special::Foreground).to_string());
|
||||||
|
assert_eq!("42", Kind::Palette(42).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,1391 @@
|
|||||||
|
//! Kitty graphics protocol command parser
|
||||||
|
//! (ZIG/src/terminal/kitty/graphics_command.zig).
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// The key-value pairs for the control information for a command. The
|
||||||
|
/// keys are always single characters and the values are either single
|
||||||
|
/// characters or 32-bit unsigned integers.
|
||||||
|
///
|
||||||
|
/// For the value of this: if the value is a single printable ASCII character
|
||||||
|
/// it is the ASCII code. Otherwise, it is parsed as a 32-bit unsigned integer.
|
||||||
|
type Kv = HashMap<u8, u32>;
|
||||||
|
|
||||||
|
/// Errors from the command parser.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("invalid format")]
|
||||||
|
InvalidFormat,
|
||||||
|
#[error("invalid data")]
|
||||||
|
InvalidData,
|
||||||
|
#[error("invalid character")]
|
||||||
|
InvalidCharacter,
|
||||||
|
#[error("overflow")]
|
||||||
|
Overflow,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mirrors `std.fmt.parseInt(T, buf, 10)` for 32-bit targets: optional sign,
|
||||||
|
/// at least one digit, range checked.
|
||||||
|
fn parse_int(buf: &[u8], signed: bool) -> Result<u32, Error> {
|
||||||
|
// kv_temp is at most 11 bytes so an i64 always suffices unless there
|
||||||
|
// are overflow digits, which we detect via checked arithmetic.
|
||||||
|
let (neg, digits) = match buf.first() {
|
||||||
|
Some(b'+') if buf.len() > 1 => (false, &buf[1..]),
|
||||||
|
Some(b'-') if buf.len() > 1 => (true, &buf[1..]),
|
||||||
|
_ => (false, buf),
|
||||||
|
};
|
||||||
|
if digits.is_empty() {
|
||||||
|
return Err(Error::InvalidCharacter);
|
||||||
|
}
|
||||||
|
let mut v: i64 = 0;
|
||||||
|
let mut overflow = false;
|
||||||
|
for &c in digits {
|
||||||
|
if !c.is_ascii_digit() {
|
||||||
|
return Err(Error::InvalidCharacter);
|
||||||
|
}
|
||||||
|
match v
|
||||||
|
.checked_mul(10)
|
||||||
|
.and_then(|x| x.checked_add(i64::from(c - b'0')))
|
||||||
|
{
|
||||||
|
Some(x) => v = x,
|
||||||
|
None => overflow = true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if overflow {
|
||||||
|
return Err(Error::Overflow);
|
||||||
|
}
|
||||||
|
if neg {
|
||||||
|
v = -v;
|
||||||
|
}
|
||||||
|
if signed {
|
||||||
|
i32::try_from(v)
|
||||||
|
.map(|x| x as u32)
|
||||||
|
.map_err(|_| Error::Overflow)
|
||||||
|
} else {
|
||||||
|
u32::try_from(v).map_err(|_| Error::Overflow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode base64 payload data. The reference implementation uses simdutf,
|
||||||
|
/// which ignores ASCII whitespace and tolerates missing padding.
|
||||||
|
pub fn decode_base64(input: &[u8]) -> Result<Vec<u8>, Error> {
|
||||||
|
use base64::Engine;
|
||||||
|
use base64::engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig};
|
||||||
|
const ENGINE: GeneralPurpose = GeneralPurpose::new(
|
||||||
|
&base64::alphabet::STANDARD,
|
||||||
|
GeneralPurposeConfig::new()
|
||||||
|
.with_decode_padding_mode(DecodePaddingMode::Indifferent)
|
||||||
|
.with_decode_allow_trailing_bits(true),
|
||||||
|
);
|
||||||
|
let cleaned: Vec<u8> = input
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|b| !matches!(b, b' ' | b'\t' | b'\n' | b'\r' | 0x0c))
|
||||||
|
.collect();
|
||||||
|
let trimmed_len = cleaned
|
||||||
|
.iter()
|
||||||
|
.rposition(|&b| b != b'=')
|
||||||
|
.map_or(0, |i| i + 1);
|
||||||
|
ENGINE
|
||||||
|
.decode(&cleaned[..trimmed_len])
|
||||||
|
.map_err(|_| Error::InvalidData)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum State {
|
||||||
|
/// Parsing k/v pairs. The "ignore" variants are in that state
|
||||||
|
/// but ignore any data because we know they're invalid.
|
||||||
|
ControlKey,
|
||||||
|
ControlKeyIgnore,
|
||||||
|
ControlValue,
|
||||||
|
ControlValueIgnore,
|
||||||
|
|
||||||
|
/// Collecting the data payload blob.
|
||||||
|
Data,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Command parser parses the Kitty graphics protocol escape sequence.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Parser {
|
||||||
|
/// The list of KV pairs that we're building up.
|
||||||
|
kv: Kv,
|
||||||
|
|
||||||
|
/// Buffer to store the key/value of a KV pair. The value of a KV pair is
|
||||||
|
/// at most a 32-bit integer which at most is 10 characters (4294967295),
|
||||||
|
/// plus one character for the sign bit on signed ints.
|
||||||
|
kv_temp: [u8; 11],
|
||||||
|
kv_temp_len: usize,
|
||||||
|
/// Current kv key
|
||||||
|
kv_current: u8,
|
||||||
|
|
||||||
|
/// The bytes of the data payload.
|
||||||
|
data: Vec<u8>,
|
||||||
|
|
||||||
|
state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Parser {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parser {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
kv: HashMap::new(),
|
||||||
|
kv_temp: [0; 11],
|
||||||
|
kv_temp_len: 0,
|
||||||
|
kv_current: 0,
|
||||||
|
data: Vec::new(),
|
||||||
|
state: State::ControlKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a complete command string.
|
||||||
|
pub fn parse_string(data: &[u8]) -> Result<Command, Error> {
|
||||||
|
let mut parser = Self::new();
|
||||||
|
for &c in data {
|
||||||
|
parser.feed(c)?;
|
||||||
|
}
|
||||||
|
parser.complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Feed a single byte to the parser.
|
||||||
|
///
|
||||||
|
/// The first byte to start parsing should be the byte immediately following
|
||||||
|
/// the "G" in the APC sequence, i.e. "\x1b_G123" the first byte should
|
||||||
|
/// be "1".
|
||||||
|
pub fn feed(&mut self, c: u8) -> Result<(), Error> {
|
||||||
|
match self.state {
|
||||||
|
State::ControlKey => match c {
|
||||||
|
// '=' means the key is complete and we're moving to the value.
|
||||||
|
b'=' => {
|
||||||
|
if self.kv_temp_len == 1 {
|
||||||
|
self.kv_current = self.kv_temp[0];
|
||||||
|
self.kv_temp_len = 0;
|
||||||
|
self.state = State::ControlValue;
|
||||||
|
} else {
|
||||||
|
// All control keys are a single character right now so
|
||||||
|
// if we're not a single character just ignore follow-up
|
||||||
|
// data.
|
||||||
|
self.state = State::ControlValueIgnore;
|
||||||
|
self.kv_temp_len = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This can be encountered if we have a sequence with no
|
||||||
|
// control data, only payload data (i.e. "\x1b_G;<data>").
|
||||||
|
//
|
||||||
|
// Kitty treats this as valid so we do as well.
|
||||||
|
b';' => self.state = State::Data,
|
||||||
|
|
||||||
|
_ => self.accumulate_value(c, State::ControlKeyIgnore),
|
||||||
|
},
|
||||||
|
|
||||||
|
State::ControlKeyIgnore => {
|
||||||
|
if c == b'=' {
|
||||||
|
self.state = State::ControlValueIgnore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
State::ControlValue => match c {
|
||||||
|
// move to next key
|
||||||
|
b',' => self.finish_value(State::ControlKey)?,
|
||||||
|
// move to data
|
||||||
|
b';' => self.finish_value(State::Data)?,
|
||||||
|
_ => self.accumulate_value(c, State::ControlValueIgnore),
|
||||||
|
},
|
||||||
|
|
||||||
|
State::ControlValueIgnore => match c {
|
||||||
|
b',' => self.state = State::ControlKeyIgnore,
|
||||||
|
b';' => self.state = State::Data,
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
|
||||||
|
State::Data => self.data.push(c),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete the parsing. This must be called after all the
|
||||||
|
/// bytes have been fed to the parser. The payload data is moved into the
|
||||||
|
/// resulting command.
|
||||||
|
pub fn complete(&mut self) -> Result<Command, Error> {
|
||||||
|
match self.state {
|
||||||
|
// We can't ever end in the control key state and be valid.
|
||||||
|
// This means the command looked something like "a=1,b"
|
||||||
|
State::ControlKey | State::ControlKeyIgnore => return Err(Error::InvalidFormat),
|
||||||
|
|
||||||
|
// Some commands (i.e. placements) end without extra data so
|
||||||
|
// we end in the value state. i.e. "a=1,b=2"
|
||||||
|
State::ControlValue => self.finish_value(State::Data)?,
|
||||||
|
State::ControlValueIgnore => {}
|
||||||
|
|
||||||
|
// Most commands end in data, i.e. "a=1,b=2;1234"
|
||||||
|
State::Data => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine our action, which is always a single character.
|
||||||
|
let action: u8 = match self.kv.get(&b'a') {
|
||||||
|
None => b't',
|
||||||
|
Some(&v) => u8::try_from(v).map_err(|_| Error::InvalidFormat)?,
|
||||||
|
};
|
||||||
|
let kv = &self.kv;
|
||||||
|
let control = match action {
|
||||||
|
b'q' => Control::Query(Transmission::parse(kv)?),
|
||||||
|
b't' => Control::Transmit(Transmission::parse(kv)?),
|
||||||
|
b'T' => Control::TransmitAndDisplay {
|
||||||
|
transmission: Transmission::parse(kv)?,
|
||||||
|
display: Display::parse(kv)?,
|
||||||
|
},
|
||||||
|
b'p' => Control::Display(Display::parse(kv)?),
|
||||||
|
b'd' => Control::Delete(Delete::parse(kv)?),
|
||||||
|
b'f' => Control::TransmitAnimationFrame(AnimationFrameLoading::parse(kv)?),
|
||||||
|
b'a' => Control::ControlAnimation(AnimationControl::parse(kv)?),
|
||||||
|
b'c' => Control::ComposeAnimation(AnimationFrameComposition::parse(kv)?),
|
||||||
|
_ => return Err(Error::InvalidFormat),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine our quiet value
|
||||||
|
let quiet = match self.kv.get(&b'q') {
|
||||||
|
Some(0) => Quiet::No,
|
||||||
|
Some(1) => Quiet::Ok,
|
||||||
|
Some(2) => Quiet::Failures,
|
||||||
|
Some(_) => return Err(Error::InvalidFormat),
|
||||||
|
None => Quiet::No,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Command {
|
||||||
|
control,
|
||||||
|
quiet,
|
||||||
|
data: self.decode_data()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes the payload data from base64 and returns it. This consumes
|
||||||
|
/// the collected payload bytes.
|
||||||
|
fn decode_data(&mut self) -> Result<Vec<u8>, Error> {
|
||||||
|
if self.data.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
let data = std::mem::take(&mut self.data);
|
||||||
|
decode_base64(&data).inspect_err(|e| {
|
||||||
|
log::warn!("failed to decode base64 payload data: {e}");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accumulate_value(&mut self, c: u8, overflow_state: State) {
|
||||||
|
let idx = self.kv_temp_len;
|
||||||
|
self.kv_temp_len += 1;
|
||||||
|
if self.kv_temp_len > self.kv_temp.len() {
|
||||||
|
self.state = overflow_state;
|
||||||
|
self.kv_temp_len = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.kv_temp[idx] = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_value(&mut self, next_state: State) -> Result<(), Error> {
|
||||||
|
// We can move states right away, we don't use it.
|
||||||
|
self.state = next_state;
|
||||||
|
|
||||||
|
// Check for ASCII chars first
|
||||||
|
if self.kv_temp_len == 1 {
|
||||||
|
let c = self.kv_temp[0];
|
||||||
|
if !c.is_ascii_digit() {
|
||||||
|
self.kv.insert(self.kv_current, u32::from(c));
|
||||||
|
self.kv_temp_len = 0;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle integer fields, parsing signed fields accordingly. We still
|
||||||
|
// store the fields as u32 as they can be bitcast back later during
|
||||||
|
// building of the higher-level command tree.
|
||||||
|
let buf = &self.kv_temp[..self.kv_temp_len];
|
||||||
|
let v = match self.kv_current {
|
||||||
|
b'z' | b'H' | b'V' => parse_int(buf, true)?,
|
||||||
|
_ => parse_int(buf, false)?,
|
||||||
|
};
|
||||||
|
self.kv.insert(self.kv_current, v);
|
||||||
|
|
||||||
|
// Clear our temp buffer
|
||||||
|
self.kv_temp_len = 0;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a possible response to a command.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Response {
|
||||||
|
pub id: u32,
|
||||||
|
pub image_number: u32,
|
||||||
|
pub placement_id: u32,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Response {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
id: 0,
|
||||||
|
image_number: 0,
|
||||||
|
placement_id: 0,
|
||||||
|
message: "OK".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
pub fn encode(&self, writer: &mut impl std::io::Write) -> std::io::Result<()> {
|
||||||
|
// We only encode a result if we have either an id or an image number.
|
||||||
|
if self.id == 0 && self.image_number == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to cheaply keep track if we need to add a comma before
|
||||||
|
// the next key-value pair.
|
||||||
|
let mut prior = false;
|
||||||
|
|
||||||
|
writer.write_all(b"\x1b_G")?;
|
||||||
|
if self.id > 0 {
|
||||||
|
prior = true;
|
||||||
|
write!(writer, "i={}", self.id)?;
|
||||||
|
}
|
||||||
|
if self.image_number > 0 {
|
||||||
|
if prior {
|
||||||
|
writer.write_all(b",")?;
|
||||||
|
} else {
|
||||||
|
prior = true;
|
||||||
|
}
|
||||||
|
write!(writer, "I={}", self.image_number)?;
|
||||||
|
}
|
||||||
|
if self.placement_id > 0 {
|
||||||
|
if prior {
|
||||||
|
writer.write_all(b",")?;
|
||||||
|
}
|
||||||
|
write!(writer, "p={}", self.placement_id)?;
|
||||||
|
}
|
||||||
|
writer.write_all(b";")?;
|
||||||
|
writer.write_all(self.message.as_bytes())?;
|
||||||
|
writer.write_all(b"\x1b\\")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if this response is not an error.
|
||||||
|
pub fn ok(&self) -> bool {
|
||||||
|
self.message == "OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Empty response
|
||||||
|
pub fn empty(&self) -> bool {
|
||||||
|
self.id == 0 && self.image_number == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Command {
|
||||||
|
pub control: Control,
|
||||||
|
pub quiet: Quiet,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Action {
|
||||||
|
/// q
|
||||||
|
Query,
|
||||||
|
/// t
|
||||||
|
Transmit,
|
||||||
|
/// T
|
||||||
|
TransmitAndDisplay,
|
||||||
|
/// p
|
||||||
|
Display,
|
||||||
|
/// d
|
||||||
|
Delete,
|
||||||
|
/// f
|
||||||
|
TransmitAnimationFrame,
|
||||||
|
/// a
|
||||||
|
ControlAnimation,
|
||||||
|
/// c
|
||||||
|
ComposeAnimation,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum Quiet {
|
||||||
|
/// 0
|
||||||
|
#[default]
|
||||||
|
No,
|
||||||
|
/// 1
|
||||||
|
Ok,
|
||||||
|
/// 2
|
||||||
|
Failures,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Control {
|
||||||
|
Query(Transmission),
|
||||||
|
Transmit(Transmission),
|
||||||
|
TransmitAndDisplay {
|
||||||
|
transmission: Transmission,
|
||||||
|
display: Display,
|
||||||
|
},
|
||||||
|
Display(Display),
|
||||||
|
Delete(Delete),
|
||||||
|
TransmitAnimationFrame(AnimationFrameLoading),
|
||||||
|
ControlAnimation(AnimationControl),
|
||||||
|
ComposeAnimation(AnimationFrameComposition),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Control {
|
||||||
|
pub fn action(&self) -> Action {
|
||||||
|
match self {
|
||||||
|
Control::Query(_) => Action::Query,
|
||||||
|
Control::Transmit(_) => Action::Transmit,
|
||||||
|
Control::TransmitAndDisplay { .. } => Action::TransmitAndDisplay,
|
||||||
|
Control::Display(_) => Action::Display,
|
||||||
|
Control::Delete(_) => Action::Delete,
|
||||||
|
Control::TransmitAnimationFrame(_) => Action::TransmitAnimationFrame,
|
||||||
|
Control::ControlAnimation(_) => Action::ControlAnimation,
|
||||||
|
Control::ComposeAnimation(_) => Action::ComposeAnimation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
|
/// Take ownership over the data in this command, leaving it empty.
|
||||||
|
pub fn to_owned_data(&mut self) -> Vec<u8> {
|
||||||
|
std::mem::take(&mut self.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the transmission data if it has any.
|
||||||
|
pub fn transmission(&self) -> Option<Transmission> {
|
||||||
|
match &self.control {
|
||||||
|
Control::Query(t) | Control::Transmit(t) => Some(*t),
|
||||||
|
Control::TransmitAndDisplay { transmission, .. } => Some(*transmission),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the display data if it has any.
|
||||||
|
pub fn display(&self) -> Option<Display> {
|
||||||
|
match &self.control {
|
||||||
|
Control::Display(d) => Some(*d),
|
||||||
|
Control::TransmitAndDisplay { display, .. } => Some(*display),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Transmission {
|
||||||
|
/// f
|
||||||
|
pub format: TransmissionFormat,
|
||||||
|
/// t
|
||||||
|
pub medium: TransmissionMedium,
|
||||||
|
/// s
|
||||||
|
pub width: u32,
|
||||||
|
/// v
|
||||||
|
pub height: u32,
|
||||||
|
/// S
|
||||||
|
pub size: u32,
|
||||||
|
/// O
|
||||||
|
pub offset: u32,
|
||||||
|
/// i
|
||||||
|
pub image_id: u32,
|
||||||
|
/// I
|
||||||
|
pub image_number: u32,
|
||||||
|
/// p
|
||||||
|
pub placement_id: u32,
|
||||||
|
/// o
|
||||||
|
pub compression: TransmissionCompression,
|
||||||
|
/// m
|
||||||
|
pub more_chunks: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Transmission {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
format: TransmissionFormat::Rgba,
|
||||||
|
medium: TransmissionMedium::Direct,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
size: 0,
|
||||||
|
offset: 0,
|
||||||
|
image_id: 0,
|
||||||
|
image_number: 0,
|
||||||
|
placement_id: 0,
|
||||||
|
compression: TransmissionCompression::None,
|
||||||
|
more_chunks: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum TransmissionFormat {
|
||||||
|
/// 24
|
||||||
|
Rgb,
|
||||||
|
/// 32
|
||||||
|
Rgba,
|
||||||
|
/// 100
|
||||||
|
Png,
|
||||||
|
|
||||||
|
// The following are not supported directly via the protocol
|
||||||
|
// but they are formats that a png may decode to that we
|
||||||
|
// support.
|
||||||
|
GrayAlpha,
|
||||||
|
Gray,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransmissionFormat {
|
||||||
|
/// Bytes per pixel.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics for `Png`, which must be validated before.
|
||||||
|
pub fn bpp(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::Gray => 1,
|
||||||
|
Self::GrayAlpha => 2,
|
||||||
|
Self::Rgb => 3,
|
||||||
|
Self::Rgba => 4,
|
||||||
|
Self::Png => unreachable!("png must be validated before"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum TransmissionMedium {
|
||||||
|
/// d
|
||||||
|
Direct,
|
||||||
|
/// f
|
||||||
|
File,
|
||||||
|
/// t
|
||||||
|
TemporaryFile,
|
||||||
|
/// s
|
||||||
|
SharedMemory,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum TransmissionCompression {
|
||||||
|
None,
|
||||||
|
/// z
|
||||||
|
ZlibDeflate,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(kv: &Kv, k: u8) -> Option<u32> {
|
||||||
|
kv.get(&k).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Transmission {
|
||||||
|
fn parse(kv: &Kv) -> Result<Transmission, Error> {
|
||||||
|
let mut result = Transmission::default();
|
||||||
|
if let Some(v) = get(kv, b'f') {
|
||||||
|
result.format = match v {
|
||||||
|
24 => TransmissionFormat::Rgb,
|
||||||
|
32 => TransmissionFormat::Rgba,
|
||||||
|
100 => TransmissionFormat::Png,
|
||||||
|
_ => return Err(Error::InvalidFormat),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(v) = get(kv, b't') {
|
||||||
|
let c = u8::try_from(v).map_err(|_| Error::InvalidFormat)?;
|
||||||
|
result.medium = match c {
|
||||||
|
b'd' => TransmissionMedium::Direct,
|
||||||
|
b'f' => TransmissionMedium::File,
|
||||||
|
b't' => TransmissionMedium::TemporaryFile,
|
||||||
|
b's' => TransmissionMedium::SharedMemory,
|
||||||
|
_ => return Err(Error::InvalidFormat),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(v) = get(kv, b's') {
|
||||||
|
result.width = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'v') {
|
||||||
|
result.height = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'S') {
|
||||||
|
result.size = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'O') {
|
||||||
|
result.offset = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'i') {
|
||||||
|
result.image_id = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'I') {
|
||||||
|
result.image_number = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'p') {
|
||||||
|
result.placement_id = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(v) = get(kv, b'o') {
|
||||||
|
let c = u8::try_from(v).map_err(|_| Error::InvalidFormat)?;
|
||||||
|
result.compression = match c {
|
||||||
|
b'z' => TransmissionCompression::ZlibDeflate,
|
||||||
|
_ => return Err(Error::InvalidFormat),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the transmission medium is a local-only medium, ignore the "m"
|
||||||
|
// key. The Kitty graphics protocol specification does not explicitly
|
||||||
|
// call out this behavior (although the "m" key is only mentioned in
|
||||||
|
// connection with remote clients) but that's how it's implemented in
|
||||||
|
// Kitty and at least one client (mpv) relies on this behavior when
|
||||||
|
// using the shared memory transmission medium.
|
||||||
|
//
|
||||||
|
// https://sw.kovidgoyal.net/kitty/graphics-protocol/#the-transmission-medium
|
||||||
|
// https://github.com/kovidgoyal/kitty/blob/ccc3bee9af794f332b4e9adcd714a649f639c397/kitty/graphics.c#L547-L592
|
||||||
|
if result.medium == TransmissionMedium::Direct
|
||||||
|
&& let Some(v) = get(kv, b'm')
|
||||||
|
{
|
||||||
|
result.more_chunks = v > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Display {
|
||||||
|
/// i
|
||||||
|
pub image_id: u32,
|
||||||
|
/// I
|
||||||
|
pub image_number: u32,
|
||||||
|
/// p
|
||||||
|
pub placement_id: u32,
|
||||||
|
/// x
|
||||||
|
pub x: u32,
|
||||||
|
/// y
|
||||||
|
pub y: u32,
|
||||||
|
/// w
|
||||||
|
pub width: u32,
|
||||||
|
/// h
|
||||||
|
pub height: u32,
|
||||||
|
/// X
|
||||||
|
pub x_offset: u32,
|
||||||
|
/// Y
|
||||||
|
pub y_offset: u32,
|
||||||
|
/// c
|
||||||
|
pub columns: u32,
|
||||||
|
/// r
|
||||||
|
pub rows: u32,
|
||||||
|
/// C
|
||||||
|
pub cursor_movement: CursorMovement,
|
||||||
|
/// U
|
||||||
|
pub virtual_placement: bool,
|
||||||
|
/// P
|
||||||
|
pub parent_id: u32,
|
||||||
|
/// Q
|
||||||
|
pub parent_placement_id: u32,
|
||||||
|
/// H
|
||||||
|
pub horizontal_offset: i32,
|
||||||
|
/// V
|
||||||
|
pub vertical_offset: i32,
|
||||||
|
/// z
|
||||||
|
pub z: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Display {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
image_id: 0,
|
||||||
|
image_number: 0,
|
||||||
|
placement_id: 0,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
x_offset: 0,
|
||||||
|
y_offset: 0,
|
||||||
|
columns: 0,
|
||||||
|
rows: 0,
|
||||||
|
cursor_movement: CursorMovement::After,
|
||||||
|
virtual_placement: false,
|
||||||
|
parent_id: 0,
|
||||||
|
parent_placement_id: 0,
|
||||||
|
horizontal_offset: 0,
|
||||||
|
vertical_offset: 0,
|
||||||
|
z: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum CursorMovement {
|
||||||
|
/// 0
|
||||||
|
After,
|
||||||
|
/// 1
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display {
|
||||||
|
fn parse(kv: &Kv) -> Result<Display, Error> {
|
||||||
|
let mut r = Display::default();
|
||||||
|
|
||||||
|
if let Some(v) = get(kv, b'i') {
|
||||||
|
r.image_id = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'I') {
|
||||||
|
r.image_number = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'p') {
|
||||||
|
r.placement_id = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'x') {
|
||||||
|
r.x = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'y') {
|
||||||
|
r.y = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'w') {
|
||||||
|
r.width = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'h') {
|
||||||
|
r.height = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'X') {
|
||||||
|
r.x_offset = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'Y') {
|
||||||
|
r.y_offset = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'c') {
|
||||||
|
r.columns = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'r') {
|
||||||
|
r.rows = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'C') {
|
||||||
|
r.cursor_movement = match v {
|
||||||
|
0 => CursorMovement::After,
|
||||||
|
1 => CursorMovement::None,
|
||||||
|
_ => return Err(Error::InvalidFormat),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'U') {
|
||||||
|
r.virtual_placement = match v {
|
||||||
|
0 => false,
|
||||||
|
1 => true,
|
||||||
|
_ => return Err(Error::InvalidFormat),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'z') {
|
||||||
|
// We can bitcast here because of how we parse it earlier.
|
||||||
|
r.z = v as i32;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'P') {
|
||||||
|
r.parent_id = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'Q') {
|
||||||
|
r.parent_placement_id = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'H') {
|
||||||
|
r.horizontal_offset = v as i32;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'V') {
|
||||||
|
r.vertical_offset = v as i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum CompositionMode {
|
||||||
|
/// 0
|
||||||
|
AlphaBlend,
|
||||||
|
/// 1
|
||||||
|
Overwrite,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn composition_mode(v: u32) -> Result<CompositionMode, Error> {
|
||||||
|
match v {
|
||||||
|
0 => Ok(CompositionMode::AlphaBlend),
|
||||||
|
1 => Ok(CompositionMode::Overwrite),
|
||||||
|
_ => Err(Error::InvalidFormat),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub struct AnimationBackground {
|
||||||
|
pub r: u8,
|
||||||
|
pub g: u8,
|
||||||
|
pub b: u8,
|
||||||
|
pub a: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnimationBackground {
|
||||||
|
/// Equivalent of the Zig `@bitCast` from a `u32` (r is the low byte).
|
||||||
|
pub fn from_int(v: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
r: v as u8,
|
||||||
|
g: (v >> 8) as u8,
|
||||||
|
b: (v >> 16) as u8,
|
||||||
|
a: (v >> 24) as u8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct AnimationFrameLoading {
|
||||||
|
/// x
|
||||||
|
pub x: u32,
|
||||||
|
/// y
|
||||||
|
pub y: u32,
|
||||||
|
/// c
|
||||||
|
pub create_frame: u32,
|
||||||
|
/// r
|
||||||
|
pub edit_frame: u32,
|
||||||
|
/// z
|
||||||
|
pub gap_ms: u32,
|
||||||
|
/// X
|
||||||
|
pub composition_mode: CompositionMode,
|
||||||
|
/// Y
|
||||||
|
pub background: AnimationBackground,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AnimationFrameLoading {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
create_frame: 0,
|
||||||
|
edit_frame: 0,
|
||||||
|
gap_ms: 0,
|
||||||
|
composition_mode: CompositionMode::AlphaBlend,
|
||||||
|
background: AnimationBackground::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnimationFrameLoading {
|
||||||
|
fn parse(kv: &Kv) -> Result<Self, Error> {
|
||||||
|
let mut r = Self::default();
|
||||||
|
if let Some(v) = get(kv, b'x') {
|
||||||
|
r.x = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'y') {
|
||||||
|
r.y = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'c') {
|
||||||
|
r.create_frame = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'r') {
|
||||||
|
r.edit_frame = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'z') {
|
||||||
|
r.gap_ms = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'X') {
|
||||||
|
r.composition_mode = composition_mode(v)?;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'Y') {
|
||||||
|
r.background = AnimationBackground::from_int(v);
|
||||||
|
}
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct AnimationFrameComposition {
|
||||||
|
/// c
|
||||||
|
pub frame: u32,
|
||||||
|
/// r
|
||||||
|
pub edit_frame: u32,
|
||||||
|
/// x
|
||||||
|
pub x: u32,
|
||||||
|
/// y
|
||||||
|
pub y: u32,
|
||||||
|
/// w
|
||||||
|
pub width: u32,
|
||||||
|
/// h
|
||||||
|
pub height: u32,
|
||||||
|
/// X
|
||||||
|
pub left_edge: u32,
|
||||||
|
/// Y
|
||||||
|
pub top_edge: u32,
|
||||||
|
/// C
|
||||||
|
pub composition_mode: CompositionMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AnimationFrameComposition {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
frame: 0,
|
||||||
|
edit_frame: 0,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
left_edge: 0,
|
||||||
|
top_edge: 0,
|
||||||
|
composition_mode: CompositionMode::AlphaBlend,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnimationFrameComposition {
|
||||||
|
fn parse(kv: &Kv) -> Result<Self, Error> {
|
||||||
|
let mut r = Self::default();
|
||||||
|
if let Some(v) = get(kv, b'c') {
|
||||||
|
r.frame = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'r') {
|
||||||
|
r.edit_frame = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'x') {
|
||||||
|
r.x = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'y') {
|
||||||
|
r.y = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'w') {
|
||||||
|
r.width = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'h') {
|
||||||
|
r.height = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'X') {
|
||||||
|
r.left_edge = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'Y') {
|
||||||
|
r.top_edge = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'C') {
|
||||||
|
r.composition_mode = composition_mode(v)?;
|
||||||
|
}
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum AnimationAction {
|
||||||
|
/// 0
|
||||||
|
Invalid,
|
||||||
|
/// 1
|
||||||
|
Stop,
|
||||||
|
/// 2
|
||||||
|
RunWait,
|
||||||
|
/// 3
|
||||||
|
Run,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct AnimationControl {
|
||||||
|
/// s
|
||||||
|
pub action: AnimationAction,
|
||||||
|
/// r
|
||||||
|
pub frame: u32,
|
||||||
|
/// z
|
||||||
|
pub gap_ms: u32,
|
||||||
|
/// c
|
||||||
|
pub current_frame: u32,
|
||||||
|
/// v
|
||||||
|
pub loops: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AnimationControl {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
action: AnimationAction::Invalid,
|
||||||
|
frame: 0,
|
||||||
|
gap_ms: 0,
|
||||||
|
current_frame: 0,
|
||||||
|
loops: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnimationControl {
|
||||||
|
fn parse(kv: &Kv) -> Result<Self, Error> {
|
||||||
|
let mut r = Self::default();
|
||||||
|
if let Some(v) = get(kv, b's') {
|
||||||
|
r.action = match v {
|
||||||
|
0 => AnimationAction::Invalid,
|
||||||
|
1 => AnimationAction::Stop,
|
||||||
|
2 => AnimationAction::RunWait,
|
||||||
|
3 => AnimationAction::Run,
|
||||||
|
_ => return Err(Error::InvalidFormat),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'r') {
|
||||||
|
r.frame = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'z') {
|
||||||
|
r.gap_ms = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'c') {
|
||||||
|
r.current_frame = v;
|
||||||
|
}
|
||||||
|
if let Some(v) = get(kv, b'v') {
|
||||||
|
r.loops = v;
|
||||||
|
}
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Delete {
|
||||||
|
/// a/A
|
||||||
|
All(bool),
|
||||||
|
|
||||||
|
/// i/I
|
||||||
|
Id {
|
||||||
|
/// uppercase
|
||||||
|
delete: bool,
|
||||||
|
/// i
|
||||||
|
image_id: u32,
|
||||||
|
/// p
|
||||||
|
placement_id: u32,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// n/N
|
||||||
|
Newest {
|
||||||
|
delete: bool,
|
||||||
|
/// I
|
||||||
|
image_number: u32,
|
||||||
|
/// p
|
||||||
|
placement_id: u32,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// c/C
|
||||||
|
IntersectCursor(bool),
|
||||||
|
|
||||||
|
/// f/F
|
||||||
|
AnimationFrames(bool),
|
||||||
|
|
||||||
|
/// p/P
|
||||||
|
IntersectCell { delete: bool, x: u32, y: u32 },
|
||||||
|
|
||||||
|
/// q/Q
|
||||||
|
IntersectCellZ {
|
||||||
|
delete: bool,
|
||||||
|
x: u32,
|
||||||
|
y: u32,
|
||||||
|
z: i32,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// r/R
|
||||||
|
Range {
|
||||||
|
delete: bool,
|
||||||
|
/// x
|
||||||
|
first: u32,
|
||||||
|
/// y
|
||||||
|
last: u32,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// x/X
|
||||||
|
Column { delete: bool, x: u32 },
|
||||||
|
|
||||||
|
/// y/Y
|
||||||
|
Row { delete: bool, y: u32 },
|
||||||
|
|
||||||
|
/// z/Z
|
||||||
|
Z { delete: bool, z: i32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Delete {
|
||||||
|
fn parse(kv: &Kv) -> Result<Delete, Error> {
|
||||||
|
let what: u8 = match get(kv, b'd') {
|
||||||
|
None => b'a',
|
||||||
|
Some(v) => u8::try_from(v).map_err(|_| Error::InvalidFormat)?,
|
||||||
|
};
|
||||||
|
let up = what.is_ascii_uppercase();
|
||||||
|
let or0 = |k: u8| get(kv, k).unwrap_or(0);
|
||||||
|
|
||||||
|
Ok(match what {
|
||||||
|
b'a' | b'A' => Delete::All(what == b'A'),
|
||||||
|
b'i' | b'I' => Delete::Id {
|
||||||
|
delete: what == b'I',
|
||||||
|
image_id: or0(b'i'),
|
||||||
|
placement_id: or0(b'p'),
|
||||||
|
},
|
||||||
|
b'n' | b'N' => Delete::Newest {
|
||||||
|
delete: what == b'N',
|
||||||
|
image_number: or0(b'I'),
|
||||||
|
placement_id: or0(b'p'),
|
||||||
|
},
|
||||||
|
b'c' | b'C' => Delete::IntersectCursor(what == b'C'),
|
||||||
|
b'f' | b'F' => Delete::AnimationFrames(what == b'F'),
|
||||||
|
b'p' | b'P' => Delete::IntersectCell {
|
||||||
|
delete: up,
|
||||||
|
x: or0(b'x'),
|
||||||
|
y: or0(b'y'),
|
||||||
|
},
|
||||||
|
b'q' | b'Q' => Delete::IntersectCellZ {
|
||||||
|
delete: up,
|
||||||
|
x: or0(b'x'),
|
||||||
|
y: or0(b'y'),
|
||||||
|
// We can bitcast here because of how we parse it earlier.
|
||||||
|
z: or0(b'z') as i32,
|
||||||
|
},
|
||||||
|
b'r' | b'R' => {
|
||||||
|
let x = get(kv, b'x').ok_or(Error::InvalidFormat)?;
|
||||||
|
let y = get(kv, b'y').ok_or(Error::InvalidFormat)?;
|
||||||
|
if x > y {
|
||||||
|
return Err(Error::InvalidFormat);
|
||||||
|
}
|
||||||
|
Delete::Range {
|
||||||
|
delete: up,
|
||||||
|
first: x,
|
||||||
|
last: y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b'x' | b'X' => Delete::Column {
|
||||||
|
delete: up,
|
||||||
|
x: or0(b'x'),
|
||||||
|
},
|
||||||
|
b'y' | b'Y' => Delete::Row {
|
||||||
|
delete: up,
|
||||||
|
y: or0(b'y'),
|
||||||
|
},
|
||||||
|
b'z' | b'Z' => Delete::Z {
|
||||||
|
delete: up,
|
||||||
|
z: or0(b'z') as i32,
|
||||||
|
},
|
||||||
|
_ => return Err(Error::InvalidFormat),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn parse(input: &str) -> Result<Command, Error> {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
for c in input.bytes() {
|
||||||
|
p.feed(c)?;
|
||||||
|
}
|
||||||
|
p.complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transmission_command() {
|
||||||
|
let command = parse("f=24,s=10,v=20").unwrap();
|
||||||
|
let Control::Transmit(v) = command.control else {
|
||||||
|
panic!("expected transmit")
|
||||||
|
};
|
||||||
|
assert_eq!(v.format, TransmissionFormat::Rgb);
|
||||||
|
assert_eq!(v.width, 10);
|
||||||
|
assert_eq!(v.height, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transmission_ignores_m_if_medium_is_not_direct() {
|
||||||
|
let command = parse("a=t,t=t,m=1").unwrap();
|
||||||
|
let Control::Transmit(v) = command.control else {
|
||||||
|
panic!("expected transmit")
|
||||||
|
};
|
||||||
|
assert_eq!(v.medium, TransmissionMedium::TemporaryFile);
|
||||||
|
assert!(!v.more_chunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transmission_respects_m_if_medium_is_direct() {
|
||||||
|
let command = parse("a=t,t=d,m=1").unwrap();
|
||||||
|
let Control::Transmit(v) = command.control else {
|
||||||
|
panic!("expected transmit")
|
||||||
|
};
|
||||||
|
assert_eq!(v.medium, TransmissionMedium::Direct);
|
||||||
|
assert!(v.more_chunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn query_command() {
|
||||||
|
let command = parse("i=31,s=1,v=1,a=q,t=d,f=24;QUFBQQ").unwrap();
|
||||||
|
let Control::Query(v) = command.control else {
|
||||||
|
panic!("expected query")
|
||||||
|
};
|
||||||
|
assert_eq!(v.medium, TransmissionMedium::Direct);
|
||||||
|
assert_eq!(v.width, 1);
|
||||||
|
assert_eq!(v.height, 1);
|
||||||
|
assert_eq!(v.image_id, 31);
|
||||||
|
assert_eq!(command.data, b"AAAA");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display_command() {
|
||||||
|
let command = parse("a=p,U=1,i=31,c=80,r=120").unwrap();
|
||||||
|
let Control::Display(v) = command.control else {
|
||||||
|
panic!("expected display")
|
||||||
|
};
|
||||||
|
assert_eq!(v.columns, 80);
|
||||||
|
assert_eq!(v.rows, 120);
|
||||||
|
assert_eq!(v.image_id, 31);
|
||||||
|
assert!(v.virtual_placement);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_command() {
|
||||||
|
let command = parse("a=d,d=p,x=3,y=4").unwrap();
|
||||||
|
let Control::Delete(Delete::IntersectCell { delete, x, y }) = command.control else {
|
||||||
|
panic!("expected delete intersect_cell")
|
||||||
|
};
|
||||||
|
assert!(!delete);
|
||||||
|
assert_eq!(x, 3);
|
||||||
|
assert_eq!(y, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_control_data() {
|
||||||
|
let command = parse(";QUFBQQ").unwrap();
|
||||||
|
assert!(matches!(command.control, Control::Transmit(_)));
|
||||||
|
assert_eq!(command.data, b"AAAA");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignore_unknown_keys_long() {
|
||||||
|
let command = parse("f=24,s=10,v=20,hello=world").unwrap();
|
||||||
|
let Control::Transmit(v) = command.control else {
|
||||||
|
panic!("expected transmit")
|
||||||
|
};
|
||||||
|
assert_eq!(v.format, TransmissionFormat::Rgb);
|
||||||
|
assert_eq!(v.width, 10);
|
||||||
|
assert_eq!(v.height, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignore_very_long_values() {
|
||||||
|
let command = parse("f=24,s=10,v=2000000000000000000000000000000000000000").unwrap();
|
||||||
|
let Control::Transmit(v) = command.control else {
|
||||||
|
panic!("expected transmit")
|
||||||
|
};
|
||||||
|
assert_eq!(v.format, TransmissionFormat::Rgb);
|
||||||
|
assert_eq!(v.width, 10);
|
||||||
|
assert_eq!(v.height, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ensure_very_large_negative_values_dont_get_skipped() {
|
||||||
|
let command = parse("a=p,i=1,z=-2000000000").unwrap();
|
||||||
|
let Control::Display(v) = command.control else {
|
||||||
|
panic!("expected display")
|
||||||
|
};
|
||||||
|
assert_eq!(v.image_id, 1);
|
||||||
|
assert_eq!(v.z, -2_000_000_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ensure_proper_overflow_error_for_u32() {
|
||||||
|
assert_eq!(parse("a=p,i=10000000000").unwrap_err(), Error::Overflow);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ensure_proper_overflow_error_for_i32() {
|
||||||
|
assert_eq!(parse("a=p,i=1,z=-9999999999").unwrap_err(), Error::Overflow);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_i32_values() {
|
||||||
|
let c = parse("a=p,i=1,z=-1").unwrap();
|
||||||
|
let Control::Display(v) = c.control else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!((v.image_id, v.z), (1, -1));
|
||||||
|
|
||||||
|
let c = parse("a=p,i=1,H=-1").unwrap();
|
||||||
|
let Control::Display(v) = c.control else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!((v.image_id, v.horizontal_offset), (1, -1));
|
||||||
|
|
||||||
|
let c = parse("a=p,i=1,V=-1").unwrap();
|
||||||
|
let Control::Display(v) = c.control else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!((v.image_id, v.vertical_offset), (1, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode(r: &Response) -> Vec<u8> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.encode(&mut buf).unwrap();
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_encode_nothing_without_id_or_image_number() {
|
||||||
|
assert_eq!(encode(&Response::default()), b"");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_encode_with_only_image_id() {
|
||||||
|
let r = Response {
|
||||||
|
id: 4,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert_eq!(encode(&r), b"\x1b_Gi=4;OK\x1b\\");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_encode_with_only_image_number() {
|
||||||
|
let r = Response {
|
||||||
|
image_number: 4,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert_eq!(encode(&r), b"\x1b_GI=4;OK\x1b\\");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_encode_with_image_id_and_number() {
|
||||||
|
let r = Response {
|
||||||
|
id: 12,
|
||||||
|
image_number: 4,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert_eq!(encode(&r), b"\x1b_Gi=12,I=4;OK\x1b\\");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn range(input: &str) -> Result<(bool, u32, u32), Error> {
|
||||||
|
match parse(input)?.control {
|
||||||
|
Control::Delete(Delete::Range {
|
||||||
|
delete,
|
||||||
|
first,
|
||||||
|
last,
|
||||||
|
}) => Ok((delete, first, last)),
|
||||||
|
_ => panic!("expected range"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_range_command_1() {
|
||||||
|
assert_eq!(range("a=d,d=r,x=3,y=4"), Ok((false, 3, 4)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_range_command_2() {
|
||||||
|
assert_eq!(range("a=d,d=R,x=5,y=11"), Ok((true, 5, 11)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_range_command_3() {
|
||||||
|
assert_eq!(range("a=d,d=R,x=5,y=4"), Err(Error::InvalidFormat));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_range_command_4() {
|
||||||
|
assert_eq!(range("a=d,d=R,x=5"), Err(Error::InvalidFormat));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_range_command_5() {
|
||||||
|
assert_eq!(range("a=d,d=R,y=5"), Err(Error::InvalidFormat));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn base64_decode_variants() {
|
||||||
|
assert_eq!(decode_base64(b"aGVsbG8gd29ybGQ=").unwrap(), b"hello world");
|
||||||
|
assert_eq!(decode_base64(b"aGVsbG8gd29ybGQ").unwrap(), b"hello world");
|
||||||
|
assert!(decode_base64(b"!!!!").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_string_works() {
|
||||||
|
let c = Parser::parse_string(b"a=q,i=1;QUFBQQ").unwrap();
|
||||||
|
assert_eq!(c.data, b"AAAA");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -170,4 +170,55 @@ mod tests {
|
|||||||
stack.pop(100);
|
stack.pop(100);
|
||||||
assert_eq!(stack, FlagStack::default());
|
assert_eq!(stack, FlagStack::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flag_stack_push_pop() {
|
||||||
|
let mut stack = FlagStack::default();
|
||||||
|
stack.push(Flags {
|
||||||
|
disambiguate: true,
|
||||||
|
..Flags::DISABLED
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
stack.current(),
|
||||||
|
Flags {
|
||||||
|
disambiguate: true,
|
||||||
|
..Flags::DISABLED
|
||||||
|
}
|
||||||
|
);
|
||||||
|
stack.pop(1);
|
||||||
|
assert_eq!(stack.current(), Flags::DISABLED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flag_stack_pop_big_number() {
|
||||||
|
let mut stack = FlagStack::default();
|
||||||
|
stack.pop(100);
|
||||||
|
assert_eq!(stack.current(), Flags::DISABLED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flag_stack_set() {
|
||||||
|
let mut stack = FlagStack::default();
|
||||||
|
let dis = Flags {
|
||||||
|
disambiguate: true,
|
||||||
|
..Flags::DISABLED
|
||||||
|
};
|
||||||
|
let ev = Flags {
|
||||||
|
report_events: true,
|
||||||
|
..Flags::DISABLED
|
||||||
|
};
|
||||||
|
stack.set(SetMode::Set, dis);
|
||||||
|
assert_eq!(stack.current(), dis);
|
||||||
|
stack.set(SetMode::Or, ev);
|
||||||
|
assert_eq!(
|
||||||
|
stack.current(),
|
||||||
|
Flags {
|
||||||
|
disambiguate: true,
|
||||||
|
report_events: true,
|
||||||
|
..Flags::DISABLED
|
||||||
|
}
|
||||||
|
);
|
||||||
|
stack.set(SetMode::Not, ev);
|
||||||
|
assert_eq!(stack.current(), dis);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
//! Kitty protocol types (ZIG/src/terminal/kitty.zig).
|
//! Kitty protocol types (ZIG/src/terminal/kitty.zig).
|
||||||
|
|
||||||
|
pub mod color;
|
||||||
|
pub mod graphics_command;
|
||||||
pub mod key;
|
pub mod key;
|
||||||
@@ -1,5 +1,20 @@
|
|||||||
//! ghostty-vt: VT parsing layer of Ghostty (parser, OSC/CSI/DCS/APC, SGR,
|
//! ghostty-vt: VT parsing layer of Ghostty (parser, OSC/CSI/DCS/APC, SGR,
|
||||||
//! colors, modes, stream). See docs/rewrite/ARCHITECTURE.md.
|
//! colors, modes, stream). See docs/rewrite/ARCHITECTURE.md.
|
||||||
|
|
||||||
|
pub mod ansi;
|
||||||
|
pub mod apc;
|
||||||
|
pub mod charsets;
|
||||||
|
pub mod color;
|
||||||
|
pub mod csi;
|
||||||
|
pub mod dcs;
|
||||||
|
pub mod device_status;
|
||||||
pub mod kitty;
|
pub mod kitty;
|
||||||
|
pub mod modes;
|
||||||
|
pub mod mouse_shape;
|
||||||
|
pub mod osc;
|
||||||
|
pub mod parse_table;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
|
pub mod sgr;
|
||||||
|
pub mod tmux;
|
||||||
|
pub mod utf8_decoder;
|
||||||
|
pub mod x11_color;
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
//! Terminal modes (ZIG/src/terminal/modes.zig): an enum of supported modes,
|
||||||
|
//! a packed bit storage for mode values, and a state struct that handles
|
||||||
|
//! save/restore.
|
||||||
|
//!
|
||||||
|
//! In Zig the enum, packed struct and lookup are generated at comptime from
|
||||||
|
//! one table. Here the single [`ENTRIES`] table drives everything and the
|
||||||
|
//! `Mode` discriminant doubles as the bit index in [`ModePacked`].
|
||||||
|
|
||||||
|
/// A struct that maintains the state of all the settable modes.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub struct ModeState {
|
||||||
|
/// The values of the current modes.
|
||||||
|
pub values: ModePacked,
|
||||||
|
/// The saved values. We only allow saving each mode once.
|
||||||
|
/// This is in line with other terminals that implement XTSAVE
|
||||||
|
/// and XTRESTORE. We can improve this in the future if it becomes
|
||||||
|
/// a real-world issue but we need to be aware of a DoS vector.
|
||||||
|
pub saved: ModePacked,
|
||||||
|
/// The default values for the modes. This is used to reset
|
||||||
|
/// the modes to their default values during reset.
|
||||||
|
pub default: ModePacked,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModeState {
|
||||||
|
/// Reset the modes to their default values. This also clears the
|
||||||
|
/// saved state.
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.values = self.default;
|
||||||
|
self.saved = ModePacked::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a mode to a value.
|
||||||
|
pub fn set(&mut self, mode: Mode, value: bool) {
|
||||||
|
self.values.set(mode, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the value of a mode.
|
||||||
|
pub fn get(&self, mode: Mode) -> bool {
|
||||||
|
self.values.get(mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save the state of the given mode. This can then be restored
|
||||||
|
/// with restore. This will only be accurate if the previous
|
||||||
|
/// mode was saved exactly once and not restored. Otherwise this
|
||||||
|
/// will just keep restoring the last stored value in memory.
|
||||||
|
pub fn save(&mut self, mode: Mode) {
|
||||||
|
let v = self.values.get(mode);
|
||||||
|
self.saved.set(mode, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See save. This will return the restored value.
|
||||||
|
pub fn restore(&mut self, mode: Mode) -> bool {
|
||||||
|
let v = self.saved.get(mode);
|
||||||
|
self.values.set(mode, v);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Packed storage of all the settable modes (one bit per mode). This
|
||||||
|
/// shouldn't be used directly but rather through the [`ModeState`] struct.
|
||||||
|
/// Defaults match each entry's default value.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct ModePacked(u64);
|
||||||
|
|
||||||
|
impl Default for ModePacked {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut bits = 0u64;
|
||||||
|
for e in ENTRIES {
|
||||||
|
if e.default {
|
||||||
|
bits |= 1 << (e.mode as u64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self(bits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModePacked {
|
||||||
|
pub fn get(&self, mode: Mode) -> bool {
|
||||||
|
self.0 & (1 << (mode as u64)) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&mut self, mode: Mode, value: bool) {
|
||||||
|
if value {
|
||||||
|
self.0 |= 1 << (mode as u64);
|
||||||
|
} else {
|
||||||
|
self.0 &= !(1 << (mode as u64));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Raw bits.
|
||||||
|
pub fn bits(&self) -> u64 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The available modes. See [`ENTRIES`] for numeric values.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum Mode {
|
||||||
|
DisableKeyboard,
|
||||||
|
Insert,
|
||||||
|
SendReceiveMode,
|
||||||
|
Linefeed,
|
||||||
|
CursorKeys,
|
||||||
|
M132Column,
|
||||||
|
SlowScroll,
|
||||||
|
ReverseColors,
|
||||||
|
Origin,
|
||||||
|
Wraparound,
|
||||||
|
Autorepeat,
|
||||||
|
MouseEventX10,
|
||||||
|
CursorBlinking,
|
||||||
|
CursorVisible,
|
||||||
|
EnableMode3,
|
||||||
|
ReverseWrap,
|
||||||
|
AltScreenLegacy,
|
||||||
|
KeypadKeys,
|
||||||
|
EnableLeftAndRightMargin,
|
||||||
|
MouseEventNormal,
|
||||||
|
MouseEventButton,
|
||||||
|
MouseEventAny,
|
||||||
|
FocusEvent,
|
||||||
|
MouseFormatUtf8,
|
||||||
|
MouseFormatSgr,
|
||||||
|
MouseAlternateScroll,
|
||||||
|
MouseFormatUrxvt,
|
||||||
|
MouseFormatSgrPixels,
|
||||||
|
IgnoreKeypadWithNumlock,
|
||||||
|
AltEscPrefix,
|
||||||
|
AltSendsEscape,
|
||||||
|
ReverseWrapExtended,
|
||||||
|
AltScreen,
|
||||||
|
SaveCursor,
|
||||||
|
AltScreenSaveCursorClearEnter,
|
||||||
|
BracketedPaste,
|
||||||
|
SynchronizedOutput,
|
||||||
|
GraphemeCluster,
|
||||||
|
ReportColorScheme,
|
||||||
|
InBandSizeReports,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mode {
|
||||||
|
/// The entry for this mode.
|
||||||
|
pub fn entry(self) -> &'static Entry {
|
||||||
|
&ENTRIES[self as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The numeric value of the mode.
|
||||||
|
pub fn value(self) -> u16 {
|
||||||
|
self.entry().value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True if this is an ANSI mode, false if its a DEC mode (?-prefixed).
|
||||||
|
pub fn ansi(self) -> bool {
|
||||||
|
self.entry().ansi
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The snake_case name of the mode (as in the Zig tag name).
|
||||||
|
pub fn name(self) -> &'static str {
|
||||||
|
self.entry().name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up a mode from its numeric value and ANSI-ness.
|
||||||
|
pub fn mode_from_int(v: u16, ansi: bool) -> Option<Mode> {
|
||||||
|
ENTRIES
|
||||||
|
.iter()
|
||||||
|
.find(|e| !e.disabled && e.value == v && e.ansi == ansi)
|
||||||
|
.map(|e| e.mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single entry of a possible mode we support.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Entry {
|
||||||
|
pub mode: Mode,
|
||||||
|
pub name: &'static str,
|
||||||
|
pub value: u16,
|
||||||
|
pub default: bool,
|
||||||
|
/// True if this is an ANSI mode, false if its a DEC mode (?-prefixed).
|
||||||
|
pub ansi: bool,
|
||||||
|
/// If true, this mode is disabled and Ghostty will not allow it to be
|
||||||
|
/// set or queried. The mode enum still has it, allowing Ghostty developers
|
||||||
|
/// to develop a mode without exposing it to real users.
|
||||||
|
pub disabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
const fn Entry(mode: Mode, name: &'static str, value: u16, default: bool, ansi: bool) -> Entry {
|
||||||
|
Entry {
|
||||||
|
mode,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
default,
|
||||||
|
ansi,
|
||||||
|
disabled: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The full list of available entries, in `Mode` declaration order.
|
||||||
|
pub const ENTRIES: &[Entry; 40] = &[
|
||||||
|
Entry(Mode::DisableKeyboard, "disable_keyboard", 2, false, true),
|
||||||
|
Entry(Mode::Insert, "insert", 4, false, true),
|
||||||
|
Entry(Mode::SendReceiveMode, "send_receive_mode", 12, true, true),
|
||||||
|
Entry(Mode::Linefeed, "linefeed", 20, false, true),
|
||||||
|
Entry(Mode::CursorKeys, "cursor_keys", 1, false, false),
|
||||||
|
Entry(Mode::M132Column, "132_column", 3, false, false),
|
||||||
|
Entry(Mode::SlowScroll, "slow_scroll", 4, false, false),
|
||||||
|
Entry(Mode::ReverseColors, "reverse_colors", 5, false, false),
|
||||||
|
Entry(Mode::Origin, "origin", 6, false, false),
|
||||||
|
Entry(Mode::Wraparound, "wraparound", 7, true, false),
|
||||||
|
Entry(Mode::Autorepeat, "autorepeat", 8, false, false),
|
||||||
|
Entry(Mode::MouseEventX10, "mouse_event_x10", 9, false, false),
|
||||||
|
Entry(Mode::CursorBlinking, "cursor_blinking", 12, false, false),
|
||||||
|
Entry(Mode::CursorVisible, "cursor_visible", 25, true, false),
|
||||||
|
Entry(Mode::EnableMode3, "enable_mode_3", 40, false, false),
|
||||||
|
Entry(Mode::ReverseWrap, "reverse_wrap", 45, false, false),
|
||||||
|
Entry(Mode::AltScreenLegacy, "alt_screen_legacy", 47, false, false),
|
||||||
|
Entry(Mode::KeypadKeys, "keypad_keys", 66, false, false),
|
||||||
|
Entry(
|
||||||
|
Mode::EnableLeftAndRightMargin,
|
||||||
|
"enable_left_and_right_margin",
|
||||||
|
69,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
Entry(
|
||||||
|
Mode::MouseEventNormal,
|
||||||
|
"mouse_event_normal",
|
||||||
|
1000,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
Entry(
|
||||||
|
Mode::MouseEventButton,
|
||||||
|
"mouse_event_button",
|
||||||
|
1002,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
Entry(Mode::MouseEventAny, "mouse_event_any", 1003, false, false),
|
||||||
|
Entry(Mode::FocusEvent, "focus_event", 1004, false, false),
|
||||||
|
Entry(
|
||||||
|
Mode::MouseFormatUtf8,
|
||||||
|
"mouse_format_utf8",
|
||||||
|
1005,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
Entry(Mode::MouseFormatSgr, "mouse_format_sgr", 1006, false, false),
|
||||||
|
Entry(
|
||||||
|
Mode::MouseAlternateScroll,
|
||||||
|
"mouse_alternate_scroll",
|
||||||
|
1007,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
Entry(
|
||||||
|
Mode::MouseFormatUrxvt,
|
||||||
|
"mouse_format_urxvt",
|
||||||
|
1015,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
Entry(
|
||||||
|
Mode::MouseFormatSgrPixels,
|
||||||
|
"mouse_format_sgr_pixels",
|
||||||
|
1016,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
Entry(
|
||||||
|
Mode::IgnoreKeypadWithNumlock,
|
||||||
|
"ignore_keypad_with_numlock",
|
||||||
|
1035,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
Entry(Mode::AltEscPrefix, "alt_esc_prefix", 1036, true, false),
|
||||||
|
Entry(Mode::AltSendsEscape, "alt_sends_escape", 1039, false, false),
|
||||||
|
Entry(
|
||||||
|
Mode::ReverseWrapExtended,
|
||||||
|
"reverse_wrap_extended",
|
||||||
|
1045,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
Entry(Mode::AltScreen, "alt_screen", 1047, false, false),
|
||||||
|
Entry(Mode::SaveCursor, "save_cursor", 1048, false, false),
|
||||||
|
Entry(
|
||||||
|
Mode::AltScreenSaveCursorClearEnter,
|
||||||
|
"alt_screen_save_cursor_clear_enter",
|
||||||
|
1049,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
Entry(Mode::BracketedPaste, "bracketed_paste", 2004, false, false),
|
||||||
|
Entry(
|
||||||
|
Mode::SynchronizedOutput,
|
||||||
|
"synchronized_output",
|
||||||
|
2026,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
Entry(
|
||||||
|
Mode::GraphemeCluster,
|
||||||
|
"grapheme_cluster",
|
||||||
|
2027,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
Entry(
|
||||||
|
Mode::ReportColorScheme,
|
||||||
|
"report_color_scheme",
|
||||||
|
2031,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
Entry(
|
||||||
|
Mode::InBandSizeReports,
|
||||||
|
"in_band_size_reports",
|
||||||
|
2048,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn entries_in_order() {
|
||||||
|
for (i, e) in ENTRIES.iter().enumerate() {
|
||||||
|
assert_eq!(e.mode as usize, i);
|
||||||
|
}
|
||||||
|
assert!(ENTRIES.len() <= 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mode_from_int_lookup() {
|
||||||
|
assert_eq!(mode_from_int(4, true), Some(Mode::Insert));
|
||||||
|
assert_eq!(mode_from_int(9, true), None);
|
||||||
|
assert_eq!(mode_from_int(9, false), Some(Mode::MouseEventX10));
|
||||||
|
assert_eq!(mode_from_int(14, true), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mode_state() {
|
||||||
|
let mut state = ModeState::default();
|
||||||
|
// Normal set/get
|
||||||
|
assert!(!state.get(Mode::CursorKeys));
|
||||||
|
state.set(Mode::CursorKeys, true);
|
||||||
|
assert!(state.get(Mode::CursorKeys));
|
||||||
|
|
||||||
|
// Save/restore
|
||||||
|
state.save(Mode::CursorKeys);
|
||||||
|
state.set(Mode::CursorKeys, false);
|
||||||
|
assert!(!state.get(Mode::CursorKeys));
|
||||||
|
assert!(state.restore(Mode::CursorKeys));
|
||||||
|
assert!(state.get(Mode::CursorKeys));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn defaults_and_reset() {
|
||||||
|
let mut state = ModeState::default();
|
||||||
|
assert!(state.get(Mode::Wraparound));
|
||||||
|
assert!(state.get(Mode::CursorVisible));
|
||||||
|
assert!(state.get(Mode::SendReceiveMode));
|
||||||
|
assert!(!state.get(Mode::Origin));
|
||||||
|
state.set(Mode::Wraparound, false);
|
||||||
|
state.reset();
|
||||||
|
assert!(state.get(Mode::Wraparound));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mode_tag_order() {
|
||||||
|
// Mirrors Zig ModeTag "order": value in low bits, ansi in the top bit.
|
||||||
|
assert_eq!(Mode::CursorKeys.value(), 1);
|
||||||
|
assert!(!Mode::CursorKeys.ansi());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn packed_size() {
|
||||||
|
// Zig asserts @sizeOf(ModePacked) == 8.
|
||||||
|
assert_eq!(std::mem::size_of::<ModePacked>(), 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
//! Mouse cursor shapes (ZIG/src/terminal/mouse_shape.zig).
|
||||||
|
|
||||||
|
/// The possible cursor shapes. Not all app runtimes support these shapes.
|
||||||
|
/// The shapes are always based on the W3C supported cursor styles so we
|
||||||
|
/// can have a cross platform list.
|
||||||
|
// Must be kept in sync with ghostty_cursor_shape_e
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum MouseShape {
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
ContextMenu,
|
||||||
|
Help,
|
||||||
|
Pointer,
|
||||||
|
Progress,
|
||||||
|
Wait,
|
||||||
|
Cell,
|
||||||
|
Crosshair,
|
||||||
|
Text,
|
||||||
|
VerticalText,
|
||||||
|
Alias,
|
||||||
|
Copy,
|
||||||
|
Move,
|
||||||
|
NoDrop,
|
||||||
|
NotAllowed,
|
||||||
|
Grab,
|
||||||
|
Grabbing,
|
||||||
|
AllScroll,
|
||||||
|
ColResize,
|
||||||
|
RowResize,
|
||||||
|
NResize,
|
||||||
|
EResize,
|
||||||
|
SResize,
|
||||||
|
WResize,
|
||||||
|
NeResize,
|
||||||
|
NwResize,
|
||||||
|
SeResize,
|
||||||
|
SwResize,
|
||||||
|
EwResize,
|
||||||
|
NsResize,
|
||||||
|
NeswResize,
|
||||||
|
NwseResize,
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MouseShape {
|
||||||
|
/// Build cursor shape from string or None if its unknown.
|
||||||
|
pub fn from_string(v: &str) -> Option<MouseShape> {
|
||||||
|
Some(match v {
|
||||||
|
"default" => MouseShape::Default,
|
||||||
|
"context-menu" => MouseShape::ContextMenu,
|
||||||
|
"help" => MouseShape::Help,
|
||||||
|
"pointer" => MouseShape::Pointer,
|
||||||
|
"progress" => MouseShape::Progress,
|
||||||
|
"wait" => MouseShape::Wait,
|
||||||
|
"cell" => MouseShape::Cell,
|
||||||
|
"crosshair" => MouseShape::Crosshair,
|
||||||
|
"text" => MouseShape::Text,
|
||||||
|
"vertical-text" => MouseShape::VerticalText,
|
||||||
|
"alias" => MouseShape::Alias,
|
||||||
|
"copy" => MouseShape::Copy,
|
||||||
|
"move" => MouseShape::Move,
|
||||||
|
"no-drop" => MouseShape::NoDrop,
|
||||||
|
"not-allowed" => MouseShape::NotAllowed,
|
||||||
|
"grab" => MouseShape::Grab,
|
||||||
|
"grabbing" => MouseShape::Grabbing,
|
||||||
|
"all-scroll" => MouseShape::AllScroll,
|
||||||
|
"col-resize" => MouseShape::ColResize,
|
||||||
|
"row-resize" => MouseShape::RowResize,
|
||||||
|
"n-resize" => MouseShape::NResize,
|
||||||
|
"e-resize" => MouseShape::EResize,
|
||||||
|
"s-resize" => MouseShape::SResize,
|
||||||
|
"w-resize" => MouseShape::WResize,
|
||||||
|
"ne-resize" => MouseShape::NeResize,
|
||||||
|
"nw-resize" => MouseShape::NwResize,
|
||||||
|
"se-resize" => MouseShape::SeResize,
|
||||||
|
"sw-resize" => MouseShape::SwResize,
|
||||||
|
"ew-resize" => MouseShape::EwResize,
|
||||||
|
"ns-resize" => MouseShape::NsResize,
|
||||||
|
"nesw-resize" => MouseShape::NeswResize,
|
||||||
|
"nwse-resize" => MouseShape::NwseResize,
|
||||||
|
"zoom-in" => MouseShape::ZoomIn,
|
||||||
|
"zoom-out" => MouseShape::ZoomOut,
|
||||||
|
"left_ptr" => MouseShape::Default,
|
||||||
|
"question_arrow" => MouseShape::Help,
|
||||||
|
"hand" => MouseShape::Pointer,
|
||||||
|
"left_ptr_watch" => MouseShape::Progress,
|
||||||
|
"watch" => MouseShape::Wait,
|
||||||
|
"cross" => MouseShape::Crosshair,
|
||||||
|
"xterm" => MouseShape::Text,
|
||||||
|
"dnd-link" => MouseShape::Alias,
|
||||||
|
"dnd-copy" => MouseShape::Copy,
|
||||||
|
"dnd-move" => MouseShape::Move,
|
||||||
|
"dnd-no-drop" => MouseShape::NoDrop,
|
||||||
|
"crossed_circle" => MouseShape::NotAllowed,
|
||||||
|
"hand1" => MouseShape::Grab,
|
||||||
|
"right_side" => MouseShape::EResize,
|
||||||
|
"top_side" => MouseShape::NResize,
|
||||||
|
"top_right_corner" => MouseShape::NeResize,
|
||||||
|
"top_left_corner" => MouseShape::NwResize,
|
||||||
|
"bottom_side" => MouseShape::SResize,
|
||||||
|
"bottom_right_corner" => MouseShape::SeResize,
|
||||||
|
"bottom_left_corner" => MouseShape::SwResize,
|
||||||
|
"left_side" => MouseShape::WResize,
|
||||||
|
"fleur" => MouseShape::AllScroll,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cursor_shape_from_string() {
|
||||||
|
assert_eq!(
|
||||||
|
MouseShape::from_string("default"),
|
||||||
|
Some(MouseShape::Default)
|
||||||
|
);
|
||||||
|
assert_eq!(MouseShape::from_string("xterm"), Some(MouseShape::Text));
|
||||||
|
assert_eq!(MouseShape::from_string("nope"), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
//! Specialized encodings used in some OSC protocols
|
||||||
|
//! (ZIG/src/terminal/osc/encoding.zig).
|
||||||
|
|
||||||
|
/// Kitty defines "Escape code safe UTF-8" as valid UTF-8 with the
|
||||||
|
/// additional requirement of not containing any C0 escape codes
|
||||||
|
/// (0x00-0x1f), DEL (0x7f) and C1 escape codes (0x80-0x9f).
|
||||||
|
///
|
||||||
|
/// Used by OSC 66 (text sizing) and OSC 99 (Kitty notifications).
|
||||||
|
///
|
||||||
|
/// See: https://sw.kovidgoyal.net/kitty/desktop-notifications/#safe-utf8
|
||||||
|
pub fn is_safe_utf8(s: &[u8]) -> bool {
|
||||||
|
let Ok(s) = std::str::from_utf8(s) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
!s.chars()
|
||||||
|
.any(|c| matches!(c as u32, 0x00..=0x1f | 0x7f | 0x80..=0x9f))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_safe_utf8_cases() {
|
||||||
|
assert!(is_safe_utf8(b"Hello world!"));
|
||||||
|
assert!(is_safe_utf8("安全的ユニコード☀️".as_bytes()));
|
||||||
|
assert!(!is_safe_utf8(b"No linebreaks\nallowed"));
|
||||||
|
assert!(!is_safe_utf8(b"\x07no bells"));
|
||||||
|
assert!(!is_safe_utf8(b"\x1b]9;no OSCs\x1b\\\x1b[m"));
|
||||||
|
assert!(!is_safe_utf8(b"\x9f8-bit escapes are clever, but no"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,741 @@
|
|||||||
|
//! OSC (Operating System Command) related functions and types
|
||||||
|
//! (ZIG/src/terminal/osc.zig).
|
||||||
|
//!
|
||||||
|
//! OSC is another set of control sequences for terminal programs that start with
|
||||||
|
//! "ESC ]". Unlike CSI or standard ESC sequences, they may contain strings
|
||||||
|
//! and other irregular formatting so a dedicated parser is created to handle it.
|
||||||
|
//!
|
||||||
|
//! Rust port notes (W1):
|
||||||
|
//! - Zig `[:0]const u8` fields become owned `Vec<u8>` (no NUL terminator, not
|
||||||
|
//! necessarily valid UTF-8).
|
||||||
|
//! - Zig's "fixed buffer vs. allocating writer" is a single `Vec<u8>` with an
|
||||||
|
//! optional maximum length (`MAX_BUF`, which always reserves one byte for
|
||||||
|
//! the NUL terminator the Zig code writes). The Zig `alloc: ?Allocator` is
|
||||||
|
//! `alloc: bool` ("an allocator is available").
|
||||||
|
//! - `Parser::end` returns an owned `Command` instead of a pointer into the
|
||||||
|
//! parser.
|
||||||
|
|
||||||
|
pub mod encoding;
|
||||||
|
pub mod parsers;
|
||||||
|
|
||||||
|
pub use parsers::color;
|
||||||
|
pub use parsers::semantic_prompt;
|
||||||
|
|
||||||
|
use crate::kitty::color as kitty_color;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Command {
|
||||||
|
/// This generally shouldn't ever be set except as an initial zero value.
|
||||||
|
/// Ignore it.
|
||||||
|
Invalid,
|
||||||
|
|
||||||
|
/// Set the window title of the terminal
|
||||||
|
///
|
||||||
|
/// If title mode 0 is set text is expect to be hex encoded (i.e. utf-8
|
||||||
|
/// with each code unit further encoded with two hex digits).
|
||||||
|
///
|
||||||
|
/// If title mode 2 is set or the terminal is setup for unconditional
|
||||||
|
/// utf-8 titles text is interpreted as utf-8. Else text is interpreted
|
||||||
|
/// as latin1.
|
||||||
|
ChangeWindowTitle(Vec<u8>),
|
||||||
|
|
||||||
|
/// Set the icon of the terminal window. The name of the icon is not
|
||||||
|
/// well defined, so this is currently ignored by Ghostty at the time
|
||||||
|
/// of writing this. We just parse it so that we don't get parse errors
|
||||||
|
/// in the log.
|
||||||
|
ChangeWindowIcon(Vec<u8>),
|
||||||
|
|
||||||
|
/// Semantic prompt command: https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md
|
||||||
|
SemanticPrompt(SemanticPrompt),
|
||||||
|
|
||||||
|
/// Set or get clipboard contents. If data is null, then the current
|
||||||
|
/// clipboard contents are sent to the pty. If data is set, this
|
||||||
|
/// contents is set on the clipboard.
|
||||||
|
ClipboardContents {
|
||||||
|
kind: u8,
|
||||||
|
data: Vec<u8>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// OSC 7. Reports the current working directory of the shell. This is
|
||||||
|
/// a moderately flawed escape sequence but one that many major terminals
|
||||||
|
/// support so we also support it. To understand the flaws, read through
|
||||||
|
/// this terminal-wg issue: https://gitlab.freedesktop.org/terminal-wg/specifications/-/issues/20
|
||||||
|
ReportPwd {
|
||||||
|
/// The reported pwd value. This is not checked for validity. It should
|
||||||
|
/// be a file URL but it is up to the caller to utilize this value.
|
||||||
|
value: Vec<u8>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// OSC 22. Set the mouse shape. There doesn't seem to be a standard
|
||||||
|
/// naming scheme for cursors but it looks like terminals such as Foot
|
||||||
|
/// are moving towards using the W3C CSS cursor names. For OSC parsing,
|
||||||
|
/// we just parse whatever string is given.
|
||||||
|
MouseShape {
|
||||||
|
value: Vec<u8>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// OSC color operations to set, reset, or report color settings. Some OSCs
|
||||||
|
/// allow multiple operations to be specified in a single OSC so we need a
|
||||||
|
/// list-like datastructure to manage them.
|
||||||
|
///
|
||||||
|
/// Currently, these OSCs are handled by `color_operation`:
|
||||||
|
///
|
||||||
|
/// 4, 5, 10-19, 104, 105, 110-119
|
||||||
|
ColorOperation {
|
||||||
|
op: color::Operation,
|
||||||
|
requests: color::List,
|
||||||
|
terminator: Terminator,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Kitty color protocol, OSC 21
|
||||||
|
/// https://sw.kovidgoyal.net/kitty/color-stack/#id1
|
||||||
|
KittyColorProtocol(kitty_color::OSC),
|
||||||
|
|
||||||
|
/// Show a desktop notification (OSC 9 or OSC 777)
|
||||||
|
ShowDesktopNotification {
|
||||||
|
title: Vec<u8>,
|
||||||
|
body: Vec<u8>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Start a hyperlink (OSC 8)
|
||||||
|
HyperlinkStart {
|
||||||
|
id: Option<Vec<u8>>,
|
||||||
|
uri: Vec<u8>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// End a hyperlink (OSC 8)
|
||||||
|
HyperlinkEnd,
|
||||||
|
|
||||||
|
/// ConEmu sleep (OSC 9;1)
|
||||||
|
ConemuSleep {
|
||||||
|
duration_ms: u16,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// ConEmu show GUI message box (OSC 9;2)
|
||||||
|
ConemuShowMessageBox(Vec<u8>),
|
||||||
|
|
||||||
|
/// ConEmu change tab title (OSC 9;3)
|
||||||
|
ConemuChangeTabTitle(ConemuChangeTabTitle),
|
||||||
|
|
||||||
|
/// ConEmu progress report (OSC 9;4)
|
||||||
|
ConemuProgressReport(ProgressReport),
|
||||||
|
|
||||||
|
/// ConEmu wait input (OSC 9;5)
|
||||||
|
ConemuWaitInput,
|
||||||
|
|
||||||
|
/// ConEmu GUI macro (OSC 9;6)
|
||||||
|
ConemuGuimacro(Vec<u8>),
|
||||||
|
|
||||||
|
/// ConEmu run process (OSC 9;7)
|
||||||
|
ConemuRunProcess(Vec<u8>),
|
||||||
|
|
||||||
|
/// ConEmu output environment variable (OSC 9;8)
|
||||||
|
ConemuOutputEnvironmentVariable(Vec<u8>),
|
||||||
|
|
||||||
|
/// ConEmu XTerm keyboard and output emulation (OSC 9;10)
|
||||||
|
/// https://conemu.github.io/en/TerminalModes.html
|
||||||
|
ConemuXtermEmulation {
|
||||||
|
/// None => do not change
|
||||||
|
/// Some(false) => turn off
|
||||||
|
/// Some(true) => turn on
|
||||||
|
keyboard: Option<bool>,
|
||||||
|
/// None => do not change
|
||||||
|
/// Some(false) => turn off
|
||||||
|
/// Some(true) => turn on
|
||||||
|
output: Option<bool>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// ConEmu comment (OSC 9;11)
|
||||||
|
ConemuComment(Vec<u8>),
|
||||||
|
|
||||||
|
/// Kitty text sizing protocol (OSC 66)
|
||||||
|
KittyTextSizing(parsers::kitty_text_sizing::OSC),
|
||||||
|
|
||||||
|
KittyClipboardProtocol(KittyClipboardProtocol),
|
||||||
|
|
||||||
|
/// OSC 3008. Hierarchical context signalling (UAPI spec).
|
||||||
|
/// https://uapi-group.org/specifications/specs/osc_context/
|
||||||
|
ContextSignal(parsers::context_signal::Command),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SemanticPrompt = parsers::semantic_prompt::Command;
|
||||||
|
pub type KittyClipboardProtocol = parsers::kitty_clipboard_protocol::OSC;
|
||||||
|
|
||||||
|
/// Payload of `Command::ConemuChangeTabTitle`.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ConemuChangeTabTitle {
|
||||||
|
Reset,
|
||||||
|
Value(Vec<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The discriminant of a [`Command`] (Zig `Command.Key`). Order matches Zig.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum Key {
|
||||||
|
Invalid,
|
||||||
|
ChangeWindowTitle,
|
||||||
|
ChangeWindowIcon,
|
||||||
|
SemanticPrompt,
|
||||||
|
ClipboardContents,
|
||||||
|
ReportPwd,
|
||||||
|
MouseShape,
|
||||||
|
ColorOperation,
|
||||||
|
KittyColorProtocol,
|
||||||
|
ShowDesktopNotification,
|
||||||
|
HyperlinkStart,
|
||||||
|
HyperlinkEnd,
|
||||||
|
ConemuSleep,
|
||||||
|
ConemuShowMessageBox,
|
||||||
|
ConemuChangeTabTitle,
|
||||||
|
ConemuProgressReport,
|
||||||
|
ConemuWaitInput,
|
||||||
|
ConemuGuimacro,
|
||||||
|
ConemuRunProcess,
|
||||||
|
ConemuOutputEnvironmentVariable,
|
||||||
|
ConemuXtermEmulation,
|
||||||
|
ConemuComment,
|
||||||
|
KittyTextSizing,
|
||||||
|
KittyClipboardProtocol,
|
||||||
|
ContextSignal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
|
pub fn key(&self) -> Key {
|
||||||
|
match self {
|
||||||
|
Command::Invalid => Key::Invalid,
|
||||||
|
Command::ChangeWindowTitle(_) => Key::ChangeWindowTitle,
|
||||||
|
Command::ChangeWindowIcon(_) => Key::ChangeWindowIcon,
|
||||||
|
Command::SemanticPrompt(_) => Key::SemanticPrompt,
|
||||||
|
Command::ClipboardContents { .. } => Key::ClipboardContents,
|
||||||
|
Command::ReportPwd { .. } => Key::ReportPwd,
|
||||||
|
Command::MouseShape { .. } => Key::MouseShape,
|
||||||
|
Command::ColorOperation { .. } => Key::ColorOperation,
|
||||||
|
Command::KittyColorProtocol(_) => Key::KittyColorProtocol,
|
||||||
|
Command::ShowDesktopNotification { .. } => Key::ShowDesktopNotification,
|
||||||
|
Command::HyperlinkStart { .. } => Key::HyperlinkStart,
|
||||||
|
Command::HyperlinkEnd => Key::HyperlinkEnd,
|
||||||
|
Command::ConemuSleep { .. } => Key::ConemuSleep,
|
||||||
|
Command::ConemuShowMessageBox(_) => Key::ConemuShowMessageBox,
|
||||||
|
Command::ConemuChangeTabTitle(_) => Key::ConemuChangeTabTitle,
|
||||||
|
Command::ConemuProgressReport(_) => Key::ConemuProgressReport,
|
||||||
|
Command::ConemuWaitInput => Key::ConemuWaitInput,
|
||||||
|
Command::ConemuGuimacro(_) => Key::ConemuGuimacro,
|
||||||
|
Command::ConemuRunProcess(_) => Key::ConemuRunProcess,
|
||||||
|
Command::ConemuOutputEnvironmentVariable(_) => Key::ConemuOutputEnvironmentVariable,
|
||||||
|
Command::ConemuXtermEmulation { .. } => Key::ConemuXtermEmulation,
|
||||||
|
Command::ConemuComment(_) => Key::ConemuComment,
|
||||||
|
Command::KittyTextSizing(_) => Key::KittyTextSizing,
|
||||||
|
Command::KittyClipboardProtocol(_) => Key::KittyClipboardProtocol,
|
||||||
|
Command::ContextSignal(_) => Key::ContextSignal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ConEmu progress report (OSC 9;4).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct ProgressReport {
|
||||||
|
pub state: ProgressState,
|
||||||
|
pub progress: Option<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ProgressState {
|
||||||
|
Remove,
|
||||||
|
Set,
|
||||||
|
Error,
|
||||||
|
Indeterminate,
|
||||||
|
Pause,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The terminator used to end an OSC command. For OSC commands that demand
|
||||||
|
/// a response, we try to match the terminator used in the request since that
|
||||||
|
/// is most likely to be accepted by the calling program.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum Terminator {
|
||||||
|
/// The preferred string terminator is ESC followed by \
|
||||||
|
#[default]
|
||||||
|
St,
|
||||||
|
|
||||||
|
/// Some applications and terminals use BELL (0x07) as the string terminator.
|
||||||
|
Bel,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Terminator {
|
||||||
|
/// Initialize the terminator based on the last byte seen. If the
|
||||||
|
/// last byte is a BEL then we use BEL, otherwise we just assume ST.
|
||||||
|
pub fn init(ch: Option<u8>) -> Terminator {
|
||||||
|
match ch {
|
||||||
|
Some(0x07) => Terminator::Bel,
|
||||||
|
_ => Terminator::St,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The terminator as a string.
|
||||||
|
pub fn string(self) -> &'static [u8] {
|
||||||
|
match self {
|
||||||
|
Terminator::St => b"\x1b\\",
|
||||||
|
Terminator::Bel => b"\x07",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Terminator {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
// The terminator is always ASCII.
|
||||||
|
f.write_str(std::str::from_utf8(self.string()).unwrap_or(""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State of the OSC parser.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum State {
|
||||||
|
Start,
|
||||||
|
Invalid,
|
||||||
|
|
||||||
|
// OSC command prefixes. Not all of these are valid OSCs, but may be
|
||||||
|
// needed to "bridge" to a valid OSC (e.g. to support OSC 777 we need to
|
||||||
|
// have a state "77" even though there is no OSC 77).
|
||||||
|
S0,
|
||||||
|
S1,
|
||||||
|
S2,
|
||||||
|
S3,
|
||||||
|
S4,
|
||||||
|
S5,
|
||||||
|
S6,
|
||||||
|
S7,
|
||||||
|
S8,
|
||||||
|
S9,
|
||||||
|
S30,
|
||||||
|
S300,
|
||||||
|
S3008,
|
||||||
|
S10,
|
||||||
|
S11,
|
||||||
|
S12,
|
||||||
|
S13,
|
||||||
|
S14,
|
||||||
|
S15,
|
||||||
|
S16,
|
||||||
|
S17,
|
||||||
|
S18,
|
||||||
|
S19,
|
||||||
|
S21,
|
||||||
|
S22,
|
||||||
|
S52,
|
||||||
|
S55,
|
||||||
|
S66,
|
||||||
|
S77,
|
||||||
|
S104,
|
||||||
|
S110,
|
||||||
|
S111,
|
||||||
|
S112,
|
||||||
|
S113,
|
||||||
|
S114,
|
||||||
|
S115,
|
||||||
|
S116,
|
||||||
|
S117,
|
||||||
|
S118,
|
||||||
|
S119,
|
||||||
|
S133,
|
||||||
|
S552,
|
||||||
|
S777,
|
||||||
|
S1337,
|
||||||
|
S5522,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How OSC payload data is being accumulated.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum Writer {
|
||||||
|
/// Not accumulating (still parsing the numeric prefix).
|
||||||
|
None,
|
||||||
|
/// Fixed-size buffer of `MAX_BUF` bytes.
|
||||||
|
Fixed,
|
||||||
|
/// Growable buffer.
|
||||||
|
Allocating,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Parser {
|
||||||
|
/// Whether an allocator is available, i.e. whether data longer than
|
||||||
|
/// MAX_BUF may be accepted for some commands (e.g. OSC 52), and whether
|
||||||
|
/// commands that require an allocator are processed. (Zig:
|
||||||
|
/// `alloc: ?Allocator`.)
|
||||||
|
pub alloc: bool,
|
||||||
|
|
||||||
|
/// Current state of the parser.
|
||||||
|
pub state: State,
|
||||||
|
|
||||||
|
/// Buffer for temporary storage of OSC data
|
||||||
|
buffer: Vec<u8>,
|
||||||
|
writer: Writer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Parser {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::init(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parser {
|
||||||
|
/// Maximum size of a "normal" OSC.
|
||||||
|
pub const MAX_BUF: usize = 2048;
|
||||||
|
|
||||||
|
pub fn init(alloc: bool) -> Parser {
|
||||||
|
Parser {
|
||||||
|
alloc,
|
||||||
|
state: State::Start,
|
||||||
|
buffer: Vec::new(),
|
||||||
|
writer: Writer::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset the parser state.
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.state = State::Start;
|
||||||
|
self.buffer.clear();
|
||||||
|
self.writer = Writer::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make sure that we have an allocator. If we don't, set the state to
|
||||||
|
/// invalid so that any additional OSC data is discarded.
|
||||||
|
fn ensure_allocator(&mut self) -> bool {
|
||||||
|
if self.alloc {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
log::warn!(
|
||||||
|
"An allocator is required to process OSC {:?} but none was provided.",
|
||||||
|
self.state
|
||||||
|
);
|
||||||
|
self.state = State::Invalid;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set up a fixed Writer to collect the rest of the OSC data.
|
||||||
|
fn write_to_fixed(&mut self) {
|
||||||
|
self.writer = Writer::Fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set up an allocating Writer to collect the rest of the OSC data. If we
|
||||||
|
/// don't have an allocator, fall back to writing to a fixed buffer and
|
||||||
|
/// hope that it's big enough.
|
||||||
|
fn write_to_allocating(&mut self) {
|
||||||
|
if !self.alloc {
|
||||||
|
self.write_to_fixed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.writer = Writer::Allocating;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper for the per-command parsers. Mirrors the Zig prologue
|
||||||
|
/// `writer orelse invalid; writer.writeByte(0) catch invalid;
|
||||||
|
/// writer.buffered()`: returns a copy of the accumulated data (without
|
||||||
|
/// the NUL terminator) or `None` (and marks the parser invalid) if no
|
||||||
|
/// data was being accumulated or there was no room for the terminator.
|
||||||
|
pub fn data(&mut self) -> Option<Vec<u8>> {
|
||||||
|
if self.writer == Writer::None {
|
||||||
|
self.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Reserve room for the NUL terminator.
|
||||||
|
if self.writer == Writer::Fixed && self.buffer.len() >= Self::MAX_BUF {
|
||||||
|
self.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(self.buffer.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like [`Parser::data`] but without reserving room for the NUL
|
||||||
|
/// terminator (for parsers that, like Zig's color parsers, read
|
||||||
|
/// `writer.buffered()` directly).
|
||||||
|
pub fn buffered(&mut self) -> Option<Vec<u8>> {
|
||||||
|
if self.writer == Writer::None {
|
||||||
|
self.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(self.buffer.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether any payload writer has been set up (Zig `parser.writer != null`).
|
||||||
|
pub fn has_writer(&self) -> bool {
|
||||||
|
self.writer != Writer::None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_byte(&mut self, c: u8) {
|
||||||
|
match self.writer {
|
||||||
|
Writer::None => unreachable!(),
|
||||||
|
Writer::Fixed => {
|
||||||
|
if self.buffer.len() >= Self::MAX_BUF {
|
||||||
|
// Overflow: discard any further input.
|
||||||
|
self.state = State::Invalid;
|
||||||
|
} else {
|
||||||
|
self.buffer.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Writer::Allocating => self.buffer.push(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consume the next character c and advance the parser state.
|
||||||
|
pub fn next(&mut self, c: u8) {
|
||||||
|
#[allow(clippy::enum_glob_use)]
|
||||||
|
use State::*;
|
||||||
|
|
||||||
|
// If the state becomes invalid for any reason, just discard
|
||||||
|
// any further input.
|
||||||
|
if self.state == Invalid {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a writer has been initialized, we just accumulate the rest of the
|
||||||
|
// OSC sequence in the writer's buffer and skip the state machine.
|
||||||
|
if self.writer != Writer::None {
|
||||||
|
self.write_byte(c);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.state {
|
||||||
|
// handled above, so should never be here
|
||||||
|
Invalid => unreachable!(),
|
||||||
|
|
||||||
|
Start => {
|
||||||
|
self.state = match c {
|
||||||
|
b'0' => S0,
|
||||||
|
b'1' => S1,
|
||||||
|
b'2' => S2,
|
||||||
|
b'3' => S3,
|
||||||
|
b'4' => S4,
|
||||||
|
b'5' => S5,
|
||||||
|
b'6' => S6,
|
||||||
|
b'7' => S7,
|
||||||
|
b'8' => S8,
|
||||||
|
b'9' => S9,
|
||||||
|
_ => Invalid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
S3 => {
|
||||||
|
self.state = match c {
|
||||||
|
b'0' => S30,
|
||||||
|
_ => Invalid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
S30 => {
|
||||||
|
self.state = match c {
|
||||||
|
b'0' => S300,
|
||||||
|
_ => Invalid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
S300 => {
|
||||||
|
self.state = match c {
|
||||||
|
b'8' => S3008,
|
||||||
|
_ => Invalid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
S3008 => match c {
|
||||||
|
b';' => self.write_to_fixed(),
|
||||||
|
_ => self.state = Invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
S1 => match c {
|
||||||
|
b';' => self.write_to_fixed(),
|
||||||
|
b'0' => self.state = S10,
|
||||||
|
b'1' => self.state = S11,
|
||||||
|
b'2' => self.state = S12,
|
||||||
|
b'3' => self.state = S13,
|
||||||
|
b'4' => self.state = S14,
|
||||||
|
b'5' => self.state = S15,
|
||||||
|
b'6' => self.state = S16,
|
||||||
|
b'7' => self.state = S17,
|
||||||
|
b'8' => self.state = S18,
|
||||||
|
b'9' => self.state = S19,
|
||||||
|
_ => self.state = Invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
S10 => match c {
|
||||||
|
b';' => {
|
||||||
|
if self.ensure_allocator() {
|
||||||
|
self.write_to_fixed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b'4' => self.state = S104,
|
||||||
|
_ => self.state = Invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
S104 => match c {
|
||||||
|
b';' => {
|
||||||
|
if self.ensure_allocator() {
|
||||||
|
self.write_to_fixed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => self.state = Invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
S11 => match c {
|
||||||
|
b';' => {
|
||||||
|
if self.ensure_allocator() {
|
||||||
|
self.write_to_fixed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b'0' => self.state = S110,
|
||||||
|
b'1' => self.state = S111,
|
||||||
|
b'2' => self.state = S112,
|
||||||
|
b'3' => self.state = S113,
|
||||||
|
b'4' => self.state = S114,
|
||||||
|
b'5' => self.state = S115,
|
||||||
|
b'6' => self.state = S116,
|
||||||
|
b'7' => self.state = S117,
|
||||||
|
b'8' => self.state = S118,
|
||||||
|
b'9' => self.state = S119,
|
||||||
|
_ => self.state = Invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
S4 | S12 | S14 | S15 | S16 | S17 | S18 | S19 | S21 | S110 | S111 | S112 | S113
|
||||||
|
| S114 | S115 | S116 | S117 | S118 | S119 => match c {
|
||||||
|
b';' => {
|
||||||
|
if self.ensure_allocator() {
|
||||||
|
self.write_to_fixed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => self.state = Invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
S13 => match c {
|
||||||
|
b';' => {
|
||||||
|
if self.ensure_allocator() {
|
||||||
|
self.write_to_fixed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b'3' => self.state = S133,
|
||||||
|
_ => self.state = Invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
S2 => match c {
|
||||||
|
b';' => self.write_to_fixed(),
|
||||||
|
b'1' => self.state = S21,
|
||||||
|
b'2' => self.state = S22,
|
||||||
|
_ => self.state = Invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
S5 => match c {
|
||||||
|
b';' => {
|
||||||
|
if self.ensure_allocator() {
|
||||||
|
self.write_to_fixed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b'2' => self.state = S52,
|
||||||
|
b'5' => self.state = S55,
|
||||||
|
_ => self.state = Invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
S6 => match c {
|
||||||
|
b'6' => self.state = S66,
|
||||||
|
_ => self.state = Invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
S52 | S66 => match c {
|
||||||
|
b';' => self.write_to_allocating(),
|
||||||
|
_ => self.state = Invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
S55 => match c {
|
||||||
|
b'2' => self.state = S552,
|
||||||
|
_ => self.state = Invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
S7 => match c {
|
||||||
|
b';' => self.write_to_fixed(),
|
||||||
|
b'7' => self.state = S77,
|
||||||
|
_ => self.state = Invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
S77 => match c {
|
||||||
|
b'7' => self.state = S777,
|
||||||
|
_ => self.state = Invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
S133 => match c {
|
||||||
|
b';' => self.write_to_fixed(),
|
||||||
|
b'7' => self.state = S1337,
|
||||||
|
_ => self.state = Invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
S552 => match c {
|
||||||
|
b'2' => self.state = S5522,
|
||||||
|
_ => self.state = Invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
S1337 => match c {
|
||||||
|
b';' => self.write_to_fixed(),
|
||||||
|
_ => self.state = Invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
S5522 => match c {
|
||||||
|
b';' => self.write_to_allocating(),
|
||||||
|
_ => self.state = Invalid,
|
||||||
|
},
|
||||||
|
|
||||||
|
S0 | S22 | S777 | S8 | S9 => match c {
|
||||||
|
b';' => self.write_to_fixed(),
|
||||||
|
_ => self.state = Invalid,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// End the sequence and return the command, if any. If the return value
|
||||||
|
/// is None, then no valid command was found. The optional terminator_ch
|
||||||
|
/// is the final character in the OSC sequence. This is used to determine
|
||||||
|
/// the response terminator.
|
||||||
|
pub fn end(&mut self, terminator_ch: Option<u8>) -> Option<Command> {
|
||||||
|
#[allow(clippy::enum_glob_use)]
|
||||||
|
use State::*;
|
||||||
|
match self.state {
|
||||||
|
Start => None,
|
||||||
|
|
||||||
|
Invalid => None,
|
||||||
|
|
||||||
|
S0 | S2 => parsers::change_window_title::parse(self, terminator_ch),
|
||||||
|
|
||||||
|
S1 => parsers::change_window_icon::parse(self, terminator_ch),
|
||||||
|
|
||||||
|
S4 | S5 | S10 | S11 | S12 | S13 | S14 | S15 | S16 | S17 | S18 | S19 | S104 | S110
|
||||||
|
| S111 | S112 | S113 | S114 | S115 | S116 | S117 | S118 | S119 => {
|
||||||
|
parsers::color::parse(self, terminator_ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
S7 => parsers::report_pwd::parse(self, terminator_ch),
|
||||||
|
|
||||||
|
S8 => parsers::hyperlink::parse(self, terminator_ch),
|
||||||
|
|
||||||
|
S9 => parsers::osc9::parse(self, terminator_ch),
|
||||||
|
|
||||||
|
S21 => parsers::kitty_color::parse(self, terminator_ch),
|
||||||
|
|
||||||
|
S22 => parsers::mouse_shape::parse(self, terminator_ch),
|
||||||
|
|
||||||
|
S52 => parsers::clipboard_operation::parse(self, terminator_ch),
|
||||||
|
|
||||||
|
S55 => None,
|
||||||
|
|
||||||
|
S3 | S30 | S300 => None,
|
||||||
|
|
||||||
|
S3008 => parsers::context_signal::parse(self, terminator_ch),
|
||||||
|
|
||||||
|
S6 => None,
|
||||||
|
|
||||||
|
S66 => parsers::kitty_text_sizing::parse(self, terminator_ch),
|
||||||
|
|
||||||
|
S77 => None,
|
||||||
|
|
||||||
|
S133 => parsers::semantic_prompt::parse(self, terminator_ch),
|
||||||
|
|
||||||
|
S552 => None,
|
||||||
|
|
||||||
|
S777 => parsers::rxvt_extension::parse(self, terminator_ch),
|
||||||
|
|
||||||
|
S1337 => parsers::iterm2::parse(self, terminator_ch),
|
||||||
|
|
||||||
|
S5522 => parsers::kitty_clipboard_protocol::parse(self, terminator_ch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
//! OSC 1 (ZIG/src/terminal/osc/parsers/change_window_icon.zig).
|
||||||
|
use crate::osc::{Command, Parser};
|
||||||
|
|
||||||
|
/// Parse OSC 1
|
||||||
|
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||||
|
let data = parser.data()?;
|
||||||
|
Some(Command::ChangeWindowIcon(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_1_change_window_icon() {
|
||||||
|
let mut p = Parser::init(false);
|
||||||
|
for c in b"1;ab" {
|
||||||
|
p.next(*c);
|
||||||
|
}
|
||||||
|
let cmd = p.end(None).unwrap();
|
||||||
|
assert_eq!(cmd, Command::ChangeWindowIcon(b"ab".to_vec()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
//! OSC 0 and OSC 2 (ZIG/src/terminal/osc/parsers/change_window_title.zig).
|
||||||
|
use crate::osc::{Command, Parser};
|
||||||
|
|
||||||
|
/// Parse OSC 0 and OSC 2
|
||||||
|
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||||
|
let data = parser.data()?;
|
||||||
|
Some(Command::ChangeWindowTitle(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn feed(p: &mut Parser, s: &[u8]) {
|
||||||
|
for c in s {
|
||||||
|
p.next(*c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_0_change_window_title() {
|
||||||
|
let mut p = Parser::init(false);
|
||||||
|
feed(&mut p, b"0;ab");
|
||||||
|
assert_eq!(
|
||||||
|
p.end(None).unwrap(),
|
||||||
|
Command::ChangeWindowTitle(b"ab".to_vec())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_0_longer_than_buffer() {
|
||||||
|
let mut p = Parser::init(false);
|
||||||
|
let mut input = b"0;".to_vec();
|
||||||
|
input.extend(std::iter::repeat_n(b'a', Parser::MAX_BUF + 2));
|
||||||
|
feed(&mut p, &input);
|
||||||
|
assert!(p.end(None).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_0_one_shorter_than_buffer_length() {
|
||||||
|
let mut p = Parser::init(false);
|
||||||
|
let title = vec![b'a'; Parser::MAX_BUF - 1];
|
||||||
|
let mut input = b"0;".to_vec();
|
||||||
|
input.extend(&title);
|
||||||
|
feed(&mut p, &input);
|
||||||
|
assert_eq!(p.end(None).unwrap(), Command::ChangeWindowTitle(title));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_0_exactly_at_buffer_length() {
|
||||||
|
let mut p = Parser::init(false);
|
||||||
|
let mut input = b"0;".to_vec();
|
||||||
|
input.extend(vec![b'a'; Parser::MAX_BUF]);
|
||||||
|
feed(&mut p, &input);
|
||||||
|
// This should be None because we always reserve space for a null terminator.
|
||||||
|
assert!(p.end(None).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_2_change_window_title_with_2() {
|
||||||
|
let mut p = Parser::init(false);
|
||||||
|
feed(&mut p, b"2;ab");
|
||||||
|
assert_eq!(
|
||||||
|
p.end(None).unwrap(),
|
||||||
|
Command::ChangeWindowTitle(b"ab".to_vec())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_2_change_window_title_with_utf8() {
|
||||||
|
let mut p = Parser::init(false);
|
||||||
|
feed(&mut p, b"2;");
|
||||||
|
// '—' EM DASH U+2014 (E2 80 94)
|
||||||
|
feed(&mut p, &[0xE2, 0x80, 0x94]);
|
||||||
|
p.next(b' ');
|
||||||
|
// '‐' HYPHEN U+2010 (E2 80 90)
|
||||||
|
// Intententionally chosen to conflict with the 0x90 C1 control
|
||||||
|
feed(&mut p, &[0xE2, 0x80, 0x90]);
|
||||||
|
assert_eq!(
|
||||||
|
p.end(None).unwrap(),
|
||||||
|
Command::ChangeWindowTitle("— ‐".as_bytes().to_vec())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_2_change_window_title_empty() {
|
||||||
|
let mut p = Parser::init(false);
|
||||||
|
feed(&mut p, b"2;");
|
||||||
|
assert_eq!(p.end(None).unwrap(), Command::ChangeWindowTitle(Vec::new()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
//! OSC 52 (ZIG/src/terminal/osc/parsers/clipboard_operation.zig).
|
||||||
|
use crate::osc::{Command, Parser, State};
|
||||||
|
|
||||||
|
/// Parse OSC 52
|
||||||
|
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||||
|
debug_assert_eq!(parser.state, State::S52);
|
||||||
|
let data = parser.data()?;
|
||||||
|
// Zig's `data` includes the NUL terminator, so `data.len == 1` there
|
||||||
|
// means the payload is empty.
|
||||||
|
if data.is_empty() {
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if data[0] == b';' {
|
||||||
|
Some(Command::ClipboardContents {
|
||||||
|
kind: b'c',
|
||||||
|
data: data[1..].to_vec(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if data.len() < 2 {
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if data[1] != b';' {
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Command::ClipboardContents {
|
||||||
|
kind: data[0],
|
||||||
|
data: data[2..].to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn run(alloc: bool, input: &[u8]) -> Option<Command> {
|
||||||
|
let mut p = Parser::init(alloc);
|
||||||
|
for c in input {
|
||||||
|
p.next(*c);
|
||||||
|
}
|
||||||
|
p.end(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_52_get_set_clipboard() {
|
||||||
|
assert_eq!(
|
||||||
|
run(false, b"52;s;?").unwrap(),
|
||||||
|
Command::ClipboardContents {
|
||||||
|
kind: b's',
|
||||||
|
data: b"?".to_vec()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_52_get_set_clipboard_optional_parameter() {
|
||||||
|
assert_eq!(
|
||||||
|
run(false, b"52;;?").unwrap(),
|
||||||
|
Command::ClipboardContents {
|
||||||
|
kind: b'c',
|
||||||
|
data: b"?".to_vec()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_52_get_set_clipboard_with_allocator() {
|
||||||
|
assert_eq!(
|
||||||
|
run(true, b"52;s;?").unwrap(),
|
||||||
|
Command::ClipboardContents {
|
||||||
|
kind: b's',
|
||||||
|
data: b"?".to_vec()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_52_clear_clipboard() {
|
||||||
|
assert_eq!(
|
||||||
|
run(false, b"52;;").unwrap(),
|
||||||
|
Command::ClipboardContents {
|
||||||
|
kind: b'c',
|
||||||
|
data: Vec::new()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,564 @@
|
|||||||
|
//! OSC color operations: 4, 5, 10-19, 104, 105, 110-119
|
||||||
|
//! (ZIG/src/terminal/osc/parsers/color.zig).
|
||||||
|
|
||||||
|
use crate::color::{Dynamic as DynamicColor, Rgb, Special as SpecialColor};
|
||||||
|
use crate::osc::{Command, Parser, State, Terminator};
|
||||||
|
|
||||||
|
/// The possible operations we support for colors.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum Operation {
|
||||||
|
Osc4,
|
||||||
|
Osc5,
|
||||||
|
Osc10,
|
||||||
|
Osc11,
|
||||||
|
Osc12,
|
||||||
|
Osc13,
|
||||||
|
Osc14,
|
||||||
|
Osc15,
|
||||||
|
Osc16,
|
||||||
|
Osc17,
|
||||||
|
Osc18,
|
||||||
|
Osc19,
|
||||||
|
Osc104,
|
||||||
|
Osc105,
|
||||||
|
Osc110,
|
||||||
|
Osc111,
|
||||||
|
Osc112,
|
||||||
|
Osc113,
|
||||||
|
Osc114,
|
||||||
|
Osc115,
|
||||||
|
Osc116,
|
||||||
|
Osc117,
|
||||||
|
Osc118,
|
||||||
|
Osc119,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse OSCs 4, 5, 10-19, 104, 110-119
|
||||||
|
pub fn parse(parser: &mut Parser, terminator_ch: Option<u8>) -> Option<Command> {
|
||||||
|
if !parser.alloc {
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// If we've collected any extra data parse that, otherwise use an empty
|
||||||
|
// string.
|
||||||
|
let data = if parser.has_writer() {
|
||||||
|
parser.buffered()?
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
// Check and make sure that we're parsing the correct OSCs
|
||||||
|
let op = match parser.state {
|
||||||
|
State::S4 => Operation::Osc4,
|
||||||
|
State::S5 => Operation::Osc5,
|
||||||
|
State::S10 => Operation::Osc10,
|
||||||
|
State::S11 => Operation::Osc11,
|
||||||
|
State::S12 => Operation::Osc12,
|
||||||
|
State::S13 => Operation::Osc13,
|
||||||
|
State::S14 => Operation::Osc14,
|
||||||
|
State::S15 => Operation::Osc15,
|
||||||
|
State::S16 => Operation::Osc16,
|
||||||
|
State::S17 => Operation::Osc17,
|
||||||
|
State::S18 => Operation::Osc18,
|
||||||
|
State::S19 => Operation::Osc19,
|
||||||
|
State::S104 => Operation::Osc104,
|
||||||
|
State::S110 => Operation::Osc110,
|
||||||
|
State::S111 => Operation::Osc111,
|
||||||
|
State::S112 => Operation::Osc112,
|
||||||
|
State::S113 => Operation::Osc113,
|
||||||
|
State::S114 => Operation::Osc114,
|
||||||
|
State::S115 => Operation::Osc115,
|
||||||
|
State::S116 => Operation::Osc116,
|
||||||
|
State::S117 => Operation::Osc117,
|
||||||
|
State::S118 => Operation::Osc118,
|
||||||
|
State::S119 => Operation::Osc119,
|
||||||
|
_ => {
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some(Command::ColorOperation {
|
||||||
|
op,
|
||||||
|
requests: parse_color(op, &data),
|
||||||
|
terminator: Terminator::init(terminator_ch),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zig `std.mem.tokenizeScalar(u8, buf, ';')`: skips empty tokens.
|
||||||
|
fn tokenize(buf: &[u8]) -> impl Iterator<Item = &[u8]> {
|
||||||
|
buf.split(|&c| c == b';').filter(|t| !t.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zig `std.fmt.parseInt(u9, s, 10)`: optional sign, digits only.
|
||||||
|
fn parse_u9(s: &[u8]) -> Option<u16> {
|
||||||
|
let (neg, digits) = match s.first()? {
|
||||||
|
b'+' => (false, &s[1..]),
|
||||||
|
b'-' => (true, &s[1..]),
|
||||||
|
_ => (false, s),
|
||||||
|
};
|
||||||
|
if digits.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut acc: u32 = 0;
|
||||||
|
for &c in digits {
|
||||||
|
if !c.is_ascii_digit() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
acc = acc * 10 + (c - b'0') as u32;
|
||||||
|
if acc > 511 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if neg && acc != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(acc as u16)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse any color operation string. This should NOT include the operation
|
||||||
|
/// itself, but only the body of the operation. e.g. for "4;a;b;c" the body
|
||||||
|
/// should be "a;b;c" and the operation should be set accordingly.
|
||||||
|
///
|
||||||
|
/// Color parsing is fairly complicated so we pull this out to a specialized
|
||||||
|
/// function rather than go through our OSC parsing state machine. This is
|
||||||
|
/// much slower and requires more memory (since we need to buffer the full
|
||||||
|
/// request) but grants us an easier to understand and testable implementation.
|
||||||
|
///
|
||||||
|
/// If color changing ends up being a bottleneck we can optimize this later.
|
||||||
|
pub fn parse_color(op: Operation, buf: &[u8]) -> List {
|
||||||
|
let mut it = tokenize(buf);
|
||||||
|
match op {
|
||||||
|
Operation::Osc4 => parse_get_set_ansi_color(Operation::Osc4, &mut it),
|
||||||
|
Operation::Osc5 => parse_get_set_ansi_color(Operation::Osc5, &mut it),
|
||||||
|
Operation::Osc104 => parse_reset_ansi_color(Operation::Osc104, &mut it),
|
||||||
|
Operation::Osc105 => parse_reset_ansi_color(Operation::Osc105, &mut it),
|
||||||
|
Operation::Osc10 => parse_get_set_dynamic_color(DynamicColor::Foreground, &mut it),
|
||||||
|
Operation::Osc11 => parse_get_set_dynamic_color(DynamicColor::Background, &mut it),
|
||||||
|
Operation::Osc12 => parse_get_set_dynamic_color(DynamicColor::Cursor, &mut it),
|
||||||
|
Operation::Osc13 => parse_get_set_dynamic_color(DynamicColor::PointerForeground, &mut it),
|
||||||
|
Operation::Osc14 => parse_get_set_dynamic_color(DynamicColor::PointerBackground, &mut it),
|
||||||
|
Operation::Osc15 => parse_get_set_dynamic_color(DynamicColor::TektronixForeground, &mut it),
|
||||||
|
Operation::Osc16 => parse_get_set_dynamic_color(DynamicColor::TektronixBackground, &mut it),
|
||||||
|
Operation::Osc17 => parse_get_set_dynamic_color(DynamicColor::HighlightBackground, &mut it),
|
||||||
|
Operation::Osc18 => parse_get_set_dynamic_color(DynamicColor::TektronixCursor, &mut it),
|
||||||
|
Operation::Osc19 => parse_get_set_dynamic_color(DynamicColor::HighlightForeground, &mut it),
|
||||||
|
Operation::Osc110 => parse_reset_dynamic_color(DynamicColor::Foreground, &mut it),
|
||||||
|
Operation::Osc111 => parse_reset_dynamic_color(DynamicColor::Background, &mut it),
|
||||||
|
Operation::Osc112 => parse_reset_dynamic_color(DynamicColor::Cursor, &mut it),
|
||||||
|
Operation::Osc113 => parse_reset_dynamic_color(DynamicColor::PointerForeground, &mut it),
|
||||||
|
Operation::Osc114 => parse_reset_dynamic_color(DynamicColor::PointerBackground, &mut it),
|
||||||
|
Operation::Osc115 => parse_reset_dynamic_color(DynamicColor::TektronixForeground, &mut it),
|
||||||
|
Operation::Osc116 => parse_reset_dynamic_color(DynamicColor::TektronixBackground, &mut it),
|
||||||
|
Operation::Osc117 => parse_reset_dynamic_color(DynamicColor::HighlightBackground, &mut it),
|
||||||
|
Operation::Osc118 => parse_reset_dynamic_color(DynamicColor::TektronixCursor, &mut it),
|
||||||
|
Operation::Osc119 => parse_reset_dynamic_color(DynamicColor::HighlightForeground, &mut it),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map a parsed numeric color to a target for OSC 4/5/104/105.
|
||||||
|
/// `special_only` is true for OSC 5/105; otherwise 0-255 is the palette and
|
||||||
|
/// 256+ is the special offset.
|
||||||
|
fn ansi_target(color: u16, special_only: bool) -> Option<Target> {
|
||||||
|
if special_only {
|
||||||
|
// std.math.cast(u3, color) then intToEnum
|
||||||
|
let v = u8::try_from(color).ok().filter(|&v| v < 8)?;
|
||||||
|
return SpecialColor::from_u8(v).map(Target::Special);
|
||||||
|
}
|
||||||
|
if let Ok(idx) = u8::try_from(color) {
|
||||||
|
return Some(Target::Palette(idx));
|
||||||
|
}
|
||||||
|
let v = u8::try_from(color - 256).ok().filter(|&v| v < 8)?;
|
||||||
|
SpecialColor::from_u8(v).map(Target::Special)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// OSC 4/5
|
||||||
|
fn parse_get_set_ansi_color<'a>(op: Operation, it: &mut impl Iterator<Item = &'a [u8]>) -> List {
|
||||||
|
// Note: in ANY error scenario below we return the accumulated results.
|
||||||
|
// This matches the xterm behavior (see misc.c ChangeAnsiColorRequest)
|
||||||
|
|
||||||
|
let mut result = List::new();
|
||||||
|
loop {
|
||||||
|
// We expect a `c; spec` pair. If either doesn't exist then
|
||||||
|
// we return the results up to this point.
|
||||||
|
let Some(color_str) = it.next() else {
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
let Some(spec_str) = it.next() else {
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Color must be numeric. u9 because that'll fit our palette + special
|
||||||
|
let Some(color) = parse_u9(color_str) else {
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the color.
|
||||||
|
let Some(target) = ansi_target(color, op == Operation::Osc5) else {
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// "?" always results in a query.
|
||||||
|
if spec_str == b"?" {
|
||||||
|
result.push(Request::Query(target));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(rgb) = Rgb::parse_bytes(spec_str) else {
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
result.push(Request::Set(ColoredTarget { target, color: rgb }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// OSC 104/105: Reset ANSI Colors
|
||||||
|
fn parse_reset_ansi_color<'a>(op: Operation, it: &mut impl Iterator<Item = &'a [u8]>) -> List {
|
||||||
|
// Note: xterm stops parsing the reset list on any error, but we're
|
||||||
|
// more flexible and try the next value. This matches the behavior of
|
||||||
|
// Kitty and I don't see a downside to being more flexible here. Hopefully
|
||||||
|
// no one depends on the exact behavior of xterm.
|
||||||
|
|
||||||
|
let mut result = List::new();
|
||||||
|
loop {
|
||||||
|
let Some(color_str) = it.next() else {
|
||||||
|
// If no parameters are given, we reset the full table.
|
||||||
|
if result.is_empty() {
|
||||||
|
result.push(match op {
|
||||||
|
Operation::Osc104 => Request::ResetPalette,
|
||||||
|
Operation::Osc105 => Request::ResetSpecial,
|
||||||
|
_ => unreachable!(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Empty color strings are ignored, not treated as an error.
|
||||||
|
if color_str.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color must be numeric. u9 because that'll fit our palette + special
|
||||||
|
let Some(color) = parse_u9(color_str) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the color.
|
||||||
|
let Some(target) = ansi_target(color, op == Operation::Osc105) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
result.push(Request::Reset(target));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// OSC 10-19: Get/Set Dynamic Colors
|
||||||
|
fn parse_get_set_dynamic_color<'a>(
|
||||||
|
start: DynamicColor,
|
||||||
|
it: &mut impl Iterator<Item = &'a [u8]>,
|
||||||
|
) -> List {
|
||||||
|
// Note: in ANY error scenario below we return the accumulated results.
|
||||||
|
// This matches the xterm behavior (see misc.c ChangeColorsRequest)
|
||||||
|
|
||||||
|
let mut result = List::new();
|
||||||
|
let mut color = start;
|
||||||
|
loop {
|
||||||
|
let Some(spec_str) = it.next() else {
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
if spec_str == b"?" {
|
||||||
|
result.push(Request::Query(Target::Dynamic(color)));
|
||||||
|
} else {
|
||||||
|
let Ok(rgb) = Rgb::parse_bytes(spec_str) else {
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
result.push(Request::Set(ColoredTarget {
|
||||||
|
target: Target::Dynamic(color),
|
||||||
|
color: rgb,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each successive value uses the next color so long as it exists.
|
||||||
|
match color.next() {
|
||||||
|
Some(n) => color = n,
|
||||||
|
None => return result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// OSC 110-119: Reset Dynamic Colors
|
||||||
|
fn parse_reset_dynamic_color<'a>(
|
||||||
|
color: DynamicColor,
|
||||||
|
it: &mut impl Iterator<Item = &'a [u8]>,
|
||||||
|
) -> List {
|
||||||
|
let mut result = List::new();
|
||||||
|
if it.next().is_some() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result.push(Request::Reset(Target::Dynamic(color)));
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of requests in a color OSC. (Zig uses a SegmentedList to
|
||||||
|
/// avoid copying; a `Vec` is equivalent.)
|
||||||
|
pub type List = Vec<Request>;
|
||||||
|
|
||||||
|
/// A single operation related to the terminal color palette.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Request {
|
||||||
|
Set(ColoredTarget),
|
||||||
|
Query(Target),
|
||||||
|
Reset(Target),
|
||||||
|
ResetPalette,
|
||||||
|
ResetSpecial,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Target {
|
||||||
|
Palette(u8),
|
||||||
|
Special(SpecialColor),
|
||||||
|
Dynamic(DynamicColor),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct ColoredTarget {
|
||||||
|
pub target: Target,
|
||||||
|
pub color: Rgb,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const RED: Rgb = Rgb { r: 255, g: 0, b: 0 };
|
||||||
|
const BLUE: Rgb = Rgb { r: 0, g: 0, b: 255 };
|
||||||
|
|
||||||
|
fn set(target: Target, color: Rgb) -> Request {
|
||||||
|
Request::Set(ColoredTarget { target, color })
|
||||||
|
}
|
||||||
|
|
||||||
|
const SPECIALS: [SpecialColor; 5] = [
|
||||||
|
SpecialColor::Bold,
|
||||||
|
SpecialColor::Underline,
|
||||||
|
SpecialColor::Blink,
|
||||||
|
SpecialColor::Reverse,
|
||||||
|
SpecialColor::Italic,
|
||||||
|
];
|
||||||
|
|
||||||
|
const DYNAMICS: [(DynamicColor, Operation, Operation); 10] = [
|
||||||
|
(
|
||||||
|
DynamicColor::Foreground,
|
||||||
|
Operation::Osc10,
|
||||||
|
Operation::Osc110,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DynamicColor::Background,
|
||||||
|
Operation::Osc11,
|
||||||
|
Operation::Osc111,
|
||||||
|
),
|
||||||
|
(DynamicColor::Cursor, Operation::Osc12, Operation::Osc112),
|
||||||
|
(
|
||||||
|
DynamicColor::PointerForeground,
|
||||||
|
Operation::Osc13,
|
||||||
|
Operation::Osc113,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DynamicColor::PointerBackground,
|
||||||
|
Operation::Osc14,
|
||||||
|
Operation::Osc114,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DynamicColor::TektronixForeground,
|
||||||
|
Operation::Osc15,
|
||||||
|
Operation::Osc115,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DynamicColor::TektronixBackground,
|
||||||
|
Operation::Osc16,
|
||||||
|
Operation::Osc116,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DynamicColor::HighlightBackground,
|
||||||
|
Operation::Osc17,
|
||||||
|
Operation::Osc117,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DynamicColor::TektronixCursor,
|
||||||
|
Operation::Osc18,
|
||||||
|
Operation::Osc118,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DynamicColor::HighlightForeground,
|
||||||
|
Operation::Osc19,
|
||||||
|
Operation::Osc119,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_4_empty_param() {
|
||||||
|
let mut p = Parser::init(false);
|
||||||
|
for ch in "4;;".bytes() {
|
||||||
|
p.next(ch);
|
||||||
|
}
|
||||||
|
assert!(p.end(Some(0x1b)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_4() {
|
||||||
|
// Test every palette index
|
||||||
|
for idx in 0..u8::MAX {
|
||||||
|
// printf '\e]4;0;red\\'
|
||||||
|
let list = parse_color(Operation::Osc4, format!("{idx};red").as_bytes());
|
||||||
|
assert_eq!(vec![set(Target::Palette(idx), RED)], list);
|
||||||
|
|
||||||
|
// printf '\e]4;0;?\\'
|
||||||
|
let list = parse_color(Operation::Osc4, format!("{idx};?").as_bytes());
|
||||||
|
assert_eq!(vec![Request::Query(Target::Palette(idx))], list);
|
||||||
|
|
||||||
|
// Trailing invalid data produces results up to that point
|
||||||
|
let list = parse_color(Operation::Osc4, format!("{idx};red;").as_bytes());
|
||||||
|
assert_eq!(vec![set(Target::Palette(idx), RED)], list);
|
||||||
|
|
||||||
|
// Whitespace doesn't produce a working value in xterm but we
|
||||||
|
// allow it because Kitty does and it seems harmless.
|
||||||
|
let list = parse_color(Operation::Osc4, format!("{idx};red ").as_bytes());
|
||||||
|
assert_eq!(vec![set(Target::Palette(idx), RED)], list);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test every special color
|
||||||
|
for (i, special) in SPECIALS.iter().enumerate() {
|
||||||
|
let list = parse_color(Operation::Osc4, format!("{};red", 256 + i).as_bytes());
|
||||||
|
assert_eq!(vec![set(Target::Special(*special), RED)], list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5() {
|
||||||
|
for (i, special) in SPECIALS.iter().enumerate() {
|
||||||
|
let list = parse_color(Operation::Osc5, format!("{i};red").as_bytes());
|
||||||
|
assert_eq!(vec![set(Target::Special(*special), RED)], list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_4_multiple_requests() {
|
||||||
|
let list = parse_color(Operation::Osc4, b"0;red;1;blue");
|
||||||
|
assert_eq!(
|
||||||
|
vec![set(Target::Palette(0), RED), set(Target::Palette(1), BLUE)],
|
||||||
|
list
|
||||||
|
);
|
||||||
|
|
||||||
|
// Multiple requests with same index overwrite each other
|
||||||
|
let list = parse_color(Operation::Osc4, b"0;red;0;blue");
|
||||||
|
assert_eq!(
|
||||||
|
vec![set(Target::Palette(0), RED), set(Target::Palette(0), BLUE)],
|
||||||
|
list
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_104() {
|
||||||
|
for idx in 0..u8::MAX {
|
||||||
|
let list = parse_color(Operation::Osc104, idx.to_string().as_bytes());
|
||||||
|
assert_eq!(vec![Request::Reset(Target::Palette(idx))], list);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, special) in SPECIALS.iter().enumerate() {
|
||||||
|
let list = parse_color(Operation::Osc104, (256 + i).to_string().as_bytes());
|
||||||
|
assert_eq!(vec![Request::Reset(Target::Special(*special))], list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_104_empty_index() {
|
||||||
|
let list = parse_color(Operation::Osc104, b"0;;1");
|
||||||
|
assert_eq!(
|
||||||
|
vec![
|
||||||
|
Request::Reset(Target::Palette(0)),
|
||||||
|
Request::Reset(Target::Palette(1))
|
||||||
|
],
|
||||||
|
list
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_104_invalid_index() {
|
||||||
|
let list = parse_color(Operation::Osc104, b"ffff;1");
|
||||||
|
assert_eq!(vec![Request::Reset(Target::Palette(1))], list);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_104_reset_all() {
|
||||||
|
let list = parse_color(Operation::Osc104, b"");
|
||||||
|
assert_eq!(vec![Request::ResetPalette], list);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_105_reset_all() {
|
||||||
|
let list = parse_color(Operation::Osc105, b"");
|
||||||
|
assert_eq!(vec![Request::ResetSpecial], list);
|
||||||
|
}
|
||||||
|
|
||||||
|
// OSC 10-19: Get/Set Dynamic Colors
|
||||||
|
#[test]
|
||||||
|
fn osc_10_to_19_dynamic() {
|
||||||
|
for (color, op, _) in DYNAMICS {
|
||||||
|
// printf '\e]10;red\e\\'
|
||||||
|
let list = parse_color(op, b"red");
|
||||||
|
assert_eq!(vec![set(Target::Dynamic(color), RED)], list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_10_to_19_dynamic_multiple() {
|
||||||
|
// printf '\e]11;red;blue\e\\'
|
||||||
|
let list = parse_color(Operation::Osc11, b"red;blue");
|
||||||
|
assert_eq!(
|
||||||
|
vec![
|
||||||
|
set(Target::Dynamic(DynamicColor::Background), RED),
|
||||||
|
set(Target::Dynamic(DynamicColor::Cursor), BLUE)
|
||||||
|
],
|
||||||
|
list
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// OSC 110-119: Reset Dynamic Colors
|
||||||
|
#[test]
|
||||||
|
fn osc_110_to_119_reset_dynamic() {
|
||||||
|
for (color, _, op) in DYNAMICS {
|
||||||
|
let expected = vec![Request::Reset(Target::Dynamic(color))];
|
||||||
|
|
||||||
|
// printf '\e]110\e\\'
|
||||||
|
assert_eq!(expected, parse_color(op, b""));
|
||||||
|
|
||||||
|
// xterm allows a trailing semicolon.
|
||||||
|
assert_eq!(expected, parse_color(op, b";"));
|
||||||
|
|
||||||
|
// xterm does NOT allow any whitespace
|
||||||
|
assert_eq!(0, parse_color(op, b" ").len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_112_incomplete_sequence() {
|
||||||
|
// https://github.com/darrenstarr/VtNetCore/pull/14
|
||||||
|
let mut p = Parser::init(true);
|
||||||
|
for ch in b"112" {
|
||||||
|
p.next(*ch);
|
||||||
|
}
|
||||||
|
let cmd = p.end(Some(0x07)).unwrap();
|
||||||
|
let Command::ColorOperation {
|
||||||
|
op,
|
||||||
|
requests,
|
||||||
|
terminator,
|
||||||
|
} = cmd
|
||||||
|
else {
|
||||||
|
panic!("wrong command");
|
||||||
|
};
|
||||||
|
assert_eq!(Terminator::Bel, terminator);
|
||||||
|
assert_eq!(Operation::Osc112, op);
|
||||||
|
assert_eq!(
|
||||||
|
vec![Request::Reset(Target::Dynamic(DynamicColor::Cursor))],
|
||||||
|
requests
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,498 @@
|
|||||||
|
//! OSC 3008: Hierarchical Context Signalling (UAPI spec)
|
||||||
|
//! Specification: https://uapi-group.org/specifications/specs/osc_context/
|
||||||
|
//! (ZIG/src/terminal/osc/parsers/context_signal.zig)
|
||||||
|
//!
|
||||||
|
//! OSC 3008 allows programs to signal context changes to the terminal emulator.
|
||||||
|
//! Each context has an identifier and metadata fields. Contexts are hierarchical
|
||||||
|
//! and form a stack.
|
||||||
|
|
||||||
|
use crate::osc::{Command as OscCommand, Parser, State};
|
||||||
|
|
||||||
|
/// Maximum length of a context identifier (per spec).
|
||||||
|
const MAX_CONTEXT_ID_LEN: usize = 64;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Action {
|
||||||
|
/// OSC 3008;start=<id> - initiates, updates, or returns to a context.
|
||||||
|
Start,
|
||||||
|
/// OSC 3008;end=<id> - terminates a context.
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single OSC 3008 context signal command.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Command {
|
||||||
|
pub action: Action,
|
||||||
|
/// The context identifier. Must be 1-64 characters in the 32..126 byte range.
|
||||||
|
pub id: Vec<u8>,
|
||||||
|
/// Raw unparsed metadata fields after the context ID.
|
||||||
|
/// Fields are semicolon-separated key=value pairs.
|
||||||
|
/// Parsed lazily via `read_option`.
|
||||||
|
pub metadata: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Value of a decoded metadata field (Zig `Field.Type()`).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum FieldValue<'a> {
|
||||||
|
Type(ContextType),
|
||||||
|
Exit(ExitStatus),
|
||||||
|
U64(u64),
|
||||||
|
Str(&'a [u8]),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
|
/// Read a metadata field value from the raw fields string.
|
||||||
|
/// Returns None if the field is not present or malformed.
|
||||||
|
pub fn read_option(&self, option: Field) -> Option<FieldValue<'_>> {
|
||||||
|
option.read(&self.metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn context_type(&self) -> Option<ContextType> {
|
||||||
|
match self.read_option(Field::Type)? {
|
||||||
|
FieldValue::Type(v) => Some(v),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exit(&self) -> Option<ExitStatus> {
|
||||||
|
match self.read_option(Field::Exit)? {
|
||||||
|
FieldValue::Exit(v) => Some(v),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For the numeric fields (`pid`, `pidfdid`, `status`).
|
||||||
|
pub fn read_u64(&self, option: Field) -> Option<u64> {
|
||||||
|
match self.read_option(option)? {
|
||||||
|
FieldValue::U64(v) => Some(v),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For the string fields.
|
||||||
|
pub fn read_str(&self, option: Field) -> Option<&[u8]> {
|
||||||
|
match self.read_option(option)? {
|
||||||
|
FieldValue::Str(v) => Some(v),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Context types defined by the specification.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ContextType {
|
||||||
|
Boot,
|
||||||
|
Container,
|
||||||
|
Vm,
|
||||||
|
Elevate,
|
||||||
|
Chpriv,
|
||||||
|
Subcontext,
|
||||||
|
Remote,
|
||||||
|
Shell,
|
||||||
|
Command,
|
||||||
|
App,
|
||||||
|
Service,
|
||||||
|
Session,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextType {
|
||||||
|
pub fn parse(value: &[u8]) -> Option<ContextType> {
|
||||||
|
Some(match value {
|
||||||
|
b"boot" => ContextType::Boot,
|
||||||
|
b"container" => ContextType::Container,
|
||||||
|
b"vm" => ContextType::Vm,
|
||||||
|
b"elevate" => ContextType::Elevate,
|
||||||
|
b"chpriv" => ContextType::Chpriv,
|
||||||
|
b"subcontext" => ContextType::Subcontext,
|
||||||
|
b"remote" => ContextType::Remote,
|
||||||
|
b"shell" => ContextType::Shell,
|
||||||
|
b"command" => ContextType::Command,
|
||||||
|
b"app" => ContextType::App,
|
||||||
|
b"service" => ContextType::Service,
|
||||||
|
b"session" => ContextType::Session,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exit status for the `exit` end-sequence field.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ExitStatus {
|
||||||
|
Success,
|
||||||
|
Failure,
|
||||||
|
Crash,
|
||||||
|
Interrupt,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExitStatus {
|
||||||
|
pub fn parse(value: &[u8]) -> Option<ExitStatus> {
|
||||||
|
Some(match value {
|
||||||
|
b"success" => ExitStatus::Success,
|
||||||
|
b"failure" => ExitStatus::Failure,
|
||||||
|
b"crash" => ExitStatus::Crash,
|
||||||
|
b"interrupt" => ExitStatus::Interrupt,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Metadata fields that can appear in OSC 3008 sequences.
|
||||||
|
/// Fields are read lazily from the raw string using the `read` method.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Field {
|
||||||
|
// Start sequence fields
|
||||||
|
Type,
|
||||||
|
User,
|
||||||
|
Hostname,
|
||||||
|
Machineid,
|
||||||
|
Bootid,
|
||||||
|
Pid,
|
||||||
|
Pidfdid,
|
||||||
|
Comm,
|
||||||
|
Cwd,
|
||||||
|
Cmdline,
|
||||||
|
Vm,
|
||||||
|
Container,
|
||||||
|
Targetuser,
|
||||||
|
Targethost,
|
||||||
|
Sessionid,
|
||||||
|
|
||||||
|
// End sequence fields
|
||||||
|
Exit,
|
||||||
|
Status,
|
||||||
|
Signal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Field {
|
||||||
|
fn key(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Field::Type => "type",
|
||||||
|
Field::User => "user",
|
||||||
|
Field::Hostname => "hostname",
|
||||||
|
Field::Machineid => "machineid",
|
||||||
|
Field::Bootid => "bootid",
|
||||||
|
Field::Pid => "pid",
|
||||||
|
Field::Pidfdid => "pidfdid",
|
||||||
|
Field::Comm => "comm",
|
||||||
|
Field::Cwd => "cwd",
|
||||||
|
Field::Cmdline => "cmdline",
|
||||||
|
Field::Vm => "vm",
|
||||||
|
Field::Container => "container",
|
||||||
|
Field::Targetuser => "targetuser",
|
||||||
|
Field::Targethost => "targethost",
|
||||||
|
Field::Sessionid => "sessionid",
|
||||||
|
Field::Exit => "exit",
|
||||||
|
Field::Status => "status",
|
||||||
|
Field::Signal => "signal",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the field value from the raw fields string.
|
||||||
|
///
|
||||||
|
/// The raw fields string contains semicolon-separated key=value pairs
|
||||||
|
/// e.g. "type=container;user=lennart;hostname=zeta".
|
||||||
|
///
|
||||||
|
/// Unknown or malformed fields are ignored per the specification.
|
||||||
|
pub fn read(self, raw: &[u8]) -> Option<FieldValue<'_>> {
|
||||||
|
let key = self.key().as_bytes();
|
||||||
|
for full in raw.split(|&c| c == b';') {
|
||||||
|
let value = match full.iter().position(|&c| c == b'=') {
|
||||||
|
Some(eql) if &full[..eql] == key => &full[eql + 1..],
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
return match self {
|
||||||
|
Field::Type => ContextType::parse(value).map(FieldValue::Type),
|
||||||
|
Field::Exit => ExitStatus::parse(value).map(FieldValue::Exit),
|
||||||
|
Field::Pid | Field::Pidfdid | Field::Status => {
|
||||||
|
if value.iter().any(|c| !c.is_ascii_digit()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
std::str::from_utf8(value)
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.parse::<u64>().ok())
|
||||||
|
.map(FieldValue::U64)
|
||||||
|
}
|
||||||
|
// String fields
|
||||||
|
_ => {
|
||||||
|
if value.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(FieldValue::Str(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not found
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse OSC 3008: hierarchical context signalling.
|
||||||
|
///
|
||||||
|
/// Expected data format (after "3008;" prefix has been consumed by the state machine):
|
||||||
|
/// start=<id>[;<field>=<value>]*
|
||||||
|
/// end=<id>[;<field>=<value>]*
|
||||||
|
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<OscCommand> {
|
||||||
|
let data = parser.buffered()?;
|
||||||
|
if data.is_empty() {
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the action (start= or end=)
|
||||||
|
let (action, prefix_len) = if data.starts_with(b"start=") {
|
||||||
|
(Action::Start, b"start=".len())
|
||||||
|
} else if data.starts_with(b"end=") {
|
||||||
|
(Action::End, b"end=".len())
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"OSC 3008: expected 'start=' or 'end=' prefix, got: {}",
|
||||||
|
String::from_utf8_lossy(&data[..data.len().min(10)])
|
||||||
|
);
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Skip past the "start=" or "end=" prefix
|
||||||
|
let rest = &data[prefix_len..];
|
||||||
|
|
||||||
|
if rest.is_empty() {
|
||||||
|
log::warn!("OSC 3008: missing context ID");
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the context ID (up to the first semicolon or end of data)
|
||||||
|
let id_end = rest.iter().position(|&c| c == b';').unwrap_or(rest.len());
|
||||||
|
let id = &rest[..id_end];
|
||||||
|
|
||||||
|
// Validate context ID length (1-64 chars per spec)
|
||||||
|
if id.is_empty() || id.len() > MAX_CONTEXT_ID_LEN {
|
||||||
|
log::warn!(
|
||||||
|
"OSC 3008: context ID length {} out of range (1-{})",
|
||||||
|
id.len(),
|
||||||
|
MAX_CONTEXT_ID_LEN
|
||||||
|
);
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate context ID characters (32..126 byte range per spec)
|
||||||
|
for &c in id {
|
||||||
|
if !(0x20..=0x7e).contains(&c) {
|
||||||
|
log::warn!("OSC 3008: invalid character 0x{c:02x} in context ID");
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract raw metadata fields (everything after the ID)
|
||||||
|
let metadata = if id_end < rest.len() {
|
||||||
|
rest[id_end + 1..].to_vec()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(OscCommand::ContextSignal(Command {
|
||||||
|
action,
|
||||||
|
id: id.to_vec(),
|
||||||
|
metadata,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn run(input: &str) -> Option<OscCommand> {
|
||||||
|
let mut p = Parser::init(false);
|
||||||
|
for &ch in input.as_bytes() {
|
||||||
|
p.next(ch);
|
||||||
|
}
|
||||||
|
p.end(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cs(input: &str) -> Command {
|
||||||
|
match run(input) {
|
||||||
|
Some(OscCommand::ContextSignal(c)) => c,
|
||||||
|
other => panic!("unexpected {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_3008_basic_start_command() {
|
||||||
|
let c = cs("3008;start=abc123");
|
||||||
|
assert_eq!(Action::Start, c.action);
|
||||||
|
assert_eq!(b"abc123", &c.id[..]);
|
||||||
|
assert_eq!(b"", &c.metadata[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_3008_basic_end_command() {
|
||||||
|
let c = cs("3008;end=abc123");
|
||||||
|
assert_eq!(Action::End, c.action);
|
||||||
|
assert_eq!(b"abc123", &c.id[..]);
|
||||||
|
assert_eq!(b"", &c.metadata[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_3008_start_with_metadata_fields() {
|
||||||
|
let c = cs(
|
||||||
|
"3008;start=bed86fab93af4328bbed0a1224af6d40;type=container;user=lennart;hostname=zeta",
|
||||||
|
);
|
||||||
|
assert_eq!(Action::Start, c.action);
|
||||||
|
assert_eq!(b"bed86fab93af4328bbed0a1224af6d40", &c.id[..]);
|
||||||
|
assert_eq!(Some(ContextType::Container), c.context_type());
|
||||||
|
assert_eq!(b"lennart", c.read_str(Field::User).unwrap());
|
||||||
|
assert_eq!(b"zeta", c.read_str(Field::Hostname).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_3008_start_with_all_common_fields() {
|
||||||
|
let c = cs(
|
||||||
|
"3008;start=myctx;type=shell;user=root;hostname=myhost;machineid=3deb5353d3ba43d08201c136a47ead7b;bootid=d4a3d0fdf2e24fdea6d971ce73f4fbf2;pid=1062862;pidfdid=1063162;comm=bash",
|
||||||
|
);
|
||||||
|
assert_eq!(Some(ContextType::Shell), c.context_type());
|
||||||
|
assert_eq!(b"root", c.read_str(Field::User).unwrap());
|
||||||
|
assert_eq!(b"myhost", c.read_str(Field::Hostname).unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
b"3deb5353d3ba43d08201c136a47ead7b",
|
||||||
|
c.read_str(Field::Machineid).unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
b"d4a3d0fdf2e24fdea6d971ce73f4fbf2",
|
||||||
|
c.read_str(Field::Bootid).unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(Some(1062862), c.read_u64(Field::Pid));
|
||||||
|
assert_eq!(Some(1063162), c.read_u64(Field::Pidfdid));
|
||||||
|
assert_eq!(b"bash", c.read_str(Field::Comm).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_3008_end_with_exit_metadata() {
|
||||||
|
let c = cs("3008;end=myctx;exit=success;status=0");
|
||||||
|
assert_eq!(Action::End, c.action);
|
||||||
|
assert_eq!(b"myctx", &c.id[..]);
|
||||||
|
assert_eq!(Some(ExitStatus::Success), c.exit());
|
||||||
|
assert_eq!(Some(0), c.read_u64(Field::Status));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_3008_end_with_failure_exit() {
|
||||||
|
let c = cs("3008;end=myctx;exit=failure;status=1;signal=SIGKILL");
|
||||||
|
assert_eq!(Some(ExitStatus::Failure), c.exit());
|
||||||
|
assert_eq!(Some(1), c.read_u64(Field::Status));
|
||||||
|
assert_eq!(b"SIGKILL", c.read_str(Field::Signal).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_3008_unknown_fields_are_ignored() {
|
||||||
|
let c = cs("3008;start=myctx;type=shell;unknownfield=value;user=root");
|
||||||
|
assert_eq!(Some(ContextType::Shell), c.context_type());
|
||||||
|
assert_eq!(b"root", c.read_str(Field::User).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_3008_missing_field_returns_null() {
|
||||||
|
let c = cs("3008;start=myctx;user=lennart");
|
||||||
|
assert!(c.context_type().is_none());
|
||||||
|
assert!(c.read_str(Field::Hostname).is_none());
|
||||||
|
assert!(c.read_u64(Field::Pid).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_3008_invalid_prefix() {
|
||||||
|
assert!(run("3008;bogus=abc123").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_3008_empty_data() {
|
||||||
|
assert!(run("3008;start=").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_3008_max_length_context_id() {
|
||||||
|
let id = "a".repeat(64);
|
||||||
|
let c = cs(&format!("3008;start={id}"));
|
||||||
|
assert_eq!(id.as_bytes(), &c.id[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_3008_over_length_context_id() {
|
||||||
|
let id = "a".repeat(65);
|
||||||
|
assert!(run(&format!("3008;start={id}")).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_3008_context_type_enum_coverage() {
|
||||||
|
let types = [
|
||||||
|
("boot", ContextType::Boot),
|
||||||
|
("container", ContextType::Container),
|
||||||
|
("vm", ContextType::Vm),
|
||||||
|
("elevate", ContextType::Elevate),
|
||||||
|
("chpriv", ContextType::Chpriv),
|
||||||
|
("subcontext", ContextType::Subcontext),
|
||||||
|
("remote", ContextType::Remote),
|
||||||
|
("shell", ContextType::Shell),
|
||||||
|
("command", ContextType::Command),
|
||||||
|
("app", ContextType::App),
|
||||||
|
("service", ContextType::Service),
|
||||||
|
("session", ContextType::Session),
|
||||||
|
];
|
||||||
|
for (s, e) in types {
|
||||||
|
assert_eq!(Some(e), ContextType::parse(s.as_bytes()));
|
||||||
|
}
|
||||||
|
assert!(ContextType::parse(b"invalid").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_3008_exit_status_enum_coverage() {
|
||||||
|
assert_eq!(Some(ExitStatus::Success), ExitStatus::parse(b"success"));
|
||||||
|
assert_eq!(Some(ExitStatus::Failure), ExitStatus::parse(b"failure"));
|
||||||
|
assert_eq!(Some(ExitStatus::Crash), ExitStatus::parse(b"crash"));
|
||||||
|
assert_eq!(Some(ExitStatus::Interrupt), ExitStatus::parse(b"interrupt"));
|
||||||
|
assert!(ExitStatus::parse(b"invalid").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_3008_spec_example_container_start() {
|
||||||
|
let c = cs(
|
||||||
|
"3008;start=bed86fab93af4328bbed0a1224af6d40;type=container;user=lennart;hostname=zeta;machineid=3deb5353d3ba43d08201c136a47ead7b;bootid=d4a3d0fdf2e24fdea6d971ce73f4fbf2;pid=1062862;pidfdid=1063162;comm=systemd-nspawn;container=foobar",
|
||||||
|
);
|
||||||
|
assert_eq!(Action::Start, c.action);
|
||||||
|
assert_eq!(b"bed86fab93af4328bbed0a1224af6d40", &c.id[..]);
|
||||||
|
assert_eq!(Some(ContextType::Container), c.context_type());
|
||||||
|
assert_eq!(b"lennart", c.read_str(Field::User).unwrap());
|
||||||
|
assert_eq!(b"zeta", c.read_str(Field::Hostname).unwrap());
|
||||||
|
assert_eq!(b"systemd-nspawn", c.read_str(Field::Comm).unwrap());
|
||||||
|
assert_eq!(b"foobar", c.read_str(Field::Container).unwrap());
|
||||||
|
assert_eq!(Some(1062862), c.read_u64(Field::Pid));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_3008_spec_example_context_end() {
|
||||||
|
let c = cs("3008;end=bed86fab93af4328bbed0a1224af6d40");
|
||||||
|
assert_eq!(Action::End, c.action);
|
||||||
|
assert_eq!(b"bed86fab93af4328bbed0a1224af6d40", &c.id[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_3008_cwd_and_cmdline_fields() {
|
||||||
|
let c = cs("3008;start=myctx;type=command;cwd=/home/user;cmdline=ls -la");
|
||||||
|
assert_eq!(b"/home/user", c.read_str(Field::Cwd).unwrap());
|
||||||
|
assert_eq!(b"ls -la", c.read_str(Field::Cmdline).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_3008_start_command_with_no_fields() {
|
||||||
|
let c = cs("3008;start=simpleid");
|
||||||
|
assert_eq!(Action::Start, c.action);
|
||||||
|
assert_eq!(b"simpleid", &c.id[..]);
|
||||||
|
assert!(c.context_type().is_none());
|
||||||
|
assert!(c.read_str(Field::User).is_none());
|
||||||
|
assert!(c.exit().is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
//! OSC 8 hyperlinks (ZIG/src/terminal/osc/parsers/hyperlink.zig).
|
||||||
|
use crate::osc::{Command, Parser, State};
|
||||||
|
|
||||||
|
/// Parse OSC 8 hyperlinks
|
||||||
|
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||||
|
let data = parser.data()?;
|
||||||
|
let Some(s) = data.iter().position(|&b| b == b';') else {
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let uri = data[s + 1..].to_vec();
|
||||||
|
let mut id: Option<Vec<u8>> = None;
|
||||||
|
|
||||||
|
// The params section is `key=value` pairs separated by ':'. This follows
|
||||||
|
// the Zig implementation byte for byte: ':' and the ';' terminator are
|
||||||
|
// turned into NULs and the segments are walked by searching for the next
|
||||||
|
// NUL starting one past the segment start.
|
||||||
|
let mut kvs = data[..s].to_vec();
|
||||||
|
kvs.push(0);
|
||||||
|
for b in &mut kvs {
|
||||||
|
if *b == b':' {
|
||||||
|
*b = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut kv_start = 0;
|
||||||
|
while kv_start < kvs.len() {
|
||||||
|
let Some(kv_end) = kvs
|
||||||
|
.iter()
|
||||||
|
.skip(kv_start + 1)
|
||||||
|
.position(|&b| b == 0)
|
||||||
|
.map(|i| i + kv_start + 1)
|
||||||
|
else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let kv = &kvs[kv_start..kv_end];
|
||||||
|
let Some(v) = kv.iter().position(|&b| b == b'=') else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let key = &kv[..v];
|
||||||
|
let value = &kv[v + 1..];
|
||||||
|
if key == b"id" {
|
||||||
|
if !value.is_empty() {
|
||||||
|
id = Some(value.to_vec());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"unknown hyperlink option: '{}'",
|
||||||
|
String::from_utf8_lossy(key)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
kv_start = kv_end + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if uri.is_empty() {
|
||||||
|
if id.is_some() {
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
return Some(Command::HyperlinkEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Command::HyperlinkStart { id, uri })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn run(input: &[u8]) -> Option<Command> {
|
||||||
|
let mut p = Parser::init(false);
|
||||||
|
for c in input {
|
||||||
|
p.next(*c);
|
||||||
|
}
|
||||||
|
p.end(Some(0x1b))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start(id: Option<&str>, uri: &str) -> Command {
|
||||||
|
Command::HyperlinkStart {
|
||||||
|
id: id.map(|s| s.as_bytes().to_vec()),
|
||||||
|
uri: uri.as_bytes().to_vec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_8_hyperlink() {
|
||||||
|
assert_eq!(
|
||||||
|
run(b"8;;http://example.com").unwrap(),
|
||||||
|
start(None, "http://example.com")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_8_hyperlink_with_id_set() {
|
||||||
|
assert_eq!(
|
||||||
|
run(b"8;id=foo;http://example.com").unwrap(),
|
||||||
|
start(Some("foo"), "http://example.com")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_8_hyperlink_with_empty_id() {
|
||||||
|
assert_eq!(
|
||||||
|
run(b"8;id=;http://example.com").unwrap(),
|
||||||
|
start(None, "http://example.com")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_8_hyperlink_with_incomplete_key() {
|
||||||
|
assert_eq!(
|
||||||
|
run(b"8;id;http://example.com").unwrap(),
|
||||||
|
start(None, "http://example.com")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_8_hyperlink_with_empty_key() {
|
||||||
|
assert_eq!(
|
||||||
|
run(b"8;=value;http://example.com").unwrap(),
|
||||||
|
start(None, "http://example.com")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_8_hyperlink_with_empty_key_and_id() {
|
||||||
|
assert_eq!(
|
||||||
|
run(b"8;=value:id=foo;http://example.com").unwrap(),
|
||||||
|
start(Some("foo"), "http://example.com")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_8_hyperlink_with_empty_uri() {
|
||||||
|
assert!(run(b"8;id=foo;").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_8_hyperlink_end() {
|
||||||
|
assert_eq!(run(b"8;;").unwrap(), Command::HyperlinkEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
//! OSC 1337 (iTerm2) (ZIG/src/terminal/osc/parsers/iterm2.zig).
|
||||||
|
|
||||||
|
use crate::osc::{Command, Parser, State};
|
||||||
|
|
||||||
|
/// iTerm2 OSC 1337 keys. Lookup is ASCII case-insensitive.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum Key {
|
||||||
|
AddAnnotation,
|
||||||
|
AddHiddenAnnotation,
|
||||||
|
Block,
|
||||||
|
Button,
|
||||||
|
ClearCapturedOutput,
|
||||||
|
ClearScrollback,
|
||||||
|
Copy,
|
||||||
|
CopyToClipboard,
|
||||||
|
CurrentDir,
|
||||||
|
CursorShape,
|
||||||
|
Custom,
|
||||||
|
Disinter,
|
||||||
|
EndCopy,
|
||||||
|
File,
|
||||||
|
FileEnd,
|
||||||
|
FilePart,
|
||||||
|
HighlightCursorLine,
|
||||||
|
MultipartFile,
|
||||||
|
OpenURL,
|
||||||
|
PopKeyLabels,
|
||||||
|
PushKeyLabels,
|
||||||
|
RemoteHost,
|
||||||
|
ReportCellSize,
|
||||||
|
ReportVariable,
|
||||||
|
RequestAttention,
|
||||||
|
RequestUpload,
|
||||||
|
SetBackgroundImageFile,
|
||||||
|
SetBadgeFormat,
|
||||||
|
SetColors,
|
||||||
|
SetKeyLabel,
|
||||||
|
SetMark,
|
||||||
|
SetProfile,
|
||||||
|
SetUserVar,
|
||||||
|
ShellIntegrationVersion,
|
||||||
|
StealFocus,
|
||||||
|
UnicodeVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEYS: &[(&str, Key)] = &[
|
||||||
|
("AddAnnotation", Key::AddAnnotation),
|
||||||
|
("AddHiddenAnnotation", Key::AddHiddenAnnotation),
|
||||||
|
("Block", Key::Block),
|
||||||
|
("Button", Key::Button),
|
||||||
|
("ClearCapturedOutput", Key::ClearCapturedOutput),
|
||||||
|
("ClearScrollback", Key::ClearScrollback),
|
||||||
|
("Copy", Key::Copy),
|
||||||
|
("CopyToClipboard", Key::CopyToClipboard),
|
||||||
|
("CurrentDir", Key::CurrentDir),
|
||||||
|
("CursorShape", Key::CursorShape),
|
||||||
|
("Custom", Key::Custom),
|
||||||
|
("Disinter", Key::Disinter),
|
||||||
|
("EndCopy", Key::EndCopy),
|
||||||
|
("File", Key::File),
|
||||||
|
("FileEnd", Key::FileEnd),
|
||||||
|
("FilePart", Key::FilePart),
|
||||||
|
("HighlightCursorLine", Key::HighlightCursorLine),
|
||||||
|
("MultipartFile", Key::MultipartFile),
|
||||||
|
("OpenURL", Key::OpenURL),
|
||||||
|
("PopKeyLabels", Key::PopKeyLabels),
|
||||||
|
("PushKeyLabels", Key::PushKeyLabels),
|
||||||
|
("RemoteHost", Key::RemoteHost),
|
||||||
|
("ReportCellSize", Key::ReportCellSize),
|
||||||
|
("ReportVariable", Key::ReportVariable),
|
||||||
|
("RequestAttention", Key::RequestAttention),
|
||||||
|
("RequestUpload", Key::RequestUpload),
|
||||||
|
("SetBackgroundImageFile", Key::SetBackgroundImageFile),
|
||||||
|
("SetBadgeFormat", Key::SetBadgeFormat),
|
||||||
|
("SetColors", Key::SetColors),
|
||||||
|
("SetKeyLabel", Key::SetKeyLabel),
|
||||||
|
("SetMark", Key::SetMark),
|
||||||
|
("SetProfile", Key::SetProfile),
|
||||||
|
("SetUserVar", Key::SetUserVar),
|
||||||
|
("ShellIntegrationVersion", Key::ShellIntegrationVersion),
|
||||||
|
("StealFocus", Key::StealFocus),
|
||||||
|
("UnicodeVersion", Key::UnicodeVersion),
|
||||||
|
];
|
||||||
|
|
||||||
|
fn lookup(s: &[u8]) -> Option<Key> {
|
||||||
|
KEYS.iter()
|
||||||
|
.find(|(name, _)| name.as_bytes().eq_ignore_ascii_case(s))
|
||||||
|
.map(|(_, k)| *k)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse OSC 1337
|
||||||
|
/// https://iterm2.com/documentation-escape-codes.html
|
||||||
|
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||||
|
debug_assert_eq!(parser.state, State::S1337);
|
||||||
|
|
||||||
|
let data = parser.data()?;
|
||||||
|
|
||||||
|
let (key_str, value): (&[u8], Option<&[u8]>) = match data.iter().position(|&b| b == b'=') {
|
||||||
|
None => (&data[..], None),
|
||||||
|
Some(index) => (&data[..index], Some(&data[index + 1..])),
|
||||||
|
};
|
||||||
|
|
||||||
|
// (Zig sets `parser.command = .invalid` and returns null on all of the
|
||||||
|
// failure paths below.)
|
||||||
|
let key = lookup(key_str)?;
|
||||||
|
|
||||||
|
match key {
|
||||||
|
Key::Copy => {
|
||||||
|
let value = value?;
|
||||||
|
|
||||||
|
// Sending a blank entry to clear the clipboard is an OSC 52-ism,
|
||||||
|
// make sure that is invalid here.
|
||||||
|
if value.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// base64 value must be prefixed by a colon
|
||||||
|
if value[0] != b':' {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = &value[1..];
|
||||||
|
|
||||||
|
// Sending a blank entry to clear the clipboard is an OSC 52-ism,
|
||||||
|
// make sure that is invalid here.
|
||||||
|
if value.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sending a '?' to query the clipboard is an OSC 52-ism, make sure
|
||||||
|
// that is invalid here.
|
||||||
|
if value == b"?" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// It would be better to check for valid base64 data here, but that
|
||||||
|
// would mean parsing the base64 data twice in the "normal" case.
|
||||||
|
|
||||||
|
Some(Command::ClipboardContents {
|
||||||
|
kind: b'c',
|
||||||
|
data: value.to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Key::CurrentDir => {
|
||||||
|
let value = value?;
|
||||||
|
if value.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Command::ReportPwd {
|
||||||
|
value: value.to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
log::debug!("unimplemented OSC 1337: {key:?}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used, clippy::panic)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn run(input: &str) -> Option<Command> {
|
||||||
|
let mut p = Parser::init(true);
|
||||||
|
for ch in input.bytes() {
|
||||||
|
p.next(ch);
|
||||||
|
}
|
||||||
|
p.end(Some(0x1b))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_1337_test_valid_unimplemented_key_with_no_value() {
|
||||||
|
assert!(run("1337;SetBadgeFormat").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_1337_test_valid_unimplemented_key_with_empty_value() {
|
||||||
|
assert!(run("1337;SetBadgeFormat=").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_1337_test_valid_unimplemented_key_with_non_empty_value() {
|
||||||
|
assert!(run("1337;SetBadgeFormat=abc123").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_1337_test_valid_key_with_lower_case_and_with_no_value() {
|
||||||
|
assert!(run("1337;setbadgeformat").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_1337_test_valid_key_with_lower_case_and_with_empty_value() {
|
||||||
|
assert!(run("1337;setbadgeformat=").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_1337_test_valid_key_with_lower_case_and_with_non_empty_value() {
|
||||||
|
assert!(run("1337;setbadgeformat=abc123").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_1337_test_invalid_key_with_no_value() {
|
||||||
|
assert!(run("1337;BobrKurwa").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_1337_test_invalid_key_with_empty_value() {
|
||||||
|
assert!(run("1337;BobrKurwa=").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_1337_test_invalid_key_with_non_empty_value() {
|
||||||
|
assert!(run("1337;BobrKurwa=abc123").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_1337_test_copy_with_no_value() {
|
||||||
|
assert!(run("1337;Copy").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_1337_test_copy_with_empty_value() {
|
||||||
|
assert!(run("1337;Copy=").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_1337_test_copy_with_only_prefix_colon() {
|
||||||
|
assert!(run("1337;Copy=:").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_1337_test_copy_with_question_mark() {
|
||||||
|
assert!(run("1337;Copy=:?").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skipped in Zig too (`return error.SkipZigTest`): for performance reasons
|
||||||
|
// base64 validity is not checked.
|
||||||
|
#[test]
|
||||||
|
#[ignore = "skipped in Zig: base64 is not validated for performance"]
|
||||||
|
fn osc_1337_test_copy_with_non_empty_value_that_is_invalid_base64() {
|
||||||
|
assert!(run("1337;Copy=:abc123").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_1337_test_copy_with_non_empty_value_that_is_valid_base64_but_not_prefixed_with_a_colon()
|
||||||
|
{
|
||||||
|
assert!(run("1337;Copy=YWJjMTIz").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_1337_test_copy_with_non_empty_value_that_is_valid_base64() {
|
||||||
|
assert_eq!(
|
||||||
|
run("1337;Copy=:YWJjMTIz"),
|
||||||
|
Some(Command::ClipboardContents {
|
||||||
|
kind: b'c',
|
||||||
|
data: b"YWJjMTIz".to_vec()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_1337_test_current_dir_with_no_value() {
|
||||||
|
assert!(run("1337;CurrentDir").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_1337_test_current_dir_with_empty_value() {
|
||||||
|
assert!(run("1337;CurrentDir=").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_1337_test_current_dir_with_non_empty_value() {
|
||||||
|
assert_eq!(
|
||||||
|
run("1337;CurrentDir=abc123"),
|
||||||
|
Some(Command::ReportPwd {
|
||||||
|
value: b"abc123".to_vec()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,658 @@
|
|||||||
|
//! Kitty's clipboard protocol (OSC 5522)
|
||||||
|
//! Specification: https://sw.kovidgoyal.net/kitty/clipboard/
|
||||||
|
//! https://rockorager.dev/misc/bracketed-paste-mime/
|
||||||
|
//! (ZIG/src/terminal/osc/parsers/kitty_clipboard_protocol.zig)
|
||||||
|
|
||||||
|
use crate::osc::{Command, Parser, State, Terminator};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct OSC {
|
||||||
|
/// The raw metadata that was received. It can be parsed by using the
|
||||||
|
/// `read_option` method (or the typed helpers).
|
||||||
|
pub metadata: Vec<u8>,
|
||||||
|
/// The raw payload. It may be Base64 encoded, check the `e` option.
|
||||||
|
pub payload: Option<Vec<u8>>,
|
||||||
|
/// The terminator that was used in case we need to send a response.
|
||||||
|
pub terminator: Terminator,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Value of a decoded option (Zig `Option.Type()`).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum OptionValue<'a> {
|
||||||
|
Str(&'a [u8]),
|
||||||
|
Loc(Location),
|
||||||
|
Status(Status),
|
||||||
|
Type(Operation),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OSC {
|
||||||
|
/// Decode an option from the metadata.
|
||||||
|
pub fn read_option(&self, key: Opt) -> Option<OptionValue<'_>> {
|
||||||
|
key.read(&self.metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> Option<&[u8]> {
|
||||||
|
match self.read_option(Opt::Id)? {
|
||||||
|
OptionValue::Str(s) => Some(s),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn loc(&self) -> Option<Location> {
|
||||||
|
match self.read_option(Opt::Loc)? {
|
||||||
|
OptionValue::Loc(v) => Some(v),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mime(&self) -> Option<&[u8]> {
|
||||||
|
match self.read_option(Opt::Mime)? {
|
||||||
|
OptionValue::Str(s) => Some(s),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> Option<&[u8]> {
|
||||||
|
match self.read_option(Opt::Name)? {
|
||||||
|
OptionValue::Str(s) => Some(s),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn password(&self) -> Option<&[u8]> {
|
||||||
|
match self.read_option(Opt::Password)? {
|
||||||
|
OptionValue::Str(s) => Some(s),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pw(&self) -> Option<&[u8]> {
|
||||||
|
match self.read_option(Opt::Pw)? {
|
||||||
|
OptionValue::Str(s) => Some(s),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn status(&self) -> Option<Status> {
|
||||||
|
match self.read_option(Opt::Status)? {
|
||||||
|
OptionValue::Status(v) => Some(v),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn type_(&self) -> Option<Operation> {
|
||||||
|
match self.read_option(Opt::Type)? {
|
||||||
|
OptionValue::Type(v) => Some(v),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Location {
|
||||||
|
Primary,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Location {
|
||||||
|
pub fn init(s: &[u8]) -> Option<Location> {
|
||||||
|
match s {
|
||||||
|
b"primary" => Some(Location::Primary),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Operation {
|
||||||
|
Read,
|
||||||
|
Walias,
|
||||||
|
Wdata,
|
||||||
|
Write,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Operation {
|
||||||
|
pub fn init(s: &[u8]) -> Option<Operation> {
|
||||||
|
match s {
|
||||||
|
b"read" => Some(Operation::Read),
|
||||||
|
b"walias" => Some(Operation::Walias),
|
||||||
|
b"wdata" => Some(Operation::Wdata),
|
||||||
|
b"write" => Some(Operation::Write),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
|
pub enum Status {
|
||||||
|
DATA,
|
||||||
|
DONE,
|
||||||
|
EBUSY,
|
||||||
|
EINVAL,
|
||||||
|
EIO,
|
||||||
|
ENOSYS,
|
||||||
|
EPERM,
|
||||||
|
OK,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Status {
|
||||||
|
pub fn init(s: &[u8]) -> Option<Status> {
|
||||||
|
match s {
|
||||||
|
b"DATA" => Some(Status::DATA),
|
||||||
|
b"DONE" => Some(Status::DONE),
|
||||||
|
b"EBUSY" => Some(Status::EBUSY),
|
||||||
|
b"EINVAL" => Some(Status::EINVAL),
|
||||||
|
b"EIO" => Some(Status::EIO),
|
||||||
|
b"ENOSYS" => Some(Status::ENOSYS),
|
||||||
|
b"EPERM" => Some(Status::EPERM),
|
||||||
|
b"OK" => Some(Status::OK),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Option keys (Zig `Option`).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Opt {
|
||||||
|
Id,
|
||||||
|
Loc,
|
||||||
|
Mime,
|
||||||
|
Name,
|
||||||
|
Password,
|
||||||
|
Pw,
|
||||||
|
Status,
|
||||||
|
Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zig `std.ascii.isWhitespace`.
|
||||||
|
fn is_ws(c: u8) -> bool {
|
||||||
|
matches!(c, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trim_ws(mut s: &[u8]) -> &[u8] {
|
||||||
|
while let [first, rest @ ..] = s {
|
||||||
|
if is_ws(*first) {
|
||||||
|
s = rest;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while let [rest @ .., last] = s {
|
||||||
|
if is_ws(*last) {
|
||||||
|
s = rest;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn index_of_from(hay: &[u8], from: usize, needle: u8) -> Option<usize> {
|
||||||
|
hay[from..]
|
||||||
|
.iter()
|
||||||
|
.position(|&c| c == needle)
|
||||||
|
.map(|i| i + from)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Opt {
|
||||||
|
pub fn tag_name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Opt::Id => "id",
|
||||||
|
Opt::Loc => "loc",
|
||||||
|
Opt::Mime => "mime",
|
||||||
|
Opt::Name => "name",
|
||||||
|
Opt::Password => "password",
|
||||||
|
Opt::Pw => "pw",
|
||||||
|
Opt::Status => "status",
|
||||||
|
Opt::Type => "type",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the option value from the raw metadata string.
|
||||||
|
pub fn read(self, metadata: &[u8]) -> Option<OptionValue<'_>> {
|
||||||
|
let tag = self.tag_name().as_bytes();
|
||||||
|
let value: &[u8] = 'value: {
|
||||||
|
let mut pos: usize = 0;
|
||||||
|
while pos < metadata.len() {
|
||||||
|
// skip any whitespace
|
||||||
|
while pos < metadata.len() && is_ws(metadata[pos]) {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
// bail if we are out of metadata
|
||||||
|
if pos >= metadata.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !metadata[pos..].starts_with(tag) {
|
||||||
|
// this isn't the key we are looking for, skip to the next option, or bail if
|
||||||
|
// there is no next option
|
||||||
|
pos = index_of_from(metadata, pos, b':')?;
|
||||||
|
pos += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// skip past the key
|
||||||
|
pos += tag.len();
|
||||||
|
// skip any whitespace
|
||||||
|
while pos < metadata.len() && is_ws(metadata[pos]) {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
// bail if we are out of metadata
|
||||||
|
if pos >= metadata.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// a valid option has an '='
|
||||||
|
if metadata[pos] != b'=' {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// the end of the value is bounded by a ':' or the end of the metadata
|
||||||
|
let end = index_of_from(metadata, pos, b':').unwrap_or(metadata.len());
|
||||||
|
let start = pos + 1;
|
||||||
|
// strip any leading or trailing whitespace
|
||||||
|
break 'value trim_ws(&metadata[start..end]);
|
||||||
|
}
|
||||||
|
// the key was not found
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
// return the parsed value
|
||||||
|
match self {
|
||||||
|
Opt::Id => parse_identifier(value).map(OptionValue::Str),
|
||||||
|
Opt::Loc => Location::init(value).map(OptionValue::Loc),
|
||||||
|
Opt::Mime | Opt::Name | Opt::Password | Opt::Pw => Some(OptionValue::Str(value)),
|
||||||
|
Opt::Status => Status::init(value).map(OptionValue::Status),
|
||||||
|
Opt::Type => Operation::init(value).map(OptionValue::Type),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Characters that are valid in identifiers.
|
||||||
|
const VALID_IDENTIFIER_CHARACTERS: &[u8] =
|
||||||
|
b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_+.";
|
||||||
|
|
||||||
|
fn is_valid_identifier(s: &[u8]) -> bool {
|
||||||
|
if s.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
s.iter().all(|c| VALID_IDENTIFIER_CHARACTERS.contains(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_identifier(s: &[u8]) -> Option<&[u8]> {
|
||||||
|
if is_valid_identifier(s) {
|
||||||
|
Some(s)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(parser: &mut Parser, terminator_ch: Option<u8>) -> Option<Command> {
|
||||||
|
debug_assert_eq!(parser.state, State::S5522);
|
||||||
|
|
||||||
|
let data = parser.buffered()?;
|
||||||
|
|
||||||
|
let (metadata, payload) = match data.iter().position(|&c| c == b';') {
|
||||||
|
None => (data, None),
|
||||||
|
Some(start) => (data[..start].to_vec(), Some(data[start + 1..].to_vec())),
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Command::KittyClipboardProtocol(OSC {
|
||||||
|
metadata,
|
||||||
|
payload,
|
||||||
|
terminator: Terminator::init(terminator_ch),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn run(input: &str) -> OSC {
|
||||||
|
let mut p = Parser::init(true);
|
||||||
|
for &ch in input.as_bytes() {
|
||||||
|
p.next(ch);
|
||||||
|
}
|
||||||
|
match p.end(Some(0x1b)) {
|
||||||
|
Some(Command::KittyClipboardProtocol(o)) => o,
|
||||||
|
other => panic!("unexpected {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::option_option)]
|
||||||
|
/// Expected values for every option; `None` means "must be null".
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Exp<'a> {
|
||||||
|
metadata: Option<&'a str>,
|
||||||
|
payload: Option<Option<&'a str>>,
|
||||||
|
id: Option<&'a str>,
|
||||||
|
loc: Option<Location>,
|
||||||
|
mime: Option<&'a str>,
|
||||||
|
name: Option<&'a str>,
|
||||||
|
password: Option<&'a str>,
|
||||||
|
pw: Option<&'a str>,
|
||||||
|
status: Option<Status>,
|
||||||
|
type_: Option<Operation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(c: &OSC, e: &Exp) {
|
||||||
|
if let Some(m) = e.metadata {
|
||||||
|
assert_eq!(m.as_bytes(), &c.metadata[..]);
|
||||||
|
}
|
||||||
|
if let Some(p) = e.payload {
|
||||||
|
assert_eq!(p.map(|s| s.as_bytes().to_vec()), c.payload);
|
||||||
|
}
|
||||||
|
assert_eq!(e.id.map(str::as_bytes), c.id());
|
||||||
|
assert_eq!(e.loc, c.loc());
|
||||||
|
assert_eq!(e.mime.map(str::as_bytes), c.mime());
|
||||||
|
assert_eq!(e.name.map(str::as_bytes), c.name());
|
||||||
|
assert_eq!(e.password.map(str::as_bytes), c.password());
|
||||||
|
assert_eq!(e.pw.map(str::as_bytes), c.pw());
|
||||||
|
assert_eq!(e.status, c.status());
|
||||||
|
assert_eq!(e.type_, c.type_());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_empty_metadata_and_missing_payload() {
|
||||||
|
let c = run("5522;");
|
||||||
|
check(
|
||||||
|
&c,
|
||||||
|
&Exp {
|
||||||
|
metadata: Some(""),
|
||||||
|
payload: Some(None),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_empty_metadata_and_empty_payload() {
|
||||||
|
let c = run("5522;;");
|
||||||
|
check(
|
||||||
|
&c,
|
||||||
|
&Exp {
|
||||||
|
metadata: Some(""),
|
||||||
|
payload: Some(Some("")),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_non_empty_metadata_and_payload() {
|
||||||
|
let c = run("5522;type=read;dGV4dC9wbGFpbg==");
|
||||||
|
check(
|
||||||
|
&c,
|
||||||
|
&Exp {
|
||||||
|
metadata: Some("type=read"),
|
||||||
|
payload: Some(Some("dGV4dC9wbGFpbg==")),
|
||||||
|
type_: Some(Operation::Read),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_empty_id() {
|
||||||
|
let c = run("5522;id=");
|
||||||
|
assert!(c.id().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_valid_id() {
|
||||||
|
let c = run("5522;id=5c076ad9-d36f-4705-847b-d4dbf356cc0d");
|
||||||
|
assert_eq!(b"5c076ad9-d36f-4705-847b-d4dbf356cc0d", c.id().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_invalid_id() {
|
||||||
|
let c = run("5522;id=*42*");
|
||||||
|
assert!(c.id().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_invalid_status() {
|
||||||
|
let c = run("5522;status=BOBR");
|
||||||
|
assert!(c.status().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_valid_status() {
|
||||||
|
let c = run("5522;status=DONE");
|
||||||
|
assert_eq!(Some(Status::DONE), c.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_invalid_location() {
|
||||||
|
let c = run("5522;loc=bobr");
|
||||||
|
assert!(c.loc().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_valid_location() {
|
||||||
|
let c = run("5522;loc=primary");
|
||||||
|
assert_eq!(Some(Location::Primary), c.loc());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_password_1() {
|
||||||
|
let c = run("5522;pw=R2hvc3R0eQ==:name=Qk9CUiBLVVJXQQ==");
|
||||||
|
assert_eq!(b"R2hvc3R0eQ==", c.pw().unwrap());
|
||||||
|
assert_eq!(b"Qk9CUiBLVVJXQQ==", c.name().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_password_2() {
|
||||||
|
let c = run("5522;password=R2hvc3R0eQ==");
|
||||||
|
assert_eq!(b"R2hvc3R0eQ==", c.password().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_example_1() {
|
||||||
|
let c = run("5522;type=read:status=OK");
|
||||||
|
check(
|
||||||
|
&c,
|
||||||
|
&Exp {
|
||||||
|
payload: Some(None),
|
||||||
|
status: Some(Status::OK),
|
||||||
|
type_: Some(Operation::Read),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_example_2() {
|
||||||
|
let c = run("5522;type=read:mime=dGV4dC9wbGFpbg==;R2hvc3R0eQ==");
|
||||||
|
check(
|
||||||
|
&c,
|
||||||
|
&Exp {
|
||||||
|
payload: Some(Some("R2hvc3R0eQ==")),
|
||||||
|
mime: Some("dGV4dC9wbGFpbg=="),
|
||||||
|
type_: Some(Operation::Read),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_example_3() {
|
||||||
|
let c = run("5522;type=read:status=OK");
|
||||||
|
check(
|
||||||
|
&c,
|
||||||
|
&Exp {
|
||||||
|
payload: Some(None),
|
||||||
|
status: Some(Status::OK),
|
||||||
|
type_: Some(Operation::Read),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_example_4() {
|
||||||
|
let c = run("5522;type=write");
|
||||||
|
check(
|
||||||
|
&c,
|
||||||
|
&Exp {
|
||||||
|
payload: Some(None),
|
||||||
|
type_: Some(Operation::Write),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_example_5() {
|
||||||
|
let c = run("5522;type=wdata:mime=dGV4dC9wbGFpbg==;R2hvc3R0eQ==");
|
||||||
|
check(
|
||||||
|
&c,
|
||||||
|
&Exp {
|
||||||
|
payload: Some(Some("R2hvc3R0eQ==")),
|
||||||
|
mime: Some("dGV4dC9wbGFpbg=="),
|
||||||
|
type_: Some(Operation::Wdata),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_example_6() {
|
||||||
|
let c = run("5522;type=wdata");
|
||||||
|
check(
|
||||||
|
&c,
|
||||||
|
&Exp {
|
||||||
|
payload: Some(None),
|
||||||
|
type_: Some(Operation::Wdata),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_example_7() {
|
||||||
|
let c = run("5522;type=write:status=DONE");
|
||||||
|
check(
|
||||||
|
&c,
|
||||||
|
&Exp {
|
||||||
|
payload: Some(None),
|
||||||
|
status: Some(Status::DONE),
|
||||||
|
type_: Some(Operation::Write),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_example_8() {
|
||||||
|
let c = run("5522;type=write:status=EPERM");
|
||||||
|
check(
|
||||||
|
&c,
|
||||||
|
&Exp {
|
||||||
|
payload: Some(None),
|
||||||
|
status: Some(Status::EPERM),
|
||||||
|
type_: Some(Operation::Write),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_example_9() {
|
||||||
|
let c = run("5522;type=walias:mime=dGV4dC9wbGFpbg==;dGV4dC9odG1sIGFwcGxpY2F0aW9uL2pzb24=");
|
||||||
|
check(
|
||||||
|
&c,
|
||||||
|
&Exp {
|
||||||
|
payload: Some(Some("dGV4dC9odG1sIGFwcGxpY2F0aW9uL2pzb24=")),
|
||||||
|
mime: Some("dGV4dC9wbGFpbg=="),
|
||||||
|
type_: Some(Operation::Walias),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_example_10() {
|
||||||
|
let c = run("5522;type=read:status=OK:password=Qk9CUiBLVVJXQQ==");
|
||||||
|
check(
|
||||||
|
&c,
|
||||||
|
&Exp {
|
||||||
|
payload: Some(None),
|
||||||
|
password: Some("Qk9CUiBLVVJXQQ=="),
|
||||||
|
status: Some(Status::OK),
|
||||||
|
type_: Some(Operation::Read),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_example_11() {
|
||||||
|
let c = run("5522;type=read:status=DATA:mime=dGV4dC9wbGFpbg==");
|
||||||
|
check(
|
||||||
|
&c,
|
||||||
|
&Exp {
|
||||||
|
payload: Some(None),
|
||||||
|
mime: Some("dGV4dC9wbGFpbg=="),
|
||||||
|
status: Some(Status::DATA),
|
||||||
|
type_: Some(Operation::Read),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_example_12() {
|
||||||
|
let c = run("5522;type=read:mime=dGV4dC9wbGFpbg==:password=Qk9CUiBLVVJXQQ==");
|
||||||
|
check(
|
||||||
|
&c,
|
||||||
|
&Exp {
|
||||||
|
payload: Some(None),
|
||||||
|
mime: Some("dGV4dC9wbGFpbg=="),
|
||||||
|
password: Some("Qk9CUiBLVVJXQQ=="),
|
||||||
|
type_: Some(Operation::Read),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_example_13() {
|
||||||
|
let c = run("5522;type=read:status=OK");
|
||||||
|
check(
|
||||||
|
&c,
|
||||||
|
&Exp {
|
||||||
|
payload: Some(None),
|
||||||
|
status: Some(Status::OK),
|
||||||
|
type_: Some(Operation::Read),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_example_14() {
|
||||||
|
let c = run("5522;type=read:status=DATA:mime=dGV4dC9wbGFpbg==;Qk9CUiBLVVJXQQ==");
|
||||||
|
check(
|
||||||
|
&c,
|
||||||
|
&Exp {
|
||||||
|
payload: Some(Some("Qk9CUiBLVVJXQQ==")),
|
||||||
|
mime: Some("dGV4dC9wbGFpbg=="),
|
||||||
|
status: Some(Status::DATA),
|
||||||
|
type_: Some(Operation::Read),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_5522_example_15() {
|
||||||
|
let c = run("5522;type=read:status=OK");
|
||||||
|
check(
|
||||||
|
&c,
|
||||||
|
&Exp {
|
||||||
|
payload: Some(None),
|
||||||
|
status: Some(Status::OK),
|
||||||
|
type_: Some(Operation::Read),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
//! OSC 21, the Kitty Color Protocol (ZIG/src/terminal/osc/parsers/kitty_color.zig).
|
||||||
|
|
||||||
|
use crate::color::Rgb;
|
||||||
|
use crate::kitty::color::{Kind, OSC, Request};
|
||||||
|
use crate::osc::{Command, Parser, State, Terminator};
|
||||||
|
|
||||||
|
/// Parse OSC 21, the Kitty Color Protocol.
|
||||||
|
pub fn parse(parser: &mut Parser, terminator_ch: Option<u8>) -> Option<Command> {
|
||||||
|
debug_assert_eq!(parser.state, State::S21);
|
||||||
|
|
||||||
|
if !parser.alloc {
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !parser.has_writer() {
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let data = parser.buffered()?;
|
||||||
|
|
||||||
|
let mut osc = OSC {
|
||||||
|
list: Vec::new(),
|
||||||
|
terminator: Terminator::init(terminator_ch),
|
||||||
|
};
|
||||||
|
for kv in data.split(|&c| c == b';') {
|
||||||
|
if osc.list.len() >= Kind::MAX * 2 {
|
||||||
|
log::warn!("exceeded limit for number of keys in kitty color protocol, ignoring");
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (k, rest) = match kv.iter().position(|&c| c == b'=') {
|
||||||
|
Some(p) => (&kv[..p], &kv[p + 1..]),
|
||||||
|
None => (kv, &kv[kv.len()..]),
|
||||||
|
};
|
||||||
|
if k.is_empty() {
|
||||||
|
log::warn!("zero length key in kitty color protocol");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(key) = Kind::parse(k) else {
|
||||||
|
log::warn!(
|
||||||
|
"unknown key in kitty color protocol: {}",
|
||||||
|
String::from_utf8_lossy(k)
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let value = rest.trim_ascii_matching(b' ');
|
||||||
|
if value.is_empty() {
|
||||||
|
osc.list.push(Request::Reset(key));
|
||||||
|
} else if value == b"?" {
|
||||||
|
osc.list.push(Request::Query(key));
|
||||||
|
} else if let Ok(color) = Rgb::parse_bytes(value) {
|
||||||
|
osc.list.push(Request::Set { key, color });
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"invalid color format in kitty color protocol: {}",
|
||||||
|
String::from_utf8_lossy(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Command::KittyColorProtocol(osc))
|
||||||
|
}
|
||||||
|
|
||||||
|
trait TrimMatching {
|
||||||
|
fn trim_ascii_matching(&self, c: u8) -> &[u8];
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrimMatching for [u8] {
|
||||||
|
fn trim_ascii_matching(&self, c: u8) -> &[u8] {
|
||||||
|
let mut s = self;
|
||||||
|
while let [first, rest @ ..] = s {
|
||||||
|
if *first != c {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
s = rest;
|
||||||
|
}
|
||||||
|
while let [rest @ .., last] = s {
|
||||||
|
if *last != c {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
s = rest;
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use super::*;
|
||||||
|
use crate::kitty::color::Special;
|
||||||
|
|
||||||
|
const INPUT: &str = "21;foreground=?;background=rgb:f0/f8/ff;cursor=aliceblue;cursor_text;visual_bell=;selection_foreground=#xxxyyzz;selection_background=?;selection_background=#aabbcc;2=?;3=rgbi:1.0/1.0/1.0";
|
||||||
|
|
||||||
|
fn feed(p: &mut Parser, s: &str) {
|
||||||
|
for ch in s.bytes() {
|
||||||
|
p.next(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rgb(r: u8, g: u8, b: u8) -> Rgb {
|
||||||
|
Rgb { r, g, b }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_21_kitty_color_protocol() {
|
||||||
|
let mut p = Parser::init(true);
|
||||||
|
feed(&mut p, INPUT);
|
||||||
|
|
||||||
|
let cmd = p.end(Some(0x1b)).unwrap();
|
||||||
|
let Command::KittyColorProtocol(osc) = cmd else {
|
||||||
|
panic!("wrong command");
|
||||||
|
};
|
||||||
|
let list = osc.list;
|
||||||
|
assert_eq!(9, list.len());
|
||||||
|
assert_eq!(Request::Query(Kind::Special(Special::Foreground)), list[0]);
|
||||||
|
assert_eq!(
|
||||||
|
Request::Set {
|
||||||
|
key: Kind::Special(Special::Background),
|
||||||
|
color: rgb(0xf0, 0xf8, 0xff)
|
||||||
|
},
|
||||||
|
list[1]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Request::Set {
|
||||||
|
key: Kind::Special(Special::Cursor),
|
||||||
|
color: rgb(0xf0, 0xf8, 0xff)
|
||||||
|
},
|
||||||
|
list[2]
|
||||||
|
);
|
||||||
|
assert_eq!(Request::Reset(Kind::Special(Special::CursorText)), list[3]);
|
||||||
|
assert_eq!(Request::Reset(Kind::Special(Special::VisualBell)), list[4]);
|
||||||
|
assert_eq!(
|
||||||
|
Request::Query(Kind::Special(Special::SelectionBackground)),
|
||||||
|
list[5]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Request::Set {
|
||||||
|
key: Kind::Special(Special::SelectionBackground),
|
||||||
|
color: rgb(0xaa, 0xbb, 0xcc)
|
||||||
|
},
|
||||||
|
list[6]
|
||||||
|
);
|
||||||
|
assert_eq!(Request::Query(Kind::Palette(2)), list[7]);
|
||||||
|
assert_eq!(
|
||||||
|
Request::Set {
|
||||||
|
key: Kind::Palette(3),
|
||||||
|
color: rgb(0xff, 0xff, 0xff)
|
||||||
|
},
|
||||||
|
list[8]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_21_kitty_color_protocol_without_allocator() {
|
||||||
|
let mut p = Parser::init(false);
|
||||||
|
feed(&mut p, "21;foreground=?");
|
||||||
|
assert!(p.end(Some(0x1b)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_21_kitty_color_protocol_double_reset() {
|
||||||
|
let mut p = Parser::init(true);
|
||||||
|
feed(&mut p, INPUT);
|
||||||
|
let cmd = p.end(Some(0x1b)).unwrap();
|
||||||
|
assert!(matches!(cmd, Command::KittyColorProtocol(_)));
|
||||||
|
|
||||||
|
p.reset();
|
||||||
|
p.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_21_kitty_color_protocol_reset_after_invalid() {
|
||||||
|
let mut p = Parser::init(true);
|
||||||
|
feed(&mut p, INPUT);
|
||||||
|
let cmd = p.end(Some(0x1b)).unwrap();
|
||||||
|
assert!(matches!(cmd, Command::KittyColorProtocol(_)));
|
||||||
|
|
||||||
|
p.reset();
|
||||||
|
|
||||||
|
assert_eq!(State::Start, p.state);
|
||||||
|
p.next(b'X');
|
||||||
|
assert_eq!(State::Invalid, p.state);
|
||||||
|
|
||||||
|
p.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_21_kitty_color_protocol_no_key() {
|
||||||
|
let mut p = Parser::init(true);
|
||||||
|
feed(&mut p, "21;");
|
||||||
|
let cmd = p.end(Some(0x1b)).unwrap();
|
||||||
|
let Command::KittyColorProtocol(osc) = cmd else {
|
||||||
|
panic!("wrong command");
|
||||||
|
};
|
||||||
|
assert_eq!(0, osc.list.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
//! Kitty's text sizing protocol (OSC 66)
|
||||||
|
//! Specification: https://sw.kovidgoyal.net/kitty/text-sizing-protocol/
|
||||||
|
//! (ZIG/src/terminal/osc/parsers/kitty_text_sizing.zig)
|
||||||
|
|
||||||
|
use crate::osc::{Command, Parser, State, encoding};
|
||||||
|
|
||||||
|
pub const MAX_PAYLOAD_LENGTH: usize = 4096;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum VAlign {
|
||||||
|
#[default]
|
||||||
|
Top,
|
||||||
|
Bottom,
|
||||||
|
Center,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VAlign {
|
||||||
|
fn from_int(v: u8) -> Option<VAlign> {
|
||||||
|
match v {
|
||||||
|
0 => Some(VAlign::Top),
|
||||||
|
1 => Some(VAlign::Bottom),
|
||||||
|
2 => Some(VAlign::Center),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum HAlign {
|
||||||
|
#[default]
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Center,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HAlign {
|
||||||
|
fn from_int(v: u8) -> Option<HAlign> {
|
||||||
|
match v {
|
||||||
|
0 => Some(HAlign::Left),
|
||||||
|
1 => Some(HAlign::Right),
|
||||||
|
2 => Some(HAlign::Center),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct OSC {
|
||||||
|
/// 1 - 7 (u3)
|
||||||
|
pub scale: u8,
|
||||||
|
/// 0 - 7 (u3), 0 means default
|
||||||
|
pub width: u8,
|
||||||
|
/// u4
|
||||||
|
pub numerator: u8,
|
||||||
|
/// u4
|
||||||
|
pub denominator: u8,
|
||||||
|
pub valign: VAlign,
|
||||||
|
pub halign: HAlign,
|
||||||
|
pub text: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum UpdateError {
|
||||||
|
UnknownKey,
|
||||||
|
InvalidValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Equivalent of Zig `std.fmt.parseInt(u4, value, 10)`.
|
||||||
|
fn parse_u4(value: &[u8]) -> Option<u8> {
|
||||||
|
let (neg, digits) = match value.first() {
|
||||||
|
Some(b'+') => (false, &value[1..]),
|
||||||
|
Some(b'-') => (true, &value[1..]),
|
||||||
|
_ => (false, value),
|
||||||
|
};
|
||||||
|
if digits.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut acc: u32 = 0;
|
||||||
|
for &c in digits {
|
||||||
|
if !c.is_ascii_digit() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
acc = acc * 10 + (c - b'0') as u32;
|
||||||
|
if acc > 15 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if neg && acc != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(acc as u8)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OSC {
|
||||||
|
fn update(&mut self, key: u8, value: &[u8]) -> Result<(), UpdateError> {
|
||||||
|
// All values are numeric, so we can do a small hack here
|
||||||
|
let v = parse_u4(value).ok_or(UpdateError::InvalidValue)?;
|
||||||
|
|
||||||
|
match key {
|
||||||
|
b's' => {
|
||||||
|
if v == 0 || v > 7 {
|
||||||
|
return Err(UpdateError::InvalidValue);
|
||||||
|
}
|
||||||
|
self.scale = v;
|
||||||
|
}
|
||||||
|
b'w' => {
|
||||||
|
if v > 7 {
|
||||||
|
return Err(UpdateError::InvalidValue);
|
||||||
|
}
|
||||||
|
self.width = v;
|
||||||
|
}
|
||||||
|
b'n' => self.numerator = v,
|
||||||
|
b'd' => self.denominator = v,
|
||||||
|
b'v' => self.valign = VAlign::from_int(v).ok_or(UpdateError::InvalidValue)?,
|
||||||
|
b'h' => self.halign = HAlign::from_int(v).ok_or(UpdateError::InvalidValue)?,
|
||||||
|
_ => return Err(UpdateError::UnknownKey),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||||
|
debug_assert_eq!(parser.state, State::S66);
|
||||||
|
|
||||||
|
let data = parser.data()?;
|
||||||
|
|
||||||
|
let Some(payload_start) = data.iter().position(|&c| c == b';') else {
|
||||||
|
log::warn!("missing semicolon before payload");
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let payload = &data[payload_start + 1..];
|
||||||
|
|
||||||
|
// Payload has to be a URL-safe UTF-8 string,
|
||||||
|
// and be under the size limit.
|
||||||
|
if payload.len() > MAX_PAYLOAD_LENGTH {
|
||||||
|
log::warn!("payload is too long");
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !encoding::is_safe_utf8(payload) {
|
||||||
|
log::warn!("payload is not escape code safe UTF-8");
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cmd = OSC {
|
||||||
|
scale: 1,
|
||||||
|
width: 0,
|
||||||
|
numerator: 0,
|
||||||
|
denominator: 0,
|
||||||
|
valign: VAlign::Top,
|
||||||
|
halign: HAlign::Left,
|
||||||
|
text: payload.to_vec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse any arguments if given
|
||||||
|
if payload_start > 0 {
|
||||||
|
for kv in data[..payload_start].split(|&c| c == b':') {
|
||||||
|
let mut it = kv.split(|&c| c == b'=');
|
||||||
|
let Some(k) = it.next() else {
|
||||||
|
log::warn!("missing key");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if k.len() != 1 {
|
||||||
|
log::warn!("key must be a single character");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(value) = it.next() else {
|
||||||
|
log::warn!("missing value");
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = cmd.update(k[0], value) {
|
||||||
|
match err {
|
||||||
|
UpdateError::UnknownKey => log::warn!("unknown key: '{}'", k[0] as char),
|
||||||
|
UpdateError::InvalidValue => {
|
||||||
|
log::warn!("invalid value for key '{}': {:?}", k[0] as char, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Command::KittyTextSizing(cmd))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn run(input: &[u8]) -> Option<Command> {
|
||||||
|
let mut p = Parser::init(false);
|
||||||
|
for &ch in input {
|
||||||
|
p.next(ch);
|
||||||
|
}
|
||||||
|
p.end(Some(0x1b))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sizing(input: &[u8]) -> OSC {
|
||||||
|
match run(input) {
|
||||||
|
Some(Command::KittyTextSizing(o)) => o,
|
||||||
|
other => panic!("unexpected {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_66_empty_parameters() {
|
||||||
|
let c = sizing(b"66;;bobr");
|
||||||
|
assert_eq!(1, c.scale);
|
||||||
|
assert_eq!(b"bobr", &c.text[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_66_single_parameter() {
|
||||||
|
let c = sizing(b"66;s=2;kurwa");
|
||||||
|
assert_eq!(2, c.scale);
|
||||||
|
assert_eq!(b"kurwa", &c.text[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_66_multiple_parameters() {
|
||||||
|
let c = sizing(b"66;s=2:w=7:n=13:d=15:v=1:h=2;long");
|
||||||
|
assert_eq!(2, c.scale);
|
||||||
|
assert_eq!(7, c.width);
|
||||||
|
assert_eq!(13, c.numerator);
|
||||||
|
assert_eq!(15, c.denominator);
|
||||||
|
assert_eq!(VAlign::Bottom, c.valign);
|
||||||
|
assert_eq!(HAlign::Center, c.halign);
|
||||||
|
assert_eq!(b"long", &c.text[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_66_scale_is_zero() {
|
||||||
|
let c = sizing(b"66;s=0;nope");
|
||||||
|
assert_eq!(1, c.scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_66_invalid_parameters() {
|
||||||
|
let c = sizing(b"66;w=8:v=3:n=16;");
|
||||||
|
assert_eq!(0, c.width);
|
||||||
|
assert_eq!(VAlign::Top, c.valign);
|
||||||
|
assert_eq!(0, c.numerator);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_66_utf8() {
|
||||||
|
let c = sizing("66;;👻魑魅魍魉ゴースッティ".as_bytes());
|
||||||
|
assert_eq!("👻魑魅魍魉ゴースッティ".as_bytes(), &c.text[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_66_unsafe_utf8() {
|
||||||
|
assert!(run(b"66;;\n").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_66_overlong_utf8() {
|
||||||
|
let mut input = b"66;;".to_vec();
|
||||||
|
input.extend(b"bobr".repeat(1025));
|
||||||
|
assert!(run(&input).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
//! OSC parsers (ZIG/src/terminal/osc/parsers.zig).
|
||||||
|
|
||||||
|
pub mod change_window_icon;
|
||||||
|
pub mod change_window_title;
|
||||||
|
pub mod clipboard_operation;
|
||||||
|
pub mod color;
|
||||||
|
pub mod context_signal;
|
||||||
|
pub mod hyperlink;
|
||||||
|
pub mod iterm2;
|
||||||
|
pub mod kitty_clipboard_protocol;
|
||||||
|
pub mod kitty_color;
|
||||||
|
pub mod kitty_text_sizing;
|
||||||
|
pub mod mouse_shape;
|
||||||
|
pub mod osc9;
|
||||||
|
pub mod report_pwd;
|
||||||
|
pub mod rxvt_extension;
|
||||||
|
pub mod semantic_prompt;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
//! OSC 22 (ZIG/src/terminal/osc/parsers/mouse_shape.zig).
|
||||||
|
use crate::osc::{Command, Parser, State};
|
||||||
|
|
||||||
|
/// Parse OSC 22
|
||||||
|
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||||
|
debug_assert_eq!(parser.state, State::S22);
|
||||||
|
let data = parser.data()?;
|
||||||
|
Some(Command::MouseShape { value: data })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_22_pointer_cursor() {
|
||||||
|
let mut p = Parser::init(false);
|
||||||
|
for c in b"22;pointer" {
|
||||||
|
p.next(*c);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
p.end(None).unwrap(),
|
||||||
|
Command::MouseShape {
|
||||||
|
value: b"pointer".to_vec()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,557 @@
|
|||||||
|
//! OSC 9 (iTerm2 notification / ConEmu extensions)
|
||||||
|
//! (ZIG/src/terminal/osc/parsers/osc9.zig).
|
||||||
|
|
||||||
|
use super::semantic_prompt;
|
||||||
|
use crate::osc::{Command, ConemuChangeTabTitle, Parser, ProgressReport, ProgressState};
|
||||||
|
|
||||||
|
/// Parse a base-10 unsigned integer from raw bytes (Zig `parseUnsigned`).
|
||||||
|
fn parse_unsigned<T: std::str::FromStr>(bytes: &[u8]) -> Option<T> {
|
||||||
|
std::str::from_utf8(bytes).ok()?.parse().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse OSC 9, which could be an iTerm2 notification or a ConEmu extension.
|
||||||
|
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||||
|
// Zig prologue: writer must exist. (The NUL-terminator capacity check is
|
||||||
|
// folded into `data()`.)
|
||||||
|
let data = parser.data()?;
|
||||||
|
|
||||||
|
// Check first to see if this is a ConEmu OSC
|
||||||
|
// https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
|
||||||
|
if let Some(cmd) = conemu(&data) {
|
||||||
|
return Some(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not a ConEmu OSC, it's an iTerm2 notification
|
||||||
|
Some(Command::ShowDesktopNotification {
|
||||||
|
title: Vec::new(),
|
||||||
|
body: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns None where the Zig code does `break :conemu`.
|
||||||
|
fn conemu(data: &[u8]) -> Option<Command> {
|
||||||
|
if data.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let msg = |skip: usize| data[skip..].to_vec();
|
||||||
|
match data[0] {
|
||||||
|
// Check for OSC 9;1 9;10 9;11 9;12
|
||||||
|
b'1' => {
|
||||||
|
if data.len() < 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match data[1] {
|
||||||
|
// OSC 9;1 sleep
|
||||||
|
b';' => Some(Command::ConemuSleep {
|
||||||
|
duration_ms: match parse_unsigned::<u16>(&data[2..]) {
|
||||||
|
Some(num) => num.min(10_000),
|
||||||
|
None => 100,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// OSC 9;10 xterm keyboard and output emulation
|
||||||
|
b'0' => {
|
||||||
|
if data.len() == 2 {
|
||||||
|
return Some(Command::ConemuXtermEmulation {
|
||||||
|
keyboard: Some(true),
|
||||||
|
output: Some(true),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if data.len() < 4 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if data[2] != b';' {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (keyboard, output) = match data[3] {
|
||||||
|
b'0' => (Some(false), Some(false)),
|
||||||
|
b'1' => (Some(true), Some(true)),
|
||||||
|
b'2' => (None, Some(false)),
|
||||||
|
b'3' => (None, Some(true)),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(Command::ConemuXtermEmulation { keyboard, output })
|
||||||
|
}
|
||||||
|
// OSC 9;11 comment
|
||||||
|
b'1' => {
|
||||||
|
if data.len() < 3 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if data[2] != b';' {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Command::ConemuComment(msg(3)))
|
||||||
|
}
|
||||||
|
// OSC 9;12 mark prompt start
|
||||||
|
b'2' => Some(Command::SemanticPrompt(semantic_prompt::Command::init(
|
||||||
|
semantic_prompt::Action::FreshLineNewPrompt,
|
||||||
|
))),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// OSC 9;2 show message box
|
||||||
|
b'2' => {
|
||||||
|
if data.len() < 2 || data[1] != b';' {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Command::ConemuShowMessageBox(msg(2)))
|
||||||
|
}
|
||||||
|
// OSC 9;3 change tab title
|
||||||
|
b'3' => {
|
||||||
|
if data.len() < 2 || data[1] != b';' {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if data.len() == 2 {
|
||||||
|
return Some(Command::ConemuChangeTabTitle(ConemuChangeTabTitle::Reset));
|
||||||
|
}
|
||||||
|
Some(Command::ConemuChangeTabTitle(ConemuChangeTabTitle::Value(
|
||||||
|
msg(2),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
// OSC 9;4 progress report
|
||||||
|
b'4' => {
|
||||||
|
if data.len() < 2 || data[1] != b';' || data.len() < 3 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut report = match data[2] {
|
||||||
|
b'0' => ProgressReport {
|
||||||
|
state: ProgressState::Remove,
|
||||||
|
progress: None,
|
||||||
|
},
|
||||||
|
b'1' => ProgressReport {
|
||||||
|
state: ProgressState::Set,
|
||||||
|
progress: Some(0),
|
||||||
|
},
|
||||||
|
b'2' => ProgressReport {
|
||||||
|
state: ProgressState::Error,
|
||||||
|
progress: None,
|
||||||
|
},
|
||||||
|
b'3' => ProgressReport {
|
||||||
|
state: ProgressState::Indeterminate,
|
||||||
|
progress: None,
|
||||||
|
},
|
||||||
|
b'4' => ProgressReport {
|
||||||
|
state: ProgressState::Pause,
|
||||||
|
progress: None,
|
||||||
|
},
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
match report.state {
|
||||||
|
ProgressState::Remove | ProgressState::Indeterminate => {}
|
||||||
|
ProgressState::Set | ProgressState::Error | ProgressState::Pause => {
|
||||||
|
if data.len() >= 4 && data[3] == b';' {
|
||||||
|
// parse the progress value
|
||||||
|
report.progress =
|
||||||
|
parse_unsigned::<usize>(&data[4..]).map(|v| v.min(100) as u8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Command::ConemuProgressReport(report))
|
||||||
|
}
|
||||||
|
// OSC 9;5 wait for input
|
||||||
|
b'5' => Some(Command::ConemuWaitInput),
|
||||||
|
// OSC 9;6 guimacro
|
||||||
|
b'6' => {
|
||||||
|
if data.len() < 2 || data[1] != b';' {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Command::ConemuGuimacro(msg(2)))
|
||||||
|
}
|
||||||
|
// OSC 9;7 run process
|
||||||
|
b'7' => {
|
||||||
|
if data.len() < 2 || data[1] != b';' {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Command::ConemuRunProcess(msg(2)))
|
||||||
|
}
|
||||||
|
// OSC 9;8 output environment variable
|
||||||
|
b'8' => {
|
||||||
|
if data.len() < 2 || data[1] != b';' {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Command::ConemuOutputEnvironmentVariable(msg(2)))
|
||||||
|
}
|
||||||
|
// OSC 9;9 current working directory
|
||||||
|
b'9' => {
|
||||||
|
if data.len() < 2 || data[1] != b';' {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Command::ReportPwd { value: msg(2) })
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used, clippy::panic)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn run(input: &str) -> Command {
|
||||||
|
let mut p = Parser::init(false);
|
||||||
|
for ch in input.bytes() {
|
||||||
|
p.next(ch);
|
||||||
|
}
|
||||||
|
p.end(Some(0x1b)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notif(input: &str, body: &str) {
|
||||||
|
match run(input) {
|
||||||
|
Command::ShowDesktopNotification { title, body: b } => {
|
||||||
|
assert_eq!(title, b"");
|
||||||
|
assert_eq!(b, body.as_bytes());
|
||||||
|
}
|
||||||
|
c => panic!("unexpected {c:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn progress(input: &str, state: ProgressState, progress: Option<u8>) {
|
||||||
|
assert_eq!(
|
||||||
|
run(input),
|
||||||
|
Command::ConemuProgressReport(ProgressReport { state, progress })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xterm(input: &str, keyboard: Option<bool>, output: Option<bool>) {
|
||||||
|
assert_eq!(
|
||||||
|
run(input),
|
||||||
|
Command::ConemuXtermEmulation { keyboard, output }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_show_desktop_notification() {
|
||||||
|
notif("9;Hello world", "Hello world");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_show_single_character_desktop_notification() {
|
||||||
|
notif("9;H", "H");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_1_conemu_sleep() {
|
||||||
|
assert_eq!(run("9;1;420"), Command::ConemuSleep { duration_ms: 420 });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_1_conemu_sleep_with_no_value_default_to_100ms() {
|
||||||
|
assert_eq!(run("9;1;"), Command::ConemuSleep { duration_ms: 100 });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_1_conemu_sleep_cannot_exceed_10000ms() {
|
||||||
|
assert_eq!(
|
||||||
|
run("9;1;12345"),
|
||||||
|
Command::ConemuSleep { duration_ms: 10000 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_1_conemu_sleep_invalid_input() {
|
||||||
|
assert_eq!(run("9;1;foo"), Command::ConemuSleep { duration_ms: 100 });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_1_conemu_sleep_desktop_notification_1() {
|
||||||
|
notif("9;1", "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_1_conemu_sleep_desktop_notification_2() {
|
||||||
|
notif("9;1a", "1a");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_2_conemu_message_box() {
|
||||||
|
assert_eq!(
|
||||||
|
run("9;2;hello world"),
|
||||||
|
Command::ConemuShowMessageBox(b"hello world".to_vec())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_2_conemu_message_box_invalid_input() {
|
||||||
|
notif("9;2", "2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_2_conemu_message_box_empty_message() {
|
||||||
|
assert_eq!(run("9;2;"), Command::ConemuShowMessageBox(Vec::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_2_conemu_message_box_spaces_only_message() {
|
||||||
|
assert_eq!(
|
||||||
|
run("9;2; "),
|
||||||
|
Command::ConemuShowMessageBox(b" ".to_vec())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_2_message_box_desktop_notification_1() {
|
||||||
|
notif("9;2", "2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_2_message_box_desktop_notification_2() {
|
||||||
|
notif("9;2a", "2a");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_3_conemu_change_tab_title() {
|
||||||
|
assert_eq!(
|
||||||
|
run("9;3;foo bar"),
|
||||||
|
Command::ConemuChangeTabTitle(ConemuChangeTabTitle::Value(b"foo bar".to_vec()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_3_conemu_change_tab_title_reset() {
|
||||||
|
assert_eq!(
|
||||||
|
run("9;3;"),
|
||||||
|
Command::ConemuChangeTabTitle(ConemuChangeTabTitle::Reset)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_3_conemu_change_tab_title_spaces_only() {
|
||||||
|
assert_eq!(
|
||||||
|
run("9;3; "),
|
||||||
|
Command::ConemuChangeTabTitle(ConemuChangeTabTitle::Value(b" ".to_vec()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_3_change_tab_title_desktop_notification_1() {
|
||||||
|
notif("9;3", "3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_3_message_box_desktop_notification_2() {
|
||||||
|
notif("9;3a", "3a");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_4_conemu_progress_set() {
|
||||||
|
progress("9;4;1;100", ProgressState::Set, Some(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_4_conemu_progress_set_overflow() {
|
||||||
|
progress("9;4;1;900", ProgressState::Set, Some(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_4_conemu_progress_set_single_digit() {
|
||||||
|
progress("9;4;1;9", ProgressState::Set, Some(9));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_4_conemu_progress_set_double_digit() {
|
||||||
|
progress("9;4;1;94", ProgressState::Set, Some(94));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_4_conemu_progress_set_extra_semicolon_ignored() {
|
||||||
|
progress("9;4;1;100", ProgressState::Set, Some(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_4_conemu_progress_remove_with_no_progress() {
|
||||||
|
progress("9;4;0;", ProgressState::Remove, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_4_conemu_progress_remove_with_double_semicolon() {
|
||||||
|
progress("9;4;0;;", ProgressState::Remove, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_4_conemu_progress_remove_ignores_progress() {
|
||||||
|
progress("9;4;0;100", ProgressState::Remove, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_4_conemu_progress_remove_extra_semicolon() {
|
||||||
|
match run("9;4;0;100;") {
|
||||||
|
Command::ConemuProgressReport(r) => assert_eq!(r.state, ProgressState::Remove),
|
||||||
|
c => panic!("unexpected {c:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_4_conemu_progress_error() {
|
||||||
|
progress("9;4;2", ProgressState::Error, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_4_conemu_progress_error_with_progress() {
|
||||||
|
progress("9;4;2;100", ProgressState::Error, Some(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_4_progress_pause() {
|
||||||
|
progress("9;4;4", ProgressState::Pause, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_4_conemu_progress_pause_with_progress() {
|
||||||
|
progress("9;4;4;100", ProgressState::Pause, Some(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_4_progress_desktop_notification_1() {
|
||||||
|
notif("9;4", "4");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_4_progress_desktop_notification_2() {
|
||||||
|
notif("9;4;", "4;");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_4_progress_desktop_notification_3() {
|
||||||
|
notif("9;4;5", "4;5");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_4_progress_desktop_notification_4() {
|
||||||
|
notif("9;4;5a", "4;5a");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_5_conemu_wait_input() {
|
||||||
|
assert_eq!(run("9;5"), Command::ConemuWaitInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_5_conemu_wait_ignores_trailing_characters() {
|
||||||
|
assert_eq!(run("9;5;foo"), Command::ConemuWaitInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_6_conemu_guimacro_1() {
|
||||||
|
assert_eq!(run("9;6;a"), Command::ConemuGuimacro(b"a".to_vec()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_6_conemu_guimacro_2() {
|
||||||
|
assert_eq!(run("9;6;ab"), Command::ConemuGuimacro(b"ab".to_vec()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_6_conemu_guimacro_3_incomplete_desktop_notification() {
|
||||||
|
notif("9;6", "6");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_7_conemu_run_process_1() {
|
||||||
|
assert_eq!(run("9;7;ab"), Command::ConemuRunProcess(b"ab".to_vec()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_7_conemu_run_process_2() {
|
||||||
|
assert_eq!(run("9;7;"), Command::ConemuRunProcess(Vec::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_7_conemu_run_process_incomplete_desktop_notification() {
|
||||||
|
notif("9;7", "7");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_8_conemu_output_environment_variable_1() {
|
||||||
|
assert_eq!(
|
||||||
|
run("9;8;ab"),
|
||||||
|
Command::ConemuOutputEnvironmentVariable(b"ab".to_vec())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_8_conemu_output_environment_variable_2() {
|
||||||
|
assert_eq!(
|
||||||
|
run("9;8;"),
|
||||||
|
Command::ConemuOutputEnvironmentVariable(Vec::new())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_8_conemu_output_environment_variable_incomplete_desktop_notification() {
|
||||||
|
notif("9;8", "8");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_9_conemu_set_current_working_directory() {
|
||||||
|
assert_eq!(
|
||||||
|
run("9;9;ab"),
|
||||||
|
Command::ReportPwd {
|
||||||
|
value: b"ab".to_vec()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_9_conemu_set_current_working_directory_incomplete_desktop_notification() {
|
||||||
|
notif("9;9", "9");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_10_conemu_xterm_keyboard_and_output_emulation_1() {
|
||||||
|
xterm("9;10", Some(true), Some(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_10_conemu_xterm_keyboard_and_output_emulation_2() {
|
||||||
|
xterm("9;10;0", Some(false), Some(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_10_conemu_xterm_keyboard_and_output_emulation_3() {
|
||||||
|
xterm("9;10;1", Some(true), Some(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_10_conemu_xterm_keyboard_and_output_emulation_4() {
|
||||||
|
xterm("9;10;2", None, Some(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_10_conemu_xterm_keyboard_and_output_emulation_5() {
|
||||||
|
xterm("9;10;3", None, Some(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_10_conemu_xterm_keyboard_and_output_emulation_6() {
|
||||||
|
notif("9;10;4", "10;4");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_10_conemu_xterm_keyboard_and_output_emulation_7() {
|
||||||
|
notif("9;10;", "10;");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_10_conemu_xterm_keyboard_and_output_emulation_8() {
|
||||||
|
notif("9;10;abc", "10;abc");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_11_conemu_comment() {
|
||||||
|
assert_eq!(run("9;11;ab"), Command::ConemuComment(b"ab".to_vec()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_11_conemu_comment_incomplete_desktop_notification() {
|
||||||
|
notif("9;11", "11");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_12_conemu_mark_prompt_start_1() {
|
||||||
|
assert!(matches!(run("9;12"), Command::SemanticPrompt(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_9_12_conemu_mark_prompt_start_2() {
|
||||||
|
assert!(matches!(run("9;12;abc"), Command::SemanticPrompt(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
//! OSC 7 (ZIG/src/terminal/osc/parsers/report_pwd.zig).
|
||||||
|
use crate::osc::{Command, Parser};
|
||||||
|
|
||||||
|
/// Parse OSC 7
|
||||||
|
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||||
|
let data = parser.data()?;
|
||||||
|
Some(Command::ReportPwd { value: data })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_7_report_pwd() {
|
||||||
|
let mut p = Parser::init(false);
|
||||||
|
for c in b"7;file:///tmp/example" {
|
||||||
|
p.next(*c);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
p.end(None).unwrap(),
|
||||||
|
Command::ReportPwd {
|
||||||
|
value: b"file:///tmp/example".to_vec()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_7_report_pwd_empty() {
|
||||||
|
let mut p = Parser::init(false);
|
||||||
|
for c in b"7;" {
|
||||||
|
p.next(*c);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
p.end(None).unwrap(),
|
||||||
|
Command::ReportPwd { value: Vec::new() }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
//! OSC 777 (ZIG/src/terminal/osc/parsers/rxvt_extension.zig).
|
||||||
|
use crate::osc::{Command, Parser, State};
|
||||||
|
|
||||||
|
/// Parse OSC 777
|
||||||
|
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<Command> {
|
||||||
|
let data = parser.data()?;
|
||||||
|
let Some(k) = data.iter().position(|&b| b == b';') else {
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let ext = &data[..k];
|
||||||
|
if ext != b"notify" {
|
||||||
|
log::warn!("unknown rxvt extension: {}", String::from_utf8_lossy(ext));
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let Some(t) = data[k + 1..]
|
||||||
|
.iter()
|
||||||
|
.position(|&b| b == b';')
|
||||||
|
.map(|i| i + k + 1)
|
||||||
|
else {
|
||||||
|
log::warn!("rxvt notify extension is missing the title");
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(Command::ShowDesktopNotification {
|
||||||
|
title: data[k + 1..t].to_vec(),
|
||||||
|
body: data[t + 1..].to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_777_show_desktop_notification_with_title() {
|
||||||
|
let mut p = Parser::init(false);
|
||||||
|
for c in b"777;notify;Title;Body" {
|
||||||
|
p.next(*c);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
p.end(Some(0x1b)).unwrap(),
|
||||||
|
Command::ShowDesktopNotification {
|
||||||
|
title: b"Title".to_vec(),
|
||||||
|
body: b"Body".to_vec()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,998 @@
|
|||||||
|
//! OSC 133 semantic prompts
|
||||||
|
//! (ZIG/src/terminal/osc/parsers/semantic_prompt.zig).
|
||||||
|
//!
|
||||||
|
//! <https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md>
|
||||||
|
|
||||||
|
use crate::osc::{Command as OscCommand, Parser, State};
|
||||||
|
|
||||||
|
/// Error returned when decoding a command line fails (Zig
|
||||||
|
/// `error.DecodeError`).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct DecodeError;
|
||||||
|
|
||||||
|
impl std::fmt::Display for DecodeError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("decode error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for DecodeError {}
|
||||||
|
|
||||||
|
/// A single semantic prompt command.
|
||||||
|
///
|
||||||
|
/// Technically according to the spec, not all commands have options
|
||||||
|
/// but it is easier to be "liberal in what we accept" here since
|
||||||
|
/// all except one do and the spec does also say to ignore unknown
|
||||||
|
/// options. So, I think this is a fair interpretation.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Command {
|
||||||
|
pub action: Action,
|
||||||
|
pub options_unvalidated: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Action {
|
||||||
|
FreshLine, // 'L'
|
||||||
|
FreshLineNewPrompt, // 'A'
|
||||||
|
NewCommand, // 'N'
|
||||||
|
PromptStart, // 'P'
|
||||||
|
EndPromptStartInput, // 'B'
|
||||||
|
EndPromptStartInputTerminateEol, // 'I'
|
||||||
|
EndInputStartOutput, // 'C'
|
||||||
|
EndCommand, // 'D'
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
|
pub fn init(action: Action) -> Command {
|
||||||
|
Command {
|
||||||
|
action,
|
||||||
|
options_unvalidated: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read an option for this command. Returns None if unset or invalid.
|
||||||
|
pub fn read_option(&self, option: OptionKey) -> Option<OptionValue<'_>> {
|
||||||
|
option.read(&self.options_unvalidated)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typed convenience: `aid` option.
|
||||||
|
pub fn aid(&self) -> Option<&[u8]> {
|
||||||
|
match self.read_option(OptionKey::Aid)? {
|
||||||
|
OptionValue::Bytes(b) => Some(b),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typed convenience: `cl` option.
|
||||||
|
pub fn cl(&self) -> Option<Click> {
|
||||||
|
match self.read_option(OptionKey::Cl)? {
|
||||||
|
OptionValue::Click(c) => Some(c),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typed convenience: `k` option.
|
||||||
|
pub fn prompt_kind(&self) -> Option<PromptKind> {
|
||||||
|
match self.read_option(OptionKey::PromptKind)? {
|
||||||
|
OptionValue::PromptKind(c) => Some(c),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typed convenience: `err` option.
|
||||||
|
pub fn err(&self) -> Option<&[u8]> {
|
||||||
|
match self.read_option(OptionKey::Err)? {
|
||||||
|
OptionValue::Bytes(b) => Some(b),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typed convenience: `redraw` option.
|
||||||
|
pub fn redraw(&self) -> Option<Redraw> {
|
||||||
|
match self.read_option(OptionKey::Redraw)? {
|
||||||
|
OptionValue::Redraw(c) => Some(c),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typed convenience: `special_key` option.
|
||||||
|
pub fn special_key(&self) -> Option<bool> {
|
||||||
|
match self.read_option(OptionKey::SpecialKey)? {
|
||||||
|
OptionValue::Bool(c) => Some(c),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typed convenience: `click_events` option.
|
||||||
|
pub fn click_events(&self) -> Option<bool> {
|
||||||
|
match self.read_option(OptionKey::ClickEvents)? {
|
||||||
|
OptionValue::Bool(c) => Some(c),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typed convenience: `exit_code` (first positional value of 'D').
|
||||||
|
pub fn exit_code(&self) -> Option<i32> {
|
||||||
|
match self.read_option(OptionKey::ExitCode)? {
|
||||||
|
OptionValue::Int(c) => Some(c),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the decoded command line (if any) to the writer. If an error
|
||||||
|
/// occurs garbage may have been written to the writer.
|
||||||
|
///
|
||||||
|
/// TODO(integration): uses local copies of `printfQDecode` /
|
||||||
|
/// `urlPercentDecode` (ZIG/src/os/string_encoding.zig); switch to
|
||||||
|
/// `ghostty-os` once available (that crate would need to be a dependency
|
||||||
|
/// of ghostty-vt, so these remain local for now).
|
||||||
|
pub fn write_command_line(&self, writer: &mut Vec<u8>) -> Result<(), DecodeError> {
|
||||||
|
if let Some(OptionValue::Bytes(command_line)) = self.read_option(OptionKey::Cmdline) {
|
||||||
|
return printf_q_decode(writer, command_line);
|
||||||
|
}
|
||||||
|
if let Some(OptionValue::Bytes(command_line)) = self.read_option(OptionKey::CmdlineUrl) {
|
||||||
|
return url_percent_decode(writer, command_line);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum OptionKey {
|
||||||
|
Aid,
|
||||||
|
Cl,
|
||||||
|
PromptKind,
|
||||||
|
Err,
|
||||||
|
Cmdline,
|
||||||
|
CmdlineUrl,
|
||||||
|
|
||||||
|
// https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
|
||||||
|
// Kitty supports a "redraw" option for prompt_start. This is extended
|
||||||
|
// by Ghostty with the "last" option. See Redraw the type for more details.
|
||||||
|
Redraw,
|
||||||
|
|
||||||
|
// Use a special key instead of arrow keys to move the cursor on
|
||||||
|
// mouse click. Useful if arrow keys have side-effets like triggering
|
||||||
|
// auto-complete. The shell integration script should bind the special
|
||||||
|
// key as needed.
|
||||||
|
// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
|
||||||
|
SpecialKey,
|
||||||
|
|
||||||
|
// If true, the shell is capable of handling mouse click events.
|
||||||
|
// Ghostty will then send a click event to the shell when the user
|
||||||
|
// clicks somewhere in the prompt. The shell can then move the cursor
|
||||||
|
// to that position or perform some other appropriate action. If false,
|
||||||
|
// Ghostty may generate a number of fake key events to move the cursor
|
||||||
|
// which is not very robust.
|
||||||
|
// See: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
|
||||||
|
ClickEvents,
|
||||||
|
|
||||||
|
// Not technically an option that can be set with k=v and only
|
||||||
|
// present currently with command 'D' but its easier to just
|
||||||
|
// parse it into our options.
|
||||||
|
ExitCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The value of an option; the variant is determined by the `Option` (Zig
|
||||||
|
/// `Option.Type()`).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum OptionValue<'a> {
|
||||||
|
Bytes(&'a [u8]),
|
||||||
|
Click(Click),
|
||||||
|
PromptKind(PromptKind),
|
||||||
|
Redraw(Redraw),
|
||||||
|
Bool(bool),
|
||||||
|
Int(i32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OptionKey {
|
||||||
|
fn key(self) -> &'static [u8] {
|
||||||
|
match self {
|
||||||
|
OptionKey::Aid => b"aid",
|
||||||
|
OptionKey::Cl => b"cl",
|
||||||
|
OptionKey::PromptKind => b"k",
|
||||||
|
OptionKey::Err => b"err",
|
||||||
|
OptionKey::Redraw => b"redraw",
|
||||||
|
OptionKey::SpecialKey => b"special_key",
|
||||||
|
OptionKey::ClickEvents => b"click_events",
|
||||||
|
OptionKey::Cmdline => b"cmdline",
|
||||||
|
OptionKey::CmdlineUrl => b"cmdline_url",
|
||||||
|
|
||||||
|
// special case, handled before ever calling key
|
||||||
|
OptionKey::ExitCode => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the option value from the raw options string.
|
||||||
|
///
|
||||||
|
/// The raw options string is the raw unparsed data after the
|
||||||
|
/// OSC 133 command. e.g. for `133;A;aid=14;cl=line`, the
|
||||||
|
/// raw options string would be `aid=14;cl=line`.
|
||||||
|
///
|
||||||
|
/// Any errors in the raw string will return None since the OSC133
|
||||||
|
/// specification says to ignore unknown or malformed options.
|
||||||
|
pub fn read(self, raw: &[u8]) -> Option<OptionValue<'_>> {
|
||||||
|
let mut remaining = raw;
|
||||||
|
'outer: while !remaining.is_empty() {
|
||||||
|
// Length of the next value is up to the `;` or the
|
||||||
|
// end of the string.
|
||||||
|
let len = remaining
|
||||||
|
.iter()
|
||||||
|
.position(|&b| b == b';')
|
||||||
|
.unwrap_or(remaining.len());
|
||||||
|
|
||||||
|
// Grab our full value and move our cursor past the `;`
|
||||||
|
let full = &remaining[..len];
|
||||||
|
|
||||||
|
// If we're looking for exit_code we special case it.
|
||||||
|
// as the first value.
|
||||||
|
if self == OptionKey::ExitCode {
|
||||||
|
return std::str::from_utf8(full)
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.parse::<i32>().ok())
|
||||||
|
.map(OptionValue::Int);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse our key=value and verify our key matches our
|
||||||
|
// expectation.
|
||||||
|
let value = 'value: {
|
||||||
|
if let Some(eql_idx) = full
|
||||||
|
.iter()
|
||||||
|
.position(|&b| b == b'=')
|
||||||
|
.filter(|&i| full[..i] == *self.key())
|
||||||
|
{
|
||||||
|
break 'value &full[eql_idx + 1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
// No match!
|
||||||
|
if len < remaining.len() {
|
||||||
|
remaining = &remaining[len + 1..];
|
||||||
|
continue 'outer;
|
||||||
|
}
|
||||||
|
|
||||||
|
break 'outer;
|
||||||
|
};
|
||||||
|
|
||||||
|
return match self {
|
||||||
|
OptionKey::Aid | OptionKey::Err | OptionKey::Cmdline | OptionKey::CmdlineUrl => {
|
||||||
|
Some(OptionValue::Bytes(value))
|
||||||
|
}
|
||||||
|
OptionKey::Cl => Click::init(value).map(OptionValue::Click),
|
||||||
|
OptionKey::PromptKind => {
|
||||||
|
if value.len() == 1 {
|
||||||
|
PromptKind::init(value[0]).map(OptionValue::PromptKind)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OptionKey::Redraw => match value {
|
||||||
|
b"0" => Some(OptionValue::Redraw(Redraw::False)),
|
||||||
|
b"1" => Some(OptionValue::Redraw(Redraw::True)),
|
||||||
|
b"last" => Some(OptionValue::Redraw(Redraw::Last)),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
OptionKey::SpecialKey | OptionKey::ClickEvents => match value {
|
||||||
|
b"0" => Some(OptionValue::Bool(false)),
|
||||||
|
b"1" => Some(OptionValue::Bool(true)),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
// Handled above
|
||||||
|
OptionKey::ExitCode => unreachable!(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not found
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `cl` option specifies what kind of cursor key sequences are handled
|
||||||
|
/// by the application for click-to-move-cursor functionality.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Click {
|
||||||
|
/// Value: "line". Allows motion within a single input line using standard
|
||||||
|
/// left/right arrow escape sequences. Only a single left/right sequence
|
||||||
|
/// should be emitted for double-width characters.
|
||||||
|
Line,
|
||||||
|
|
||||||
|
/// Value: "m". Allows movement between different lines in the same group,
|
||||||
|
/// but only using left/right arrow escape sequences.
|
||||||
|
Multiple,
|
||||||
|
|
||||||
|
/// Value: "v". Like `multiple` but cursor up/down should be used. The
|
||||||
|
/// terminal should be conservative when moving between lines: move the
|
||||||
|
/// cursor left to the start of line, emit the needed up/down sequences,
|
||||||
|
/// then move the cursor right to the clicked destination.
|
||||||
|
ConservativeVertical,
|
||||||
|
|
||||||
|
/// Value: "w". Like `conservative_vertical` but specifies that there are
|
||||||
|
/// no spurious spaces at the end of the line, and the application editor
|
||||||
|
/// handles "smart vertical movement" (moving 2 lines up from position 20,
|
||||||
|
/// where the intermediate line is 15 chars wide and the destination is
|
||||||
|
/// 18 chars wide, ends at position 18).
|
||||||
|
SmartVertical,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Click {
|
||||||
|
pub fn init(value: &[u8]) -> Option<Click> {
|
||||||
|
if value.len() == 1 {
|
||||||
|
match value[0] {
|
||||||
|
b'm' => Some(Click::Multiple),
|
||||||
|
b'v' => Some(Click::ConservativeVertical),
|
||||||
|
b'w' => Some(Click::SmartVertical),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
} else if value == b"line" {
|
||||||
|
Some(Click::Line)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum PromptKind {
|
||||||
|
Initial,
|
||||||
|
Right,
|
||||||
|
Continuation,
|
||||||
|
Secondary,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PromptKind {
|
||||||
|
pub fn init(c: u8) -> Option<PromptKind> {
|
||||||
|
match c {
|
||||||
|
b'i' => Some(PromptKind::Initial),
|
||||||
|
b'r' => Some(PromptKind::Right),
|
||||||
|
b'c' => Some(PromptKind::Continuation),
|
||||||
|
b's' => Some(PromptKind::Secondary),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The values for the `redraw` extension to OSC133. This was
|
||||||
|
/// started by Kitty[1] and extended by Ghostty (the "last" option).
|
||||||
|
///
|
||||||
|
/// [1]: https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Redraw {
|
||||||
|
/// The shell supports redrawing the full prompt and all continuations.
|
||||||
|
/// This is the default value, it does not need to be explicitly set
|
||||||
|
/// unless it is to reset a prior other value.
|
||||||
|
True,
|
||||||
|
|
||||||
|
/// The shell does NOT support redrawing. In this case, Ghostty will NOT
|
||||||
|
/// clear any prompt lines on resize.
|
||||||
|
False,
|
||||||
|
|
||||||
|
/// The shell supports redrawing only the LAST line of the prompt.
|
||||||
|
/// Ghostty will only clear the last line of the prompt on resize.
|
||||||
|
///
|
||||||
|
/// This is specifically introduced because Bash only redraws the last
|
||||||
|
/// line. It is literally the only shell that does this and it does this
|
||||||
|
/// because its bad and they should feel bad. Don't be like Bash.
|
||||||
|
Last,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse OSC 133, semantic prompts
|
||||||
|
pub fn parse(parser: &mut Parser, _terminator_ch: Option<u8>) -> Option<OscCommand> {
|
||||||
|
let data = parser.buffered()?;
|
||||||
|
if data.is_empty() {
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All valid cases terminate within this block. Any fallthroughs
|
||||||
|
// are invalid.
|
||||||
|
let action = match data[0] {
|
||||||
|
b'A' => Action::FreshLineNewPrompt,
|
||||||
|
b'B' => Action::EndPromptStartInput,
|
||||||
|
b'I' => Action::EndPromptStartInputTerminateEol,
|
||||||
|
b'C' => Action::EndInputStartOutput,
|
||||||
|
b'D' => Action::EndCommand,
|
||||||
|
b'N' => Action::NewCommand,
|
||||||
|
b'P' => Action::PromptStart,
|
||||||
|
b'L' => {
|
||||||
|
if data.len() > 1 {
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
return Some(OscCommand::SemanticPrompt(Command::init(Action::FreshLine)));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cmd = Command::init(action);
|
||||||
|
if data.len() > 1 {
|
||||||
|
if data[1] != b';' {
|
||||||
|
parser.state = State::Invalid;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
cmd.options_unvalidated = data[2..].to_vec();
|
||||||
|
}
|
||||||
|
Some(OscCommand::SemanticPrompt(cmd))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(integration): local copies of ZIG/src/os/string_encoding.zig.
|
||||||
|
|
||||||
|
/// Decode data that has been encoded in the same way that `bash`'s
|
||||||
|
/// `printf %q` encodes a string and write it to the writer. If an error is
|
||||||
|
/// returned garbage may have been written to the buffer.
|
||||||
|
pub fn printf_q_decode(writer: &mut Vec<u8>, buf: &[u8]) -> Result<(), DecodeError> {
|
||||||
|
let data: &[u8] = if buf.starts_with(b"$'") {
|
||||||
|
// Strip off `$''` quoting.
|
||||||
|
if buf.len() < 3 || !buf.ends_with(b"'") {
|
||||||
|
return Err(DecodeError);
|
||||||
|
}
|
||||||
|
&buf[2..buf.len() - 1]
|
||||||
|
} else if buf.starts_with(b"'") {
|
||||||
|
// Strip off `''` quoting.
|
||||||
|
if buf.len() < 2 || !buf.ends_with(b"'") {
|
||||||
|
return Err(DecodeError);
|
||||||
|
}
|
||||||
|
&buf[1..buf.len() - 1]
|
||||||
|
} else {
|
||||||
|
buf
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut src = 0;
|
||||||
|
while src < data.len() {
|
||||||
|
match data[src] {
|
||||||
|
b'\\' => {
|
||||||
|
if src + 1 >= data.len() {
|
||||||
|
return Err(DecodeError);
|
||||||
|
}
|
||||||
|
match data[src + 1] {
|
||||||
|
c @ (b' ' | b'\\' | b'"' | b'\'' | b'$') => writer.push(c),
|
||||||
|
b'e' => writer.push(0x1b),
|
||||||
|
b'n' => writer.push(b'\n'),
|
||||||
|
b'r' => writer.push(b'\r'),
|
||||||
|
b't' => writer.push(b'\t'),
|
||||||
|
b'v' => writer.push(0x0b),
|
||||||
|
_ => return Err(DecodeError),
|
||||||
|
}
|
||||||
|
src += 2;
|
||||||
|
}
|
||||||
|
c => {
|
||||||
|
writer.push(c);
|
||||||
|
src += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode data that has been URL percent encoded and write it to the given
|
||||||
|
/// buffer. If an error is returned garbage may have been written.
|
||||||
|
pub fn url_percent_decode(writer: &mut Vec<u8>, buf: &[u8]) -> Result<(), DecodeError> {
|
||||||
|
fn hex(c: u8) -> Option<u8> {
|
||||||
|
match c {
|
||||||
|
b'0'..=b'9' => Some(c - b'0'),
|
||||||
|
b'a'..=b'f' => Some(c - b'a' + 10),
|
||||||
|
b'A'..=b'F' => Some(c - b'A' + 10),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut src = 0;
|
||||||
|
while src < buf.len() {
|
||||||
|
match buf[src] {
|
||||||
|
b'%' => {
|
||||||
|
if src + 2 >= buf.len() {
|
||||||
|
return Err(DecodeError);
|
||||||
|
}
|
||||||
|
match (hex(buf[src + 1]), hex(buf[src + 2])) {
|
||||||
|
(Some(a), Some(b)) => {
|
||||||
|
writer.push((a << 4) | b);
|
||||||
|
src += 3;
|
||||||
|
}
|
||||||
|
_ => return Err(DecodeError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c => {
|
||||||
|
writer.push(c);
|
||||||
|
src += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used, clippy::panic)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn run(input: &str) -> Option<Command> {
|
||||||
|
let mut p = Parser::init(false);
|
||||||
|
for ch in input.bytes() {
|
||||||
|
p.next(ch);
|
||||||
|
}
|
||||||
|
match p.end(None) {
|
||||||
|
Some(OscCommand::SemanticPrompt(c)) => Some(c),
|
||||||
|
Some(c) => panic!("unexpected {c:?}"),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmdline(input: &str) -> (Command, Result<Vec<u8>, DecodeError>) {
|
||||||
|
let cmd = run(input).unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::EndInputStartOutput);
|
||||||
|
let mut w = Vec::new();
|
||||||
|
let r = cmd.write_command_line(&mut w).map(|()| w);
|
||||||
|
(cmd, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmdline_ok(input: &str, expected: &str) {
|
||||||
|
assert_eq!(cmdline(input).1.unwrap(), expected.as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmdline_err(input: &str) {
|
||||||
|
assert_eq!(cmdline(input).1, Err(DecodeError));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_input_start_output() {
|
||||||
|
let cmd = run("133;C").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::EndInputStartOutput);
|
||||||
|
assert!(cmd.aid().is_none());
|
||||||
|
assert!(cmd.cl().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_input_start_output_extra_contents() {
|
||||||
|
assert!(run("133;Cextra").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_input_start_output_with_options() {
|
||||||
|
let cmd = run("133;C;aid=foo").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::EndInputStartOutput);
|
||||||
|
assert_eq!(cmd.aid().unwrap(), b"foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_input_start_output_with_cmdline() {
|
||||||
|
cmdline_ok("133;C;cmdline=echo bobr kurwa", "echo bobr kurwa");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_input_start_output_with_cmdline_3() {
|
||||||
|
cmdline_ok("133;C;cmdline=echo bobr\\nkurwa", "echo bobr\nkurwa");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_input_start_output_with_cmdline_4() {
|
||||||
|
cmdline_ok("133;C;cmdline=$'echo bobr kurwa'", "echo bobr kurwa");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_input_start_output_with_cmdline_5() {
|
||||||
|
cmdline_ok("133;C;cmdline='echo bobr kurwa'", "echo bobr kurwa");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_input_start_output_with_cmdline_6() {
|
||||||
|
cmdline_err("133;C;cmdline='echo bobr kurwa");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_input_start_output_with_cmdline_7() {
|
||||||
|
cmdline_err("133;C;cmdline=$'echo bobr kurwa");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_input_start_output_with_cmdline_8() {
|
||||||
|
cmdline_err("133;C;cmdline=$'");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_input_start_output_with_cmdline_9() {
|
||||||
|
cmdline_ok("133;C;cmdline=", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_input_start_output_with_cmdline_url_1() {
|
||||||
|
cmdline_ok("133;C;cmdline_url=echo bobr kurwa", "echo bobr kurwa");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_input_start_output_with_cmdline_url_2() {
|
||||||
|
cmdline_ok("133;C;cmdline_url=echo bobr%20kurwa", "echo bobr kurwa");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_input_start_output_with_cmdline_url_3() {
|
||||||
|
cmdline_ok("133;C;cmdline_url=echo bobr%3bkurwa", "echo bobr;kurwa");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_input_start_output_with_cmdline_url_4() {
|
||||||
|
cmdline_err("133;C;cmdline_url=echo bobr%3kurwa");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_input_start_output_with_cmdline_url_5() {
|
||||||
|
cmdline_err("133;C;cmdline_url=echo bobr%kurwa");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_input_start_output_with_cmdline_url_6() {
|
||||||
|
cmdline_ok("133;C;cmdline_url=echo bobr kurwa%20", "echo bobr kurwa ");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_input_start_output_with_cmdline_url_7() {
|
||||||
|
cmdline_err("133;C;cmdline_url=echo bobr kurwa%2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_input_start_output_with_cmdline_url_8() {
|
||||||
|
cmdline_err("133;C;cmdline_url=echo bobr kurwa%");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_fresh_line() {
|
||||||
|
assert_eq!(run("133;L").unwrap().action, Action::FreshLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_fresh_line_extra_contents() {
|
||||||
|
// Random
|
||||||
|
assert!(run("133;Lol").is_none());
|
||||||
|
// Options
|
||||||
|
assert!(run("133;L;aid=foo").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_fresh_line_new_prompt() {
|
||||||
|
let cmd = run("133;A").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::FreshLineNewPrompt);
|
||||||
|
assert!(cmd.aid().is_none());
|
||||||
|
assert!(cmd.cl().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_fresh_line_new_prompt_with_aid() {
|
||||||
|
let cmd = run("133;A;aid=14").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::FreshLineNewPrompt);
|
||||||
|
assert_eq!(cmd.aid().unwrap(), b"14");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_fresh_line_new_prompt_with_equals_in_aid() {
|
||||||
|
let cmd = run("133;A;aid=a=b").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::FreshLineNewPrompt);
|
||||||
|
assert_eq!(cmd.aid().unwrap(), b"a=b");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_fresh_line_new_prompt_with_cl_line() {
|
||||||
|
let cmd = run("133;A;cl=line").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::FreshLineNewPrompt);
|
||||||
|
assert_eq!(cmd.cl(), Some(Click::Line));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_fresh_line_new_prompt_with_cl_m() {
|
||||||
|
let cmd = run("133;A;cl=m").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::FreshLineNewPrompt);
|
||||||
|
assert_eq!(cmd.cl(), Some(Click::Multiple));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_fresh_line_new_prompt_with_invalid_cl() {
|
||||||
|
let cmd = run("133;A;cl=invalid").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::FreshLineNewPrompt);
|
||||||
|
assert!(cmd.cl().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_fresh_line_new_prompt_with_trailing_semicolon() {
|
||||||
|
let cmd = run("133;A;").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::FreshLineNewPrompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_fresh_line_new_prompt_with_bare_key() {
|
||||||
|
let cmd = run("133;A;barekey").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::FreshLineNewPrompt);
|
||||||
|
assert!(cmd.aid().is_none());
|
||||||
|
assert!(cmd.cl().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_fresh_line_new_prompt_with_multiple_options() {
|
||||||
|
let cmd = run("133;A;aid=foo;cl=line").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::FreshLineNewPrompt);
|
||||||
|
assert_eq!(cmd.aid().unwrap(), b"foo");
|
||||||
|
assert_eq!(cmd.cl(), Some(Click::Line));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_fresh_line_new_prompt_default_redraw() {
|
||||||
|
let cmd = run("133;A").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::FreshLineNewPrompt);
|
||||||
|
assert!(cmd.redraw().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_fresh_line_new_prompt_with_redraw_0() {
|
||||||
|
let cmd = run("133;A;redraw=0").unwrap();
|
||||||
|
assert_eq!(cmd.redraw(), Some(Redraw::False));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_fresh_line_new_prompt_with_redraw_1() {
|
||||||
|
let cmd = run("133;A;redraw=1").unwrap();
|
||||||
|
assert_eq!(cmd.redraw(), Some(Redraw::True));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_fresh_line_new_prompt_with_invalid_redraw() {
|
||||||
|
let cmd = run("133;A;redraw=x").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::FreshLineNewPrompt);
|
||||||
|
assert!(cmd.redraw().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_prompt_start() {
|
||||||
|
let cmd = run("133;P").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::PromptStart);
|
||||||
|
assert!(cmd.prompt_kind().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_prompt_start_with_k_i() {
|
||||||
|
let cmd = run("133;P;k=i").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::PromptStart);
|
||||||
|
assert_eq!(cmd.prompt_kind(), Some(PromptKind::Initial));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_prompt_start_with_k_r() {
|
||||||
|
let cmd = run("133;P;k=r").unwrap();
|
||||||
|
assert_eq!(cmd.prompt_kind(), Some(PromptKind::Right));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_prompt_start_with_k_c() {
|
||||||
|
let cmd = run("133;P;k=c").unwrap();
|
||||||
|
assert_eq!(cmd.prompt_kind(), Some(PromptKind::Continuation));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_prompt_start_with_k_s() {
|
||||||
|
let cmd = run("133;P;k=s").unwrap();
|
||||||
|
assert_eq!(cmd.prompt_kind(), Some(PromptKind::Secondary));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_prompt_start_with_invalid_k() {
|
||||||
|
let cmd = run("133;P;k=x").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::PromptStart);
|
||||||
|
assert!(cmd.prompt_kind().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_prompt_start_extra_contents() {
|
||||||
|
assert!(run("133;Pextra").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_new_command() {
|
||||||
|
let cmd = run("133;N").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::NewCommand);
|
||||||
|
assert!(cmd.aid().is_none());
|
||||||
|
assert!(cmd.cl().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_new_command_with_aid() {
|
||||||
|
let cmd = run("133;N;aid=foo").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::NewCommand);
|
||||||
|
assert_eq!(cmd.aid().unwrap(), b"foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_new_command_with_cl_line() {
|
||||||
|
let cmd = run("133;N;cl=line").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::NewCommand);
|
||||||
|
assert_eq!(cmd.cl(), Some(Click::Line));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_new_command_with_multiple_options() {
|
||||||
|
let cmd = run("133;N;aid=foo;cl=line").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::NewCommand);
|
||||||
|
assert_eq!(cmd.aid().unwrap(), b"foo");
|
||||||
|
assert_eq!(cmd.cl(), Some(Click::Line));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_new_command_extra_contents() {
|
||||||
|
assert!(run("133;Nextra").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_prompt_start_input() {
|
||||||
|
let cmd = run("133;B").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::EndPromptStartInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_prompt_start_input_extra_contents() {
|
||||||
|
assert!(run("133;Bextra").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_prompt_start_input_with_options() {
|
||||||
|
let cmd = run("133;B;aid=foo").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::EndPromptStartInput);
|
||||||
|
assert_eq!(cmd.aid().unwrap(), b"foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_prompt_start_input_terminate_eol() {
|
||||||
|
let cmd = run("133;I").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::EndPromptStartInputTerminateEol);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_prompt_start_input_terminate_eol_extra_contents() {
|
||||||
|
assert!(run("133;Iextra").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_prompt_start_input_terminate_eol_with_options() {
|
||||||
|
let cmd = run("133;I;aid=foo").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::EndPromptStartInputTerminateEol);
|
||||||
|
assert_eq!(cmd.aid().unwrap(), b"foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_command() {
|
||||||
|
let cmd = run("133;D").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::EndCommand);
|
||||||
|
assert!(cmd.exit_code().is_none());
|
||||||
|
assert!(cmd.aid().is_none());
|
||||||
|
assert!(cmd.err().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_command_extra_contents() {
|
||||||
|
assert!(run("133;Dextra").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_command_with_exit_code_0() {
|
||||||
|
let cmd = run("133;D;0").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::EndCommand);
|
||||||
|
assert_eq!(cmd.exit_code(), Some(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_133_end_command_with_exit_code_and_aid() {
|
||||||
|
let cmd = run("133;D;12;aid=foo").unwrap();
|
||||||
|
assert_eq!(cmd.action, Action::EndCommand);
|
||||||
|
assert_eq!(cmd.aid().unwrap(), b"foo");
|
||||||
|
assert_eq!(cmd.exit_code(), Some(12));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bytes(v: Option<OptionValue<'_>>) -> Option<&[u8]> {
|
||||||
|
match v {
|
||||||
|
Some(OptionValue::Bytes(b)) => Some(b),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn option_read_aid() {
|
||||||
|
let r = |s: &'static str| bytes(OptionKey::Aid.read(s.as_bytes()));
|
||||||
|
assert_eq!(r("aid=test123").unwrap(), b"test123");
|
||||||
|
assert_eq!(r("cl=line;aid=myaid;k=i").unwrap(), b"myaid");
|
||||||
|
assert!(r("cl=line;k=i").is_none());
|
||||||
|
assert_eq!(r("aid=").unwrap(), b"");
|
||||||
|
assert_eq!(r("k=i;aid=last").unwrap(), b"last");
|
||||||
|
assert_eq!(r("aid=first;k=i").unwrap(), b"first");
|
||||||
|
assert!(r("").is_none());
|
||||||
|
assert!(r("aid").is_none());
|
||||||
|
assert_eq!(r(";;aid=value;;").unwrap(), b"value");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn option_read_cl() {
|
||||||
|
let r = |s: &'static str| OptionKey::Cl.read(s.as_bytes());
|
||||||
|
assert_eq!(r("cl=line"), Some(OptionValue::Click(Click::Line)));
|
||||||
|
assert_eq!(r("cl=m"), Some(OptionValue::Click(Click::Multiple)));
|
||||||
|
assert_eq!(
|
||||||
|
r("cl=v"),
|
||||||
|
Some(OptionValue::Click(Click::ConservativeVertical))
|
||||||
|
);
|
||||||
|
assert_eq!(r("cl=w"), Some(OptionValue::Click(Click::SmartVertical)));
|
||||||
|
assert!(r("cl=invalid").is_none());
|
||||||
|
assert!(r("aid=foo").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn option_read_prompt_kind() {
|
||||||
|
let r = |s: &'static str| OptionKey::PromptKind.read(s.as_bytes());
|
||||||
|
assert_eq!(r("k=i"), Some(OptionValue::PromptKind(PromptKind::Initial)));
|
||||||
|
assert_eq!(r("k=r"), Some(OptionValue::PromptKind(PromptKind::Right)));
|
||||||
|
assert_eq!(
|
||||||
|
r("k=c"),
|
||||||
|
Some(OptionValue::PromptKind(PromptKind::Continuation))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
r("k=s"),
|
||||||
|
Some(OptionValue::PromptKind(PromptKind::Secondary))
|
||||||
|
);
|
||||||
|
assert!(r("k=x").is_none());
|
||||||
|
assert!(r("k=ii").is_none());
|
||||||
|
assert!(r("k=").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn option_read_err() {
|
||||||
|
assert_eq!(
|
||||||
|
bytes(OptionKey::Err.read(b"err=some_error")).unwrap(),
|
||||||
|
b"some_error"
|
||||||
|
);
|
||||||
|
assert!(OptionKey::Err.read(b"aid=foo").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn option_read_redraw() {
|
||||||
|
let r = |s: &'static str| OptionKey::Redraw.read(s.as_bytes());
|
||||||
|
assert_eq!(r("redraw=1"), Some(OptionValue::Redraw(Redraw::True)));
|
||||||
|
assert_eq!(r("redraw=0"), Some(OptionValue::Redraw(Redraw::False)));
|
||||||
|
assert_eq!(r("redraw=last"), Some(OptionValue::Redraw(Redraw::Last)));
|
||||||
|
assert!(r("redraw=2").is_none());
|
||||||
|
assert!(r("redraw=10").is_none());
|
||||||
|
assert!(r("redraw=").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn option_read_special_key() {
|
||||||
|
let r = |s: &'static str| OptionKey::SpecialKey.read(s.as_bytes());
|
||||||
|
assert_eq!(r("special_key=1"), Some(OptionValue::Bool(true)));
|
||||||
|
assert_eq!(r("special_key=0"), Some(OptionValue::Bool(false)));
|
||||||
|
assert!(r("special_key=x").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn option_read_click_events() {
|
||||||
|
let r = |s: &'static str| OptionKey::ClickEvents.read(s.as_bytes());
|
||||||
|
assert_eq!(r("click_events=1"), Some(OptionValue::Bool(true)));
|
||||||
|
assert_eq!(r("click_events=0"), Some(OptionValue::Bool(false)));
|
||||||
|
assert!(r("click_events=yes").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn option_read_exit_code() {
|
||||||
|
let r = |s: &'static str| OptionKey::ExitCode.read(s.as_bytes());
|
||||||
|
assert_eq!(r("42"), Some(OptionValue::Int(42)));
|
||||||
|
assert_eq!(r("0"), Some(OptionValue::Int(0)));
|
||||||
|
assert_eq!(r("-1"), Some(OptionValue::Int(-1)));
|
||||||
|
assert!(r("abc").is_none());
|
||||||
|
assert_eq!(r("127;aid=foo"), Some(OptionValue::Int(127)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_encoding_extra_printf_q_and_percent() {
|
||||||
|
let mut w = Vec::new();
|
||||||
|
printf_q_decode(&mut w, b"bobr\\ kurwa").unwrap();
|
||||||
|
assert_eq!(w, b"bobr kurwa");
|
||||||
|
assert_eq!(
|
||||||
|
printf_q_decode(&mut Vec::new(), b"bobr\\dkurwa"),
|
||||||
|
Err(DecodeError)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
printf_q_decode(&mut Vec::new(), b"bobr kurwa\\"),
|
||||||
|
Err(DecodeError)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,527 @@
|
|||||||
|
//! The primary export of this file is `TABLE`, which contains a
|
||||||
|
//! compile-time-generated state transition table for VT emulation
|
||||||
|
//! (ZIG/src/terminal/parse_table.zig).
|
||||||
|
//!
|
||||||
|
//! This is based on the vt100.net state machine:
|
||||||
|
//! https://vt100.net/emu/dec_ansi_parser
|
||||||
|
//! But has some modifications:
|
||||||
|
//!
|
||||||
|
//! * csi_param accepts the colon character (':') since the SGR command
|
||||||
|
//! accepts colon as a valid parameter value.
|
||||||
|
|
||||||
|
use crate::parser::{State, TransitionAction as A};
|
||||||
|
|
||||||
|
const NUM_STATES: usize = 14;
|
||||||
|
|
||||||
|
const STATES: [State; NUM_STATES] = [
|
||||||
|
State::Ground,
|
||||||
|
State::Escape,
|
||||||
|
State::EscapeIntermediate,
|
||||||
|
State::CsiEntry,
|
||||||
|
State::CsiIntermediate,
|
||||||
|
State::CsiParam,
|
||||||
|
State::CsiIgnore,
|
||||||
|
State::DcsEntry,
|
||||||
|
State::DcsParam,
|
||||||
|
State::DcsIntermediate,
|
||||||
|
State::DcsPassthrough,
|
||||||
|
State::DcsIgnore,
|
||||||
|
State::OscString,
|
||||||
|
State::SosPmApcString,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Transition is the transition to take within the table
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Transition {
|
||||||
|
pub state: State,
|
||||||
|
pub action: A,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type of the state transition table: `[byte][state]`.
|
||||||
|
pub type Table = [[Transition; NUM_STATES]; 256];
|
||||||
|
|
||||||
|
type OptionalTable = [[Option<Transition>; NUM_STATES]; 256];
|
||||||
|
|
||||||
|
/// The state transition table, indexed `TABLE[byte][state as usize]`.
|
||||||
|
pub const TABLE: Table = gen_table();
|
||||||
|
|
||||||
|
/// Function to generate the full state transition table for VT emulation.
|
||||||
|
const fn gen_table() -> Table {
|
||||||
|
// We accumulate using an "optional" table so we can detect unset entries.
|
||||||
|
let mut result: OptionalTable = [[None; NUM_STATES]; 256];
|
||||||
|
|
||||||
|
// anywhere transitions
|
||||||
|
let mut s = 0;
|
||||||
|
while s < NUM_STATES {
|
||||||
|
let source = STATES[s];
|
||||||
|
|
||||||
|
// anywhere => ground
|
||||||
|
single(&mut result, 0x18, source, State::Ground, A::Execute);
|
||||||
|
single(&mut result, 0x1A, source, State::Ground, A::Execute);
|
||||||
|
range(&mut result, 0x80, 0x8F, source, State::Ground, A::Execute);
|
||||||
|
range(&mut result, 0x91, 0x97, source, State::Ground, A::Execute);
|
||||||
|
single(&mut result, 0x99, source, State::Ground, A::Execute);
|
||||||
|
single(&mut result, 0x9A, source, State::Ground, A::Execute);
|
||||||
|
single(&mut result, 0x9C, source, State::Ground, A::None);
|
||||||
|
|
||||||
|
// anywhere => escape
|
||||||
|
single(&mut result, 0x1B, source, State::Escape, A::None);
|
||||||
|
|
||||||
|
// anywhere => sos_pm_apc_string
|
||||||
|
single(&mut result, 0x98, source, State::SosPmApcString, A::None);
|
||||||
|
single(&mut result, 0x9E, source, State::SosPmApcString, A::None);
|
||||||
|
single(&mut result, 0x9F, source, State::SosPmApcString, A::None);
|
||||||
|
|
||||||
|
// anywhere => csi_entry
|
||||||
|
single(&mut result, 0x9B, source, State::CsiEntry, A::None);
|
||||||
|
|
||||||
|
// anywhere => dcs_entry
|
||||||
|
single(&mut result, 0x90, source, State::DcsEntry, A::None);
|
||||||
|
|
||||||
|
// anywhere => osc_string
|
||||||
|
single(&mut result, 0x9D, source, State::OscString, A::None);
|
||||||
|
s += 1;
|
||||||
|
}
|
||||||
|
// ground
|
||||||
|
{
|
||||||
|
// events
|
||||||
|
single(&mut result, 0x19, State::Ground, State::Ground, A::Execute);
|
||||||
|
range(
|
||||||
|
&mut result,
|
||||||
|
0,
|
||||||
|
0x17,
|
||||||
|
State::Ground,
|
||||||
|
State::Ground,
|
||||||
|
A::Execute,
|
||||||
|
);
|
||||||
|
range(
|
||||||
|
&mut result,
|
||||||
|
0x1C,
|
||||||
|
0x1F,
|
||||||
|
State::Ground,
|
||||||
|
State::Ground,
|
||||||
|
A::Execute,
|
||||||
|
);
|
||||||
|
range(
|
||||||
|
&mut result,
|
||||||
|
0x20,
|
||||||
|
0x7F,
|
||||||
|
State::Ground,
|
||||||
|
State::Ground,
|
||||||
|
A::Print,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// escape_intermediate
|
||||||
|
{
|
||||||
|
let source = State::EscapeIntermediate;
|
||||||
|
|
||||||
|
single(&mut result, 0x19, source, source, A::Execute);
|
||||||
|
range(&mut result, 0, 0x17, source, source, A::Execute);
|
||||||
|
range(&mut result, 0x1C, 0x1F, source, source, A::Execute);
|
||||||
|
range(&mut result, 0x20, 0x2F, source, source, A::Collect);
|
||||||
|
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||||
|
|
||||||
|
// => ground
|
||||||
|
range(
|
||||||
|
&mut result,
|
||||||
|
0x30,
|
||||||
|
0x7E,
|
||||||
|
source,
|
||||||
|
State::Ground,
|
||||||
|
A::EscDispatch,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// sos_pm_apc_string
|
||||||
|
{
|
||||||
|
let source = State::SosPmApcString;
|
||||||
|
|
||||||
|
// events
|
||||||
|
single(&mut result, 0x19, source, source, A::ApcPut);
|
||||||
|
range(&mut result, 0, 0x17, source, source, A::ApcPut);
|
||||||
|
range(&mut result, 0x1C, 0x1F, source, source, A::ApcPut);
|
||||||
|
range(&mut result, 0x20, 0x7F, source, source, A::ApcPut);
|
||||||
|
}
|
||||||
|
|
||||||
|
// escape
|
||||||
|
{
|
||||||
|
let source = State::Escape;
|
||||||
|
|
||||||
|
// events
|
||||||
|
single(&mut result, 0x19, source, source, A::Execute);
|
||||||
|
range(&mut result, 0, 0x17, source, source, A::Execute);
|
||||||
|
range(&mut result, 0x1C, 0x1F, source, source, A::Execute);
|
||||||
|
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||||
|
|
||||||
|
// => ground
|
||||||
|
range(
|
||||||
|
&mut result,
|
||||||
|
0x30,
|
||||||
|
0x4F,
|
||||||
|
source,
|
||||||
|
State::Ground,
|
||||||
|
A::EscDispatch,
|
||||||
|
);
|
||||||
|
range(
|
||||||
|
&mut result,
|
||||||
|
0x51,
|
||||||
|
0x57,
|
||||||
|
source,
|
||||||
|
State::Ground,
|
||||||
|
A::EscDispatch,
|
||||||
|
);
|
||||||
|
range(
|
||||||
|
&mut result,
|
||||||
|
0x60,
|
||||||
|
0x7E,
|
||||||
|
source,
|
||||||
|
State::Ground,
|
||||||
|
A::EscDispatch,
|
||||||
|
);
|
||||||
|
single(&mut result, 0x59, source, State::Ground, A::EscDispatch);
|
||||||
|
single(&mut result, 0x5A, source, State::Ground, A::EscDispatch);
|
||||||
|
single(&mut result, 0x5C, source, State::Ground, A::EscDispatch);
|
||||||
|
|
||||||
|
// => escape_intermediate
|
||||||
|
range(
|
||||||
|
&mut result,
|
||||||
|
0x20,
|
||||||
|
0x2F,
|
||||||
|
source,
|
||||||
|
State::EscapeIntermediate,
|
||||||
|
A::Collect,
|
||||||
|
);
|
||||||
|
|
||||||
|
// => sos_pm_apc_string
|
||||||
|
single(&mut result, 0x58, source, State::SosPmApcString, A::None);
|
||||||
|
single(&mut result, 0x5E, source, State::SosPmApcString, A::None);
|
||||||
|
single(&mut result, 0x5F, source, State::SosPmApcString, A::None);
|
||||||
|
|
||||||
|
// => dcs_entry
|
||||||
|
single(&mut result, 0x50, source, State::DcsEntry, A::None);
|
||||||
|
|
||||||
|
// => csi_entry
|
||||||
|
single(&mut result, 0x5B, source, State::CsiEntry, A::None);
|
||||||
|
|
||||||
|
// => osc_string
|
||||||
|
single(&mut result, 0x5D, source, State::OscString, A::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// dcs_entry
|
||||||
|
{
|
||||||
|
let source = State::DcsEntry;
|
||||||
|
|
||||||
|
// events
|
||||||
|
single(&mut result, 0x19, source, source, A::Ignore);
|
||||||
|
range(&mut result, 0, 0x17, source, source, A::Ignore);
|
||||||
|
range(&mut result, 0x1C, 0x1F, source, source, A::Ignore);
|
||||||
|
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||||
|
|
||||||
|
// => dcs_intermediate
|
||||||
|
range(
|
||||||
|
&mut result,
|
||||||
|
0x20,
|
||||||
|
0x2F,
|
||||||
|
source,
|
||||||
|
State::DcsIntermediate,
|
||||||
|
A::Collect,
|
||||||
|
);
|
||||||
|
|
||||||
|
// => dcs_ignore
|
||||||
|
single(&mut result, 0x3A, source, State::DcsIgnore, A::None);
|
||||||
|
|
||||||
|
// => dcs_param
|
||||||
|
range(&mut result, 0x30, 0x39, source, State::DcsParam, A::Param);
|
||||||
|
single(&mut result, 0x3B, source, State::DcsParam, A::Param);
|
||||||
|
range(&mut result, 0x3C, 0x3F, source, State::DcsParam, A::Collect);
|
||||||
|
|
||||||
|
// => dcs_passthrough
|
||||||
|
range(
|
||||||
|
&mut result,
|
||||||
|
0x40,
|
||||||
|
0x7E,
|
||||||
|
source,
|
||||||
|
State::DcsPassthrough,
|
||||||
|
A::None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// dcs_intermediate
|
||||||
|
{
|
||||||
|
let source = State::DcsIntermediate;
|
||||||
|
|
||||||
|
// events
|
||||||
|
single(&mut result, 0x19, source, source, A::Ignore);
|
||||||
|
range(&mut result, 0, 0x17, source, source, A::Ignore);
|
||||||
|
range(&mut result, 0x1C, 0x1F, source, source, A::Ignore);
|
||||||
|
range(&mut result, 0x20, 0x2F, source, source, A::Collect);
|
||||||
|
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||||
|
|
||||||
|
// => dcs_ignore
|
||||||
|
range(&mut result, 0x30, 0x3F, source, State::DcsIgnore, A::None);
|
||||||
|
|
||||||
|
// => dcs_passthrough
|
||||||
|
range(
|
||||||
|
&mut result,
|
||||||
|
0x40,
|
||||||
|
0x7E,
|
||||||
|
source,
|
||||||
|
State::DcsPassthrough,
|
||||||
|
A::None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// dcs_ignore
|
||||||
|
{
|
||||||
|
let source = State::DcsIgnore;
|
||||||
|
|
||||||
|
// events
|
||||||
|
single(&mut result, 0x19, source, source, A::Ignore);
|
||||||
|
range(&mut result, 0, 0x17, source, source, A::Ignore);
|
||||||
|
range(&mut result, 0x1C, 0x1F, source, source, A::Ignore);
|
||||||
|
}
|
||||||
|
|
||||||
|
// dcs_param
|
||||||
|
{
|
||||||
|
let source = State::DcsParam;
|
||||||
|
|
||||||
|
// events
|
||||||
|
single(&mut result, 0x19, source, source, A::Ignore);
|
||||||
|
range(&mut result, 0, 0x17, source, source, A::Ignore);
|
||||||
|
range(&mut result, 0x1C, 0x1F, source, source, A::Ignore);
|
||||||
|
range(&mut result, 0x30, 0x39, source, source, A::Param);
|
||||||
|
single(&mut result, 0x3B, source, source, A::Param);
|
||||||
|
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||||
|
|
||||||
|
// => dcs_ignore
|
||||||
|
single(&mut result, 0x3A, source, State::DcsIgnore, A::None);
|
||||||
|
range(&mut result, 0x3C, 0x3F, source, State::DcsIgnore, A::None);
|
||||||
|
|
||||||
|
// => dcs_intermediate
|
||||||
|
range(
|
||||||
|
&mut result,
|
||||||
|
0x20,
|
||||||
|
0x2F,
|
||||||
|
source,
|
||||||
|
State::DcsIntermediate,
|
||||||
|
A::Collect,
|
||||||
|
);
|
||||||
|
|
||||||
|
// => dcs_passthrough
|
||||||
|
range(
|
||||||
|
&mut result,
|
||||||
|
0x40,
|
||||||
|
0x7E,
|
||||||
|
source,
|
||||||
|
State::DcsPassthrough,
|
||||||
|
A::None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// dcs_passthrough
|
||||||
|
{
|
||||||
|
let source = State::DcsPassthrough;
|
||||||
|
|
||||||
|
// events
|
||||||
|
single(&mut result, 0x19, source, source, A::Put);
|
||||||
|
range(&mut result, 0, 0x17, source, source, A::Put);
|
||||||
|
range(&mut result, 0x1C, 0x1F, source, source, A::Put);
|
||||||
|
range(&mut result, 0x20, 0x7E, source, source, A::Put);
|
||||||
|
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||||
|
}
|
||||||
|
|
||||||
|
// csi_param
|
||||||
|
{
|
||||||
|
let source = State::CsiParam;
|
||||||
|
|
||||||
|
// events
|
||||||
|
single(&mut result, 0x19, source, source, A::Execute);
|
||||||
|
range(&mut result, 0, 0x17, source, source, A::Execute);
|
||||||
|
range(&mut result, 0x1C, 0x1F, source, source, A::Execute);
|
||||||
|
range(&mut result, 0x30, 0x39, source, source, A::Param);
|
||||||
|
single(&mut result, 0x3A, source, source, A::Param);
|
||||||
|
single(&mut result, 0x3B, source, source, A::Param);
|
||||||
|
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||||
|
|
||||||
|
// => ground
|
||||||
|
range(
|
||||||
|
&mut result,
|
||||||
|
0x40,
|
||||||
|
0x7E,
|
||||||
|
source,
|
||||||
|
State::Ground,
|
||||||
|
A::CsiDispatch,
|
||||||
|
);
|
||||||
|
|
||||||
|
// => csi_ignore
|
||||||
|
range(&mut result, 0x3C, 0x3F, source, State::CsiIgnore, A::None);
|
||||||
|
|
||||||
|
// => csi_intermediate
|
||||||
|
range(
|
||||||
|
&mut result,
|
||||||
|
0x20,
|
||||||
|
0x2F,
|
||||||
|
source,
|
||||||
|
State::CsiIntermediate,
|
||||||
|
A::Collect,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// csi_ignore
|
||||||
|
{
|
||||||
|
let source = State::CsiIgnore;
|
||||||
|
|
||||||
|
// events
|
||||||
|
single(&mut result, 0x19, source, source, A::Execute);
|
||||||
|
range(&mut result, 0, 0x17, source, source, A::Execute);
|
||||||
|
range(&mut result, 0x1C, 0x1F, source, source, A::Execute);
|
||||||
|
range(&mut result, 0x20, 0x3F, source, source, A::Ignore);
|
||||||
|
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||||
|
|
||||||
|
// => ground
|
||||||
|
range(&mut result, 0x40, 0x7E, source, State::Ground, A::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// csi_intermediate
|
||||||
|
{
|
||||||
|
let source = State::CsiIntermediate;
|
||||||
|
|
||||||
|
// events
|
||||||
|
single(&mut result, 0x19, source, source, A::Execute);
|
||||||
|
range(&mut result, 0, 0x17, source, source, A::Execute);
|
||||||
|
range(&mut result, 0x1C, 0x1F, source, source, A::Execute);
|
||||||
|
range(&mut result, 0x20, 0x2F, source, source, A::Collect);
|
||||||
|
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||||
|
|
||||||
|
// => ground
|
||||||
|
range(
|
||||||
|
&mut result,
|
||||||
|
0x40,
|
||||||
|
0x7E,
|
||||||
|
source,
|
||||||
|
State::Ground,
|
||||||
|
A::CsiDispatch,
|
||||||
|
);
|
||||||
|
|
||||||
|
// => csi_ignore
|
||||||
|
range(&mut result, 0x30, 0x3F, source, State::CsiIgnore, A::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// csi_entry
|
||||||
|
{
|
||||||
|
let source = State::CsiEntry;
|
||||||
|
|
||||||
|
// events
|
||||||
|
single(&mut result, 0x19, source, source, A::Execute);
|
||||||
|
range(&mut result, 0, 0x17, source, source, A::Execute);
|
||||||
|
range(&mut result, 0x1C, 0x1F, source, source, A::Execute);
|
||||||
|
single(&mut result, 0x7F, source, source, A::Ignore);
|
||||||
|
|
||||||
|
// => ground
|
||||||
|
range(
|
||||||
|
&mut result,
|
||||||
|
0x40,
|
||||||
|
0x7E,
|
||||||
|
source,
|
||||||
|
State::Ground,
|
||||||
|
A::CsiDispatch,
|
||||||
|
);
|
||||||
|
|
||||||
|
// => csi_ignore
|
||||||
|
single(&mut result, 0x3A, source, State::CsiIgnore, A::None);
|
||||||
|
|
||||||
|
// => csi_intermediate
|
||||||
|
range(
|
||||||
|
&mut result,
|
||||||
|
0x20,
|
||||||
|
0x2F,
|
||||||
|
source,
|
||||||
|
State::CsiIntermediate,
|
||||||
|
A::Collect,
|
||||||
|
);
|
||||||
|
|
||||||
|
// => csi_param
|
||||||
|
range(&mut result, 0x30, 0x39, source, State::CsiParam, A::Param);
|
||||||
|
single(&mut result, 0x3B, source, State::CsiParam, A::Param);
|
||||||
|
range(&mut result, 0x3C, 0x3F, source, State::CsiParam, A::Collect);
|
||||||
|
}
|
||||||
|
|
||||||
|
// osc_string
|
||||||
|
{
|
||||||
|
let source = State::OscString;
|
||||||
|
|
||||||
|
// events
|
||||||
|
single(&mut result, 0x19, source, source, A::Ignore);
|
||||||
|
range(&mut result, 0, 0x06, source, source, A::Ignore);
|
||||||
|
range(&mut result, 0x08, 0x17, source, source, A::Ignore);
|
||||||
|
range(&mut result, 0x1C, 0x1F, source, source, A::Ignore);
|
||||||
|
range(&mut result, 0x20, 0xFF, source, source, A::OscPut);
|
||||||
|
|
||||||
|
// XTerm accepts either BEL or ST for terminating OSC
|
||||||
|
// sequences, and when returning information, uses the same
|
||||||
|
// terminator used in a query.
|
||||||
|
single(&mut result, 0x07, source, State::Ground, A::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create our immutable version
|
||||||
|
let mut fin: Table = [[Transition {
|
||||||
|
state: State::Ground,
|
||||||
|
action: A::None,
|
||||||
|
}; NUM_STATES]; 256];
|
||||||
|
let mut i = 0;
|
||||||
|
while i < 256 {
|
||||||
|
let mut j = 0;
|
||||||
|
while j < NUM_STATES {
|
||||||
|
fin[i][j] = match result[i][j] {
|
||||||
|
Some(t) => t,
|
||||||
|
None => transition(STATES[j], A::None),
|
||||||
|
};
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fin
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn single(t: &mut OptionalTable, c: u8, s0: State, s1: State, a: A) {
|
||||||
|
t[c as usize][s0 as usize] = Some(transition(s1, a));
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn range(t: &mut OptionalTable, from: u8, to: u8, s0: State, s1: State, a: A) {
|
||||||
|
let mut i = from;
|
||||||
|
loop {
|
||||||
|
single(t, i, s0, s1, a);
|
||||||
|
// If 'to' is 0xFF, our next pass would overflow.
|
||||||
|
if i == to {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn transition(state: State, action: A) -> Transition {
|
||||||
|
Transition { state, action }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_builds() {
|
||||||
|
// This forces evaluation of the table, so we're just testing
|
||||||
|
// that it succeeds in creation.
|
||||||
|
let _ = &TABLE;
|
||||||
|
assert_eq!(
|
||||||
|
TABLE[b'a' as usize][State::Ground as usize],
|
||||||
|
transition(State::Ground, A::Print)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
TABLE[0x1B][State::CsiParam as usize],
|
||||||
|
transition(State::Escape, A::None)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,20 @@
|
|||||||
//! VT-series parser for escape and control sequences
|
//! VT-series parser for escape and control sequences
|
||||||
//! (ZIG/src/terminal/Parser.zig).
|
//! (ZIG/src/terminal/Parser.zig).
|
||||||
//!
|
//!
|
||||||
//! NOTE(W1): this file is owned by work package W1. `MAX_PARAMS` and
|
//! This is implemented directly as the state machine described on
|
||||||
//! `SepList` were pre-created by the orchestrator because `sgr` (W2)
|
//! vt100.net: https://vt100.net/emu/dec_ansi_parser
|
||||||
//! depends on them; keep their API stable.
|
//!
|
||||||
|
//! `MAX_PARAMS` and `SepList` were pre-created by the orchestrator because
|
||||||
|
//! `sgr` depends on them; keep their API stable.
|
||||||
|
//!
|
||||||
|
//! Rust port note: Zig's `Action` borrows slices out of the parser
|
||||||
|
//! (`csi_dispatch.params` etc., "only valid until the next call to next").
|
||||||
|
//! Here the CSI/ESC/DCS payloads are small fixed-size inline copies exposed
|
||||||
|
//! through `params()` / `intermediates()` accessors, so an `Action` owns its
|
||||||
|
//! data and is independent of the parser's lifetime.
|
||||||
|
|
||||||
|
use crate::osc;
|
||||||
|
use crate::parse_table::TABLE;
|
||||||
|
|
||||||
/// Maximum number of CSI parameters. This is a const in Ghostty so we
|
/// Maximum number of CSI parameters. This is a const in Ghostty so we
|
||||||
/// match it.
|
/// match it.
|
||||||
@@ -50,3 +61,924 @@ impl SepList {
|
|||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Maximum number of intermediate characters during parsing. This is
|
||||||
|
/// 4 because we also use the intermediates array for UTF8 decoding which
|
||||||
|
/// can be at most 4 bytes.
|
||||||
|
pub const MAX_INTERMEDIATE: usize = 4;
|
||||||
|
|
||||||
|
/// States for the state machine
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum State {
|
||||||
|
Ground,
|
||||||
|
Escape,
|
||||||
|
EscapeIntermediate,
|
||||||
|
CsiEntry,
|
||||||
|
CsiIntermediate,
|
||||||
|
CsiParam,
|
||||||
|
CsiIgnore,
|
||||||
|
DcsEntry,
|
||||||
|
DcsParam,
|
||||||
|
DcsIntermediate,
|
||||||
|
DcsPassthrough,
|
||||||
|
DcsIgnore,
|
||||||
|
OscString,
|
||||||
|
SosPmApcString,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transition action is an action that can be taken during a state
|
||||||
|
/// transition. This is more of an internal action, not one used by
|
||||||
|
/// end users, typically.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum TransitionAction {
|
||||||
|
None,
|
||||||
|
Ignore,
|
||||||
|
Print,
|
||||||
|
Execute,
|
||||||
|
Collect,
|
||||||
|
Param,
|
||||||
|
EscDispatch,
|
||||||
|
CsiDispatch,
|
||||||
|
Put,
|
||||||
|
OscPut,
|
||||||
|
ApcPut,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The separator used for CSI params.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Sep {
|
||||||
|
Semicolon = 0,
|
||||||
|
Colon = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CSI dispatch payload (Zig `Action.CSI`).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Csi {
|
||||||
|
intermediates_buf: [u8; MAX_INTERMEDIATE],
|
||||||
|
intermediates_len: u8,
|
||||||
|
params_buf: [u16; MAX_PARAMS],
|
||||||
|
params_len: u8,
|
||||||
|
/// The list of separators used for CSI params. The value of the
|
||||||
|
/// bit can be mapped to Sep. The index of this bit set specifies
|
||||||
|
/// the separator AFTER that param. For example: 0;4:3 would have
|
||||||
|
/// index 1 set.
|
||||||
|
pub params_sep: SepList,
|
||||||
|
pub final_: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Csi {
|
||||||
|
pub fn intermediates(&self) -> &[u8] {
|
||||||
|
&self.intermediates_buf[..self.intermediates_len as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn params(&self) -> &[u16] {
|
||||||
|
&self.params_buf[..self.params_len as usize]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Csi {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"ESC [ {} {:?} {}",
|
||||||
|
String::from_utf8_lossy(self.intermediates()),
|
||||||
|
self.params(),
|
||||||
|
self.final_ as char
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ESC dispatch payload (Zig `Action.ESC`).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Esc {
|
||||||
|
intermediates_buf: [u8; MAX_INTERMEDIATE],
|
||||||
|
intermediates_len: u8,
|
||||||
|
pub final_: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Esc {
|
||||||
|
pub fn intermediates(&self) -> &[u8] {
|
||||||
|
&self.intermediates_buf[..self.intermediates_len as usize]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Esc {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"ESC {} {}",
|
||||||
|
String::from_utf8_lossy(self.intermediates()),
|
||||||
|
self.final_ as char
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DCS hook payload (Zig `Action.DCS`).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Dcs {
|
||||||
|
intermediates_buf: [u8; MAX_INTERMEDIATE],
|
||||||
|
intermediates_len: u8,
|
||||||
|
params_buf: [u16; MAX_PARAMS],
|
||||||
|
params_len: u8,
|
||||||
|
pub final_: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dcs {
|
||||||
|
/// Build a DCS from slices (extra elements beyond the maximums are
|
||||||
|
/// dropped). Useful for tests and for consumers constructing hooks.
|
||||||
|
pub fn new(intermediates: &[u8], params: &[u16], final_: u8) -> Self {
|
||||||
|
let mut d = Dcs {
|
||||||
|
intermediates_buf: [0; MAX_INTERMEDIATE],
|
||||||
|
intermediates_len: 0,
|
||||||
|
params_buf: [0; MAX_PARAMS],
|
||||||
|
params_len: 0,
|
||||||
|
final_,
|
||||||
|
};
|
||||||
|
let n = intermediates.len().min(MAX_INTERMEDIATE);
|
||||||
|
d.intermediates_buf[..n].copy_from_slice(&intermediates[..n]);
|
||||||
|
d.intermediates_len = n as u8;
|
||||||
|
let n = params.len().min(MAX_PARAMS);
|
||||||
|
d.params_buf[..n].copy_from_slice(¶ms[..n]);
|
||||||
|
d.params_len = n as u8;
|
||||||
|
d
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn intermediates(&self) -> &[u8] {
|
||||||
|
&self.intermediates_buf[..self.intermediates_len as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn params(&self) -> &[u16] {
|
||||||
|
&self.params_buf[..self.params_len as usize]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Action is the action that a caller of the parser is expected to
|
||||||
|
/// take as a result of some input character.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Action {
|
||||||
|
/// Draw character to the screen. This is a unicode codepoint.
|
||||||
|
Print(u32),
|
||||||
|
|
||||||
|
/// Execute the C0 or C1 function.
|
||||||
|
Execute(u8),
|
||||||
|
|
||||||
|
/// Execute the CSI command.
|
||||||
|
CsiDispatch(Csi),
|
||||||
|
|
||||||
|
/// Execute the ESC command.
|
||||||
|
EscDispatch(Esc),
|
||||||
|
|
||||||
|
/// Execute the OSC command.
|
||||||
|
OscDispatch(osc::Command),
|
||||||
|
|
||||||
|
/// DCS-related events.
|
||||||
|
DcsHook(Dcs),
|
||||||
|
DcsPut(u8),
|
||||||
|
DcsUnhook,
|
||||||
|
|
||||||
|
/// APC data
|
||||||
|
ApcStart,
|
||||||
|
ApcPut(u8),
|
||||||
|
ApcEnd,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The VT parser state machine.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Parser {
|
||||||
|
/// Current state of the state machine
|
||||||
|
pub state: State,
|
||||||
|
|
||||||
|
/// Intermediate tracking.
|
||||||
|
intermediates: [u8; MAX_INTERMEDIATE],
|
||||||
|
intermediates_idx: u8,
|
||||||
|
|
||||||
|
/// Param tracking, building
|
||||||
|
params: [u16; MAX_PARAMS],
|
||||||
|
params_sep: SepList,
|
||||||
|
params_idx: u8,
|
||||||
|
param_acc: u16,
|
||||||
|
param_acc_idx: u8,
|
||||||
|
|
||||||
|
/// Parser for OSC sequences
|
||||||
|
pub osc_parser: osc::Parser,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Parser {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parser {
|
||||||
|
pub fn new() -> Parser {
|
||||||
|
Parser {
|
||||||
|
state: State::Ground,
|
||||||
|
intermediates: [0; MAX_INTERMEDIATE],
|
||||||
|
intermediates_idx: 0,
|
||||||
|
params: [0; MAX_PARAMS],
|
||||||
|
params_sep: SepList::init_empty(),
|
||||||
|
params_idx: 0,
|
||||||
|
param_acc: 0,
|
||||||
|
param_acc_idx: 0,
|
||||||
|
osc_parser: osc::Parser::init(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Next consumes the next character c and returns the actions to execute.
|
||||||
|
/// Up to 3 actions may need to be executed -- in order -- representing
|
||||||
|
/// the state exit, transition, and entry actions.
|
||||||
|
pub fn next(&mut self, c: u8) -> [Option<Action>; 3] {
|
||||||
|
let effect = TABLE[c as usize][self.state as usize];
|
||||||
|
|
||||||
|
let next_state = effect.state;
|
||||||
|
let action = effect.action;
|
||||||
|
|
||||||
|
// When going from one state to another, the actions take place in this order:
|
||||||
|
//
|
||||||
|
// 1. exit action from old state
|
||||||
|
// 2. transition action
|
||||||
|
// 3. entry action to new state
|
||||||
|
|
||||||
|
// Exit depends on current state
|
||||||
|
let exit = if self.state == next_state {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
match self.state {
|
||||||
|
State::OscString => self.osc_parser.end(Some(c)).map(Action::OscDispatch),
|
||||||
|
State::DcsPassthrough => Some(Action::DcsUnhook),
|
||||||
|
State::SosPmApcString => Some(Action::ApcEnd),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let transition = self.do_action(action, c);
|
||||||
|
|
||||||
|
// Entry depends on new state
|
||||||
|
let entry = if self.state == next_state {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
match next_state {
|
||||||
|
State::Escape | State::DcsEntry | State::CsiEntry => {
|
||||||
|
self.clear();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
State::OscString => {
|
||||||
|
self.osc_parser.reset();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
State::DcsPassthrough => 'dcs_hook: {
|
||||||
|
// Ignore too many parameters
|
||||||
|
if self.params_idx as usize >= MAX_PARAMS {
|
||||||
|
break 'dcs_hook None;
|
||||||
|
}
|
||||||
|
// Finalize parameters
|
||||||
|
if self.param_acc_idx > 0 {
|
||||||
|
self.params[self.params_idx as usize] = self.param_acc;
|
||||||
|
self.params_idx += 1;
|
||||||
|
}
|
||||||
|
Some(Action::DcsHook(Dcs::new(
|
||||||
|
&self.intermediates[..self.intermediates_idx as usize],
|
||||||
|
&self.params[..self.params_idx as usize],
|
||||||
|
c,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
State::SosPmApcString => Some(Action::ApcStart),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// After generating the actions, we set our next state.
|
||||||
|
self.state = next_state;
|
||||||
|
|
||||||
|
[exit, transition, entry]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn collect(&mut self, c: u8) {
|
||||||
|
if self.intermediates_idx as usize >= MAX_INTERMEDIATE {
|
||||||
|
log::warn!("invalid intermediates count");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.intermediates[self.intermediates_idx as usize] = c;
|
||||||
|
self.intermediates_idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn do_action(&mut self, action: TransitionAction, c: u8) -> Option<Action> {
|
||||||
|
match action {
|
||||||
|
TransitionAction::None | TransitionAction::Ignore => None,
|
||||||
|
TransitionAction::Print => Some(Action::Print(c as u32)),
|
||||||
|
TransitionAction::Execute => Some(Action::Execute(c)),
|
||||||
|
TransitionAction::Collect => {
|
||||||
|
self.collect(c);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
TransitionAction::Param => {
|
||||||
|
// Semicolon separates parameters. If we encounter a semicolon
|
||||||
|
// we need to store and move on to the next parameter.
|
||||||
|
if c == b';' || c == b':' {
|
||||||
|
// Ignore too many parameters
|
||||||
|
if self.params_idx as usize >= MAX_PARAMS {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set param final value
|
||||||
|
self.params[self.params_idx as usize] = self.param_acc;
|
||||||
|
if c == b':' {
|
||||||
|
self.params_sep.set(self.params_idx as usize);
|
||||||
|
}
|
||||||
|
self.params_idx += 1;
|
||||||
|
|
||||||
|
// Reset current param value to 0
|
||||||
|
self.param_acc = 0;
|
||||||
|
self.param_acc_idx = 0;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A numeric value. Add it to our accumulator.
|
||||||
|
self.param_acc = self.param_acc.saturating_mul(10);
|
||||||
|
self.param_acc = self.param_acc.saturating_add((c - b'0') as u16);
|
||||||
|
|
||||||
|
// Increment our accumulator index. If we overflow then
|
||||||
|
// we're out of bounds and we exit immediately.
|
||||||
|
self.param_acc_idx = self.param_acc_idx.wrapping_add(1);
|
||||||
|
|
||||||
|
// The client is expected to perform no action.
|
||||||
|
None
|
||||||
|
}
|
||||||
|
TransitionAction::OscPut => {
|
||||||
|
self.osc_parser.next(c);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
TransitionAction::CsiDispatch => {
|
||||||
|
// Ignore too many parameters
|
||||||
|
if self.params_idx as usize >= MAX_PARAMS {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize parameters if we have one
|
||||||
|
if self.param_acc_idx > 0 {
|
||||||
|
self.params[self.params_idx as usize] = self.param_acc;
|
||||||
|
self.params_idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut csi = Csi {
|
||||||
|
intermediates_buf: self.intermediates,
|
||||||
|
intermediates_len: self.intermediates_idx,
|
||||||
|
params_buf: [0; MAX_PARAMS],
|
||||||
|
params_len: self.params_idx,
|
||||||
|
params_sep: self.params_sep,
|
||||||
|
final_: c,
|
||||||
|
};
|
||||||
|
csi.params_buf[..self.params_idx as usize]
|
||||||
|
.copy_from_slice(&self.params[..self.params_idx as usize]);
|
||||||
|
|
||||||
|
// We only allow colon or mixed separators for the 'm' command.
|
||||||
|
if c != b'm' && self.params_sep.count() > 0 {
|
||||||
|
log::warn!(
|
||||||
|
"CSI colon or mixed separators only allowed for 'm' command, got: {csi}"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Action::CsiDispatch(csi))
|
||||||
|
}
|
||||||
|
TransitionAction::EscDispatch => Some(Action::EscDispatch(Esc {
|
||||||
|
intermediates_buf: self.intermediates,
|
||||||
|
intermediates_len: self.intermediates_idx,
|
||||||
|
final_: c,
|
||||||
|
})),
|
||||||
|
TransitionAction::Put => Some(Action::DcsPut(c)),
|
||||||
|
TransitionAction::ApcPut => Some(Action::ApcPut(c)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.intermediates_idx = 0;
|
||||||
|
self.params_idx = 0;
|
||||||
|
self.params_sep = SepList::init_empty();
|
||||||
|
self.param_acc = 0;
|
||||||
|
self.param_acc_idx = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use super::*;
|
||||||
|
use crate::osc::Command;
|
||||||
|
|
||||||
|
fn feed_quiet(p: &mut Parser, s: &[u8]) {
|
||||||
|
for &c in s {
|
||||||
|
let a = p.next(c);
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(a[1].is_none());
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn csi(a: &[Option<Action>; 3]) -> Csi {
|
||||||
|
match a[1].as_ref().expect("csi_dispatch") {
|
||||||
|
Action::CsiDispatch(c) => *c,
|
||||||
|
other => panic!("expected csi_dispatch, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parser_basic() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x9E);
|
||||||
|
assert_eq!(p.state, State::SosPmApcString);
|
||||||
|
p.next(0x9C);
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
|
||||||
|
{
|
||||||
|
let a = p.next(b'a');
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(matches!(a[1], Some(Action::Print(_))));
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let a = p.next(0x19);
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(matches!(a[1], Some(Action::Execute(_))));
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn esc_esc_paren_b() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B);
|
||||||
|
p.next(b'(');
|
||||||
|
|
||||||
|
let a = p.next(b'B');
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
let Some(Action::EscDispatch(d)) = &a[1] else {
|
||||||
|
panic!("expected esc_dispatch");
|
||||||
|
};
|
||||||
|
assert_eq!(d.final_, b'B');
|
||||||
|
assert_eq!(d.intermediates(), b"(");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn csi_esc_bracket_h() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B);
|
||||||
|
p.next(0x5B);
|
||||||
|
|
||||||
|
let a = p.next(0x48);
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
let d = csi(&a);
|
||||||
|
assert_eq!(d.final_, 0x48);
|
||||||
|
assert_eq!(d.params().len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn csi_esc_bracket_1_semicolon_4_h() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B);
|
||||||
|
p.next(0x5B);
|
||||||
|
p.next(0x31); // 1
|
||||||
|
p.next(0x3B); // ;
|
||||||
|
p.next(0x34); // 4
|
||||||
|
|
||||||
|
let a = p.next(0x48); // H
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
let d = csi(&a);
|
||||||
|
assert_eq!(d.final_, b'H');
|
||||||
|
assert_eq!(d.params(), &[1, 4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn csi_sgr_esc_bracket_38_colon_2_m() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B);
|
||||||
|
p.next(b'[');
|
||||||
|
p.next(b'3');
|
||||||
|
p.next(b'8');
|
||||||
|
p.next(b':');
|
||||||
|
p.next(b'2');
|
||||||
|
|
||||||
|
let a = p.next(b'm');
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
let d = csi(&a);
|
||||||
|
assert_eq!(d.final_, b'm');
|
||||||
|
assert_eq!(d.params(), &[38, 2]);
|
||||||
|
assert!(d.params_sep.is_set(0));
|
||||||
|
assert!(!d.params_sep.is_set(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn csi_sgr_colon_followed_by_semicolon() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B);
|
||||||
|
feed_quiet(&mut p, b"[48:2");
|
||||||
|
|
||||||
|
{
|
||||||
|
let a = p.next(b'm');
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(matches!(a[1], Some(Action::CsiDispatch(_))));
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
p.next(0x1B);
|
||||||
|
p.next(b'[');
|
||||||
|
{
|
||||||
|
let a = p.next(b'H');
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(matches!(a[1], Some(Action::CsiDispatch(_))));
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn csi_sgr_mixed_colon_and_semicolon() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B);
|
||||||
|
feed_quiet(&mut p, b"[38:5:1;48:5:0");
|
||||||
|
|
||||||
|
let a = p.next(b'm');
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(matches!(a[1], Some(Action::CsiDispatch(_))));
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn csi_sgr_esc_bracket_48_colon_2_m() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B);
|
||||||
|
feed_quiet(&mut p, b"[48:2:240:143:104");
|
||||||
|
|
||||||
|
let a = p.next(b'm');
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
let d = csi(&a);
|
||||||
|
assert_eq!(d.final_, b'm');
|
||||||
|
assert_eq!(d.params(), &[48, 2, 240, 143, 104]);
|
||||||
|
for (i, expect) in [true, true, true, true, false].iter().enumerate() {
|
||||||
|
assert_eq!(d.params_sep.is_set(i), *expect, "sep {i}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn csi_sgr_esc_bracket_4_colon_3_m_colon() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B);
|
||||||
|
p.next(b'[');
|
||||||
|
p.next(b'4');
|
||||||
|
p.next(b':');
|
||||||
|
p.next(b'3');
|
||||||
|
|
||||||
|
let a = p.next(b'm');
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
let d = csi(&a);
|
||||||
|
assert_eq!(d.final_, b'm');
|
||||||
|
assert_eq!(d.params(), &[4, 3]);
|
||||||
|
assert!(d.params_sep.is_set(0));
|
||||||
|
assert!(!d.params_sep.is_set(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn csi_sgr_with_many_blank_and_colon() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B);
|
||||||
|
feed_quiet(&mut p, b"[58:2::240:143:104");
|
||||||
|
|
||||||
|
let a = p.next(b'm');
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
let d = csi(&a);
|
||||||
|
assert_eq!(d.final_, b'm');
|
||||||
|
assert_eq!(d.params(), &[58, 2, 0, 240, 143, 104]);
|
||||||
|
for (i, expect) in [true, true, true, true, true, false].iter().enumerate() {
|
||||||
|
assert_eq!(d.params_sep.is_set(i), *expect, "sep {i}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is from a Kakoune actual SGR sequence.
|
||||||
|
#[test]
|
||||||
|
fn csi_sgr_mixed_colon_and_semicolon_with_blank() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B);
|
||||||
|
feed_quiet(&mut p, b"[;4:3;38;2;175;175;215;58:2::190:80:70");
|
||||||
|
|
||||||
|
let a = p.next(b'm');
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
let d = csi(&a);
|
||||||
|
assert_eq!(d.final_, b'm');
|
||||||
|
assert_eq!(
|
||||||
|
d.params(),
|
||||||
|
&[0, 4, 3, 38, 2, 175, 175, 215, 58, 2, 0, 190, 80, 70]
|
||||||
|
);
|
||||||
|
let set = [1usize, 8, 9, 10, 11, 12];
|
||||||
|
for i in 0..14 {
|
||||||
|
assert_eq!(d.params_sep.is_set(i), set.contains(&i), "sep {i}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is from a Kakoune actual SGR sequence also.
|
||||||
|
#[test]
|
||||||
|
fn csi_sgr_mixed_colon_and_semicolon_setting_underline_bg_fg() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B);
|
||||||
|
feed_quiet(
|
||||||
|
&mut p,
|
||||||
|
b"[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136",
|
||||||
|
);
|
||||||
|
|
||||||
|
let a = p.next(b'm');
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
let d = csi(&a);
|
||||||
|
assert_eq!(d.final_, b'm');
|
||||||
|
assert_eq!(
|
||||||
|
d.params(),
|
||||||
|
&[
|
||||||
|
4, 3, 38, 2, 51, 51, 51, 48, 2, 170, 170, 170, 58, 2, 255, 97, 136
|
||||||
|
]
|
||||||
|
);
|
||||||
|
for i in 0..17 {
|
||||||
|
assert_eq!(d.params_sep.is_set(i), i == 0, "sep {i}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn csi_colon_for_non_m_final() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B);
|
||||||
|
feed_quiet(&mut p, b"[38:2h");
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn csi_request_mode_decrqm() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B);
|
||||||
|
feed_quiet(&mut p, b"[?2026$");
|
||||||
|
|
||||||
|
let a = p.next(b'p');
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
let d = csi(&a);
|
||||||
|
assert_eq!(d.final_, b'p');
|
||||||
|
assert_eq!(d.intermediates(), b"?$");
|
||||||
|
assert_eq!(d.params(), &[2026]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn csi_change_cursor() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B);
|
||||||
|
feed_quiet(&mut p, b"[3 ");
|
||||||
|
|
||||||
|
let a = p.next(b'q');
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
let d = csi(&a);
|
||||||
|
assert_eq!(d.final_, b'q');
|
||||||
|
assert_eq!(d.intermediates(), b" ");
|
||||||
|
assert_eq!(d.params(), &[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_change_window_title() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
for c in [0x1B, b']', b'0', b';', b'a', b'b', b'c'] {
|
||||||
|
p.next(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
let a = p.next(0x07); // BEL
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[1].is_none());
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
assert_eq!(
|
||||||
|
a[0],
|
||||||
|
Some(Action::OscDispatch(Command::ChangeWindowTitle(
|
||||||
|
b"abc".to_vec()
|
||||||
|
)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_change_window_title_end_in_esc() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
for c in [0x1B, b']', b'0', b';', b'a', b'b', b'c'] {
|
||||||
|
p.next(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
let a = p.next(0x1B);
|
||||||
|
p.next(b'\\');
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[1].is_none());
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
assert_eq!(
|
||||||
|
a[0],
|
||||||
|
Some(Action::OscDispatch(Command::ChangeWindowTitle(
|
||||||
|
b"abc".to_vec()
|
||||||
|
)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/darrenstarr/VtNetCore/pull/14
|
||||||
|
// Saw this on HN, decided to add a test case because why not.
|
||||||
|
#[test]
|
||||||
|
fn osc_112_incomplete_sequence() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.osc_parser.alloc = true;
|
||||||
|
|
||||||
|
for c in [0x1B, b']', b'1', b'1', b'2'] {
|
||||||
|
p.next(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
let a = p.next(0x07);
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[1].is_none());
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
let Some(Action::OscDispatch(Command::ColorOperation {
|
||||||
|
op,
|
||||||
|
requests,
|
||||||
|
terminator,
|
||||||
|
})) = a[0].clone()
|
||||||
|
else {
|
||||||
|
panic!("expected color_operation");
|
||||||
|
};
|
||||||
|
assert_eq!(terminator, osc::Terminator::Bel);
|
||||||
|
assert_eq!(op, osc::color::Operation::Osc112);
|
||||||
|
assert_eq!(requests.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
requests[0],
|
||||||
|
osc::color::Request::Reset(osc::color::Target::Dynamic(crate::color::Dynamic::Cursor))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osc_104_empty() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.osc_parser.alloc = true;
|
||||||
|
|
||||||
|
for c in [0x1B, b']', b'1', b'0', b'4'] {
|
||||||
|
p.next(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
let a = p.next(0x07);
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[1].is_none());
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
let Some(Action::OscDispatch(Command::ColorOperation {
|
||||||
|
op,
|
||||||
|
requests,
|
||||||
|
terminator,
|
||||||
|
})) = a[0].clone()
|
||||||
|
else {
|
||||||
|
panic!("expected color_operation");
|
||||||
|
};
|
||||||
|
assert_eq!(terminator, osc::Terminator::Bel);
|
||||||
|
assert_eq!(op, osc::color::Operation::Osc104);
|
||||||
|
assert_eq!(requests.len(), 1);
|
||||||
|
assert_eq!(requests[0], osc::color::Request::ResetPalette);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn csi_too_many_params() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B);
|
||||||
|
p.next(b'[');
|
||||||
|
for _ in 0..100 {
|
||||||
|
p.next(b'1');
|
||||||
|
p.next(b';');
|
||||||
|
}
|
||||||
|
p.next(b'1');
|
||||||
|
|
||||||
|
let a = p.next(b'C');
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(a[1].is_none());
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn csi_sgr_with_up_to_our_max_parameters() {
|
||||||
|
for max in 1..=MAX_PARAMS {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B);
|
||||||
|
p.next(b'[');
|
||||||
|
|
||||||
|
for _ in 0..max - 1 {
|
||||||
|
p.next(b'1');
|
||||||
|
p.next(b';');
|
||||||
|
}
|
||||||
|
p.next(b'2');
|
||||||
|
|
||||||
|
let a = p.next(b'H');
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
let c = csi(&a);
|
||||||
|
assert_eq!(c.params().len(), max);
|
||||||
|
assert_eq!(c.params()[max - 1], 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn csi_sgr_beyond_our_max_drops_it() {
|
||||||
|
// Has to be +2 for the loops below
|
||||||
|
let max = MAX_PARAMS + 2;
|
||||||
|
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B);
|
||||||
|
p.next(b'[');
|
||||||
|
|
||||||
|
for _ in 0..max - 1 {
|
||||||
|
p.next(b'1');
|
||||||
|
p.next(b';');
|
||||||
|
}
|
||||||
|
p.next(b'2');
|
||||||
|
|
||||||
|
let a = p.next(b'H');
|
||||||
|
assert_eq!(p.state, State::Ground);
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(a[1].is_none());
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dcs_xtgettcap() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B);
|
||||||
|
feed_quiet(&mut p, b"P+");
|
||||||
|
|
||||||
|
let a = p.next(b'q');
|
||||||
|
assert_eq!(p.state, State::DcsPassthrough);
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(a[1].is_none());
|
||||||
|
let Some(Action::DcsHook(hook)) = &a[2] else {
|
||||||
|
panic!("expected dcs_hook");
|
||||||
|
};
|
||||||
|
assert_eq!(hook.intermediates(), b"+");
|
||||||
|
assert_eq!(hook.params(), &[] as &[u16]);
|
||||||
|
assert_eq!(hook.final_, b'q');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dcs_params() {
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B);
|
||||||
|
feed_quiet(&mut p, b"P1000");
|
||||||
|
|
||||||
|
let a = p.next(b'p');
|
||||||
|
assert_eq!(p.state, State::DcsPassthrough);
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(a[1].is_none());
|
||||||
|
let Some(Action::DcsHook(hook)) = &a[2] else {
|
||||||
|
panic!("expected dcs_hook");
|
||||||
|
};
|
||||||
|
assert_eq!(hook.params(), &[1000]);
|
||||||
|
assert_eq!(hook.final_, b'p');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dcs_too_many_params() {
|
||||||
|
// Regression test for a crash found by fuzzing (afl). When a DCS
|
||||||
|
// sequence has more than MAX_PARAMS parameters and param_acc_idx > 0,
|
||||||
|
// entering dcs_passthrough wrote to params[params_idx] without a
|
||||||
|
// bounds check, causing an out-of-bounds access.
|
||||||
|
let mut p = Parser::new();
|
||||||
|
p.next(0x1B); // ESC
|
||||||
|
p.next(b'P'); // DCS entry
|
||||||
|
|
||||||
|
// Feed a digit then MAX_PARAMS semicolons to fill all param slots.
|
||||||
|
p.next(b'6');
|
||||||
|
for _ in 0..MAX_PARAMS {
|
||||||
|
p.next(b';');
|
||||||
|
}
|
||||||
|
// Feed another digit so param_acc_idx > 0 while params_idx == MAX_PARAMS.
|
||||||
|
p.next(b'7');
|
||||||
|
|
||||||
|
// A final byte triggers entry to dcs_passthrough. The DCS should
|
||||||
|
// be dropped entirely, consistent with how CSI handles overflow.
|
||||||
|
let a = p.next(b'p');
|
||||||
|
assert!(a[0].is_none());
|
||||||
|
assert!(a[1].is_none());
|
||||||
|
assert!(a[2].is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,782 @@
|
|||||||
|
255 250 250 snow
|
||||||
|
248 248 255 ghost white
|
||||||
|
248 248 255 GhostWhite
|
||||||
|
245 245 245 white smoke
|
||||||
|
245 245 245 WhiteSmoke
|
||||||
|
220 220 220 gainsboro
|
||||||
|
255 250 240 floral white
|
||||||
|
255 250 240 FloralWhite
|
||||||
|
253 245 230 old lace
|
||||||
|
253 245 230 OldLace
|
||||||
|
250 240 230 linen
|
||||||
|
250 235 215 antique white
|
||||||
|
250 235 215 AntiqueWhite
|
||||||
|
255 239 213 papaya whip
|
||||||
|
255 239 213 PapayaWhip
|
||||||
|
255 235 205 blanched almond
|
||||||
|
255 235 205 BlanchedAlmond
|
||||||
|
255 228 196 bisque
|
||||||
|
255 218 185 peach puff
|
||||||
|
255 218 185 PeachPuff
|
||||||
|
255 222 173 navajo white
|
||||||
|
255 222 173 NavajoWhite
|
||||||
|
255 228 181 moccasin
|
||||||
|
255 248 220 cornsilk
|
||||||
|
255 255 240 ivory
|
||||||
|
255 250 205 lemon chiffon
|
||||||
|
255 250 205 LemonChiffon
|
||||||
|
255 245 238 seashell
|
||||||
|
240 255 240 honeydew
|
||||||
|
245 255 250 mint cream
|
||||||
|
245 255 250 MintCream
|
||||||
|
240 255 255 azure
|
||||||
|
240 248 255 alice blue
|
||||||
|
240 248 255 AliceBlue
|
||||||
|
230 230 250 lavender
|
||||||
|
255 240 245 lavender blush
|
||||||
|
255 240 245 LavenderBlush
|
||||||
|
255 228 225 misty rose
|
||||||
|
255 228 225 MistyRose
|
||||||
|
255 255 255 white
|
||||||
|
0 0 0 black
|
||||||
|
47 79 79 dark slate gray
|
||||||
|
47 79 79 DarkSlateGray
|
||||||
|
47 79 79 dark slate grey
|
||||||
|
47 79 79 DarkSlateGrey
|
||||||
|
105 105 105 dim gray
|
||||||
|
105 105 105 DimGray
|
||||||
|
105 105 105 dim grey
|
||||||
|
105 105 105 DimGrey
|
||||||
|
112 128 144 slate gray
|
||||||
|
112 128 144 SlateGray
|
||||||
|
112 128 144 slate grey
|
||||||
|
112 128 144 SlateGrey
|
||||||
|
119 136 153 light slate gray
|
||||||
|
119 136 153 LightSlateGray
|
||||||
|
119 136 153 light slate grey
|
||||||
|
119 136 153 LightSlateGrey
|
||||||
|
190 190 190 gray
|
||||||
|
190 190 190 grey
|
||||||
|
190 190 190 x11 gray
|
||||||
|
190 190 190 X11Gray
|
||||||
|
190 190 190 x11 grey
|
||||||
|
190 190 190 X11Grey
|
||||||
|
128 128 128 web gray
|
||||||
|
128 128 128 WebGray
|
||||||
|
128 128 128 web grey
|
||||||
|
128 128 128 WebGrey
|
||||||
|
211 211 211 light grey
|
||||||
|
211 211 211 LightGrey
|
||||||
|
211 211 211 light gray
|
||||||
|
211 211 211 LightGray
|
||||||
|
25 25 112 midnight blue
|
||||||
|
25 25 112 MidnightBlue
|
||||||
|
0 0 128 navy
|
||||||
|
0 0 128 navy blue
|
||||||
|
0 0 128 NavyBlue
|
||||||
|
100 149 237 cornflower blue
|
||||||
|
100 149 237 CornflowerBlue
|
||||||
|
72 61 139 dark slate blue
|
||||||
|
72 61 139 DarkSlateBlue
|
||||||
|
106 90 205 slate blue
|
||||||
|
106 90 205 SlateBlue
|
||||||
|
123 104 238 medium slate blue
|
||||||
|
123 104 238 MediumSlateBlue
|
||||||
|
132 112 255 light slate blue
|
||||||
|
132 112 255 LightSlateBlue
|
||||||
|
0 0 205 medium blue
|
||||||
|
0 0 205 MediumBlue
|
||||||
|
65 105 225 royal blue
|
||||||
|
65 105 225 RoyalBlue
|
||||||
|
0 0 255 blue
|
||||||
|
30 144 255 dodger blue
|
||||||
|
30 144 255 DodgerBlue
|
||||||
|
0 191 255 deep sky blue
|
||||||
|
0 191 255 DeepSkyBlue
|
||||||
|
135 206 235 sky blue
|
||||||
|
135 206 235 SkyBlue
|
||||||
|
135 206 250 light sky blue
|
||||||
|
135 206 250 LightSkyBlue
|
||||||
|
70 130 180 steel blue
|
||||||
|
70 130 180 SteelBlue
|
||||||
|
176 196 222 light steel blue
|
||||||
|
176 196 222 LightSteelBlue
|
||||||
|
173 216 230 light blue
|
||||||
|
173 216 230 LightBlue
|
||||||
|
176 224 230 powder blue
|
||||||
|
176 224 230 PowderBlue
|
||||||
|
175 238 238 pale turquoise
|
||||||
|
175 238 238 PaleTurquoise
|
||||||
|
0 206 209 dark turquoise
|
||||||
|
0 206 209 DarkTurquoise
|
||||||
|
72 209 204 medium turquoise
|
||||||
|
72 209 204 MediumTurquoise
|
||||||
|
64 224 208 turquoise
|
||||||
|
0 255 255 cyan
|
||||||
|
0 255 255 aqua
|
||||||
|
224 255 255 light cyan
|
||||||
|
224 255 255 LightCyan
|
||||||
|
95 158 160 cadet blue
|
||||||
|
95 158 160 CadetBlue
|
||||||
|
102 205 170 medium aquamarine
|
||||||
|
102 205 170 MediumAquamarine
|
||||||
|
127 255 212 aquamarine
|
||||||
|
0 100 0 dark green
|
||||||
|
0 100 0 DarkGreen
|
||||||
|
85 107 47 dark olive green
|
||||||
|
85 107 47 DarkOliveGreen
|
||||||
|
143 188 143 dark sea green
|
||||||
|
143 188 143 DarkSeaGreen
|
||||||
|
46 139 87 sea green
|
||||||
|
46 139 87 SeaGreen
|
||||||
|
60 179 113 medium sea green
|
||||||
|
60 179 113 MediumSeaGreen
|
||||||
|
32 178 170 light sea green
|
||||||
|
32 178 170 LightSeaGreen
|
||||||
|
152 251 152 pale green
|
||||||
|
152 251 152 PaleGreen
|
||||||
|
0 255 127 spring green
|
||||||
|
0 255 127 SpringGreen
|
||||||
|
124 252 0 lawn green
|
||||||
|
124 252 0 LawnGreen
|
||||||
|
0 255 0 green
|
||||||
|
0 255 0 lime
|
||||||
|
0 255 0 x11 green
|
||||||
|
0 255 0 X11Green
|
||||||
|
0 128 0 web green
|
||||||
|
0 128 0 WebGreen
|
||||||
|
127 255 0 chartreuse
|
||||||
|
0 250 154 medium spring green
|
||||||
|
0 250 154 MediumSpringGreen
|
||||||
|
173 255 47 green yellow
|
||||||
|
173 255 47 GreenYellow
|
||||||
|
50 205 50 lime green
|
||||||
|
50 205 50 LimeGreen
|
||||||
|
154 205 50 yellow green
|
||||||
|
154 205 50 YellowGreen
|
||||||
|
34 139 34 forest green
|
||||||
|
34 139 34 ForestGreen
|
||||||
|
107 142 35 olive drab
|
||||||
|
107 142 35 OliveDrab
|
||||||
|
189 183 107 dark khaki
|
||||||
|
189 183 107 DarkKhaki
|
||||||
|
240 230 140 khaki
|
||||||
|
238 232 170 pale goldenrod
|
||||||
|
238 232 170 PaleGoldenrod
|
||||||
|
250 250 210 light goldenrod yellow
|
||||||
|
250 250 210 LightGoldenrodYellow
|
||||||
|
255 255 224 light yellow
|
||||||
|
255 255 224 LightYellow
|
||||||
|
255 255 0 yellow
|
||||||
|
255 215 0 gold
|
||||||
|
238 221 130 light goldenrod
|
||||||
|
238 221 130 LightGoldenrod
|
||||||
|
218 165 32 goldenrod
|
||||||
|
184 134 11 dark goldenrod
|
||||||
|
184 134 11 DarkGoldenrod
|
||||||
|
188 143 143 rosy brown
|
||||||
|
188 143 143 RosyBrown
|
||||||
|
205 92 92 indian red
|
||||||
|
205 92 92 IndianRed
|
||||||
|
139 69 19 saddle brown
|
||||||
|
139 69 19 SaddleBrown
|
||||||
|
160 82 45 sienna
|
||||||
|
205 133 63 peru
|
||||||
|
222 184 135 burlywood
|
||||||
|
245 245 220 beige
|
||||||
|
245 222 179 wheat
|
||||||
|
244 164 96 sandy brown
|
||||||
|
244 164 96 SandyBrown
|
||||||
|
210 180 140 tan
|
||||||
|
210 105 30 chocolate
|
||||||
|
178 34 34 firebrick
|
||||||
|
165 42 42 brown
|
||||||
|
233 150 122 dark salmon
|
||||||
|
233 150 122 DarkSalmon
|
||||||
|
250 128 114 salmon
|
||||||
|
255 160 122 light salmon
|
||||||
|
255 160 122 LightSalmon
|
||||||
|
255 165 0 orange
|
||||||
|
255 140 0 dark orange
|
||||||
|
255 140 0 DarkOrange
|
||||||
|
255 127 80 coral
|
||||||
|
240 128 128 light coral
|
||||||
|
240 128 128 LightCoral
|
||||||
|
255 99 71 tomato
|
||||||
|
255 69 0 orange red
|
||||||
|
255 69 0 OrangeRed
|
||||||
|
255 0 0 red
|
||||||
|
255 105 180 hot pink
|
||||||
|
255 105 180 HotPink
|
||||||
|
255 20 147 deep pink
|
||||||
|
255 20 147 DeepPink
|
||||||
|
255 192 203 pink
|
||||||
|
255 182 193 light pink
|
||||||
|
255 182 193 LightPink
|
||||||
|
219 112 147 pale violet red
|
||||||
|
219 112 147 PaleVioletRed
|
||||||
|
176 48 96 maroon
|
||||||
|
176 48 96 x11 maroon
|
||||||
|
176 48 96 X11Maroon
|
||||||
|
128 0 0 web maroon
|
||||||
|
128 0 0 WebMaroon
|
||||||
|
199 21 133 medium violet red
|
||||||
|
199 21 133 MediumVioletRed
|
||||||
|
208 32 144 violet red
|
||||||
|
208 32 144 VioletRed
|
||||||
|
255 0 255 magenta
|
||||||
|
255 0 255 fuchsia
|
||||||
|
238 130 238 violet
|
||||||
|
221 160 221 plum
|
||||||
|
218 112 214 orchid
|
||||||
|
186 85 211 medium orchid
|
||||||
|
186 85 211 MediumOrchid
|
||||||
|
153 50 204 dark orchid
|
||||||
|
153 50 204 DarkOrchid
|
||||||
|
148 0 211 dark violet
|
||||||
|
148 0 211 DarkViolet
|
||||||
|
138 43 226 blue violet
|
||||||
|
138 43 226 BlueViolet
|
||||||
|
160 32 240 purple
|
||||||
|
160 32 240 x11 purple
|
||||||
|
160 32 240 X11Purple
|
||||||
|
128 0 128 web purple
|
||||||
|
128 0 128 WebPurple
|
||||||
|
147 112 219 medium purple
|
||||||
|
147 112 219 MediumPurple
|
||||||
|
216 191 216 thistle
|
||||||
|
255 250 250 snow1
|
||||||
|
238 233 233 snow2
|
||||||
|
205 201 201 snow3
|
||||||
|
139 137 137 snow4
|
||||||
|
255 245 238 seashell1
|
||||||
|
238 229 222 seashell2
|
||||||
|
205 197 191 seashell3
|
||||||
|
139 134 130 seashell4
|
||||||
|
255 239 219 AntiqueWhite1
|
||||||
|
238 223 204 AntiqueWhite2
|
||||||
|
205 192 176 AntiqueWhite3
|
||||||
|
139 131 120 AntiqueWhite4
|
||||||
|
255 228 196 bisque1
|
||||||
|
238 213 183 bisque2
|
||||||
|
205 183 158 bisque3
|
||||||
|
139 125 107 bisque4
|
||||||
|
255 218 185 PeachPuff1
|
||||||
|
238 203 173 PeachPuff2
|
||||||
|
205 175 149 PeachPuff3
|
||||||
|
139 119 101 PeachPuff4
|
||||||
|
255 222 173 NavajoWhite1
|
||||||
|
238 207 161 NavajoWhite2
|
||||||
|
205 179 139 NavajoWhite3
|
||||||
|
139 121 94 NavajoWhite4
|
||||||
|
255 250 205 LemonChiffon1
|
||||||
|
238 233 191 LemonChiffon2
|
||||||
|
205 201 165 LemonChiffon3
|
||||||
|
139 137 112 LemonChiffon4
|
||||||
|
255 248 220 cornsilk1
|
||||||
|
238 232 205 cornsilk2
|
||||||
|
205 200 177 cornsilk3
|
||||||
|
139 136 120 cornsilk4
|
||||||
|
255 255 240 ivory1
|
||||||
|
238 238 224 ivory2
|
||||||
|
205 205 193 ivory3
|
||||||
|
139 139 131 ivory4
|
||||||
|
240 255 240 honeydew1
|
||||||
|
224 238 224 honeydew2
|
||||||
|
193 205 193 honeydew3
|
||||||
|
131 139 131 honeydew4
|
||||||
|
255 240 245 LavenderBlush1
|
||||||
|
238 224 229 LavenderBlush2
|
||||||
|
205 193 197 LavenderBlush3
|
||||||
|
139 131 134 LavenderBlush4
|
||||||
|
255 228 225 MistyRose1
|
||||||
|
238 213 210 MistyRose2
|
||||||
|
205 183 181 MistyRose3
|
||||||
|
139 125 123 MistyRose4
|
||||||
|
240 255 255 azure1
|
||||||
|
224 238 238 azure2
|
||||||
|
193 205 205 azure3
|
||||||
|
131 139 139 azure4
|
||||||
|
131 111 255 SlateBlue1
|
||||||
|
122 103 238 SlateBlue2
|
||||||
|
105 89 205 SlateBlue3
|
||||||
|
71 60 139 SlateBlue4
|
||||||
|
72 118 255 RoyalBlue1
|
||||||
|
67 110 238 RoyalBlue2
|
||||||
|
58 95 205 RoyalBlue3
|
||||||
|
39 64 139 RoyalBlue4
|
||||||
|
0 0 255 blue1
|
||||||
|
0 0 238 blue2
|
||||||
|
0 0 205 blue3
|
||||||
|
0 0 139 blue4
|
||||||
|
30 144 255 DodgerBlue1
|
||||||
|
28 134 238 DodgerBlue2
|
||||||
|
24 116 205 DodgerBlue3
|
||||||
|
16 78 139 DodgerBlue4
|
||||||
|
99 184 255 SteelBlue1
|
||||||
|
92 172 238 SteelBlue2
|
||||||
|
79 148 205 SteelBlue3
|
||||||
|
54 100 139 SteelBlue4
|
||||||
|
0 191 255 DeepSkyBlue1
|
||||||
|
0 178 238 DeepSkyBlue2
|
||||||
|
0 154 205 DeepSkyBlue3
|
||||||
|
0 104 139 DeepSkyBlue4
|
||||||
|
135 206 255 SkyBlue1
|
||||||
|
126 192 238 SkyBlue2
|
||||||
|
108 166 205 SkyBlue3
|
||||||
|
74 112 139 SkyBlue4
|
||||||
|
176 226 255 LightSkyBlue1
|
||||||
|
164 211 238 LightSkyBlue2
|
||||||
|
141 182 205 LightSkyBlue3
|
||||||
|
96 123 139 LightSkyBlue4
|
||||||
|
198 226 255 SlateGray1
|
||||||
|
185 211 238 SlateGray2
|
||||||
|
159 182 205 SlateGray3
|
||||||
|
108 123 139 SlateGray4
|
||||||
|
202 225 255 LightSteelBlue1
|
||||||
|
188 210 238 LightSteelBlue2
|
||||||
|
162 181 205 LightSteelBlue3
|
||||||
|
110 123 139 LightSteelBlue4
|
||||||
|
191 239 255 LightBlue1
|
||||||
|
178 223 238 LightBlue2
|
||||||
|
154 192 205 LightBlue3
|
||||||
|
104 131 139 LightBlue4
|
||||||
|
224 255 255 LightCyan1
|
||||||
|
209 238 238 LightCyan2
|
||||||
|
180 205 205 LightCyan3
|
||||||
|
122 139 139 LightCyan4
|
||||||
|
187 255 255 PaleTurquoise1
|
||||||
|
174 238 238 PaleTurquoise2
|
||||||
|
150 205 205 PaleTurquoise3
|
||||||
|
102 139 139 PaleTurquoise4
|
||||||
|
152 245 255 CadetBlue1
|
||||||
|
142 229 238 CadetBlue2
|
||||||
|
122 197 205 CadetBlue3
|
||||||
|
83 134 139 CadetBlue4
|
||||||
|
0 245 255 turquoise1
|
||||||
|
0 229 238 turquoise2
|
||||||
|
0 197 205 turquoise3
|
||||||
|
0 134 139 turquoise4
|
||||||
|
0 255 255 cyan1
|
||||||
|
0 238 238 cyan2
|
||||||
|
0 205 205 cyan3
|
||||||
|
0 139 139 cyan4
|
||||||
|
151 255 255 DarkSlateGray1
|
||||||
|
141 238 238 DarkSlateGray2
|
||||||
|
121 205 205 DarkSlateGray3
|
||||||
|
82 139 139 DarkSlateGray4
|
||||||
|
127 255 212 aquamarine1
|
||||||
|
118 238 198 aquamarine2
|
||||||
|
102 205 170 aquamarine3
|
||||||
|
69 139 116 aquamarine4
|
||||||
|
193 255 193 DarkSeaGreen1
|
||||||
|
180 238 180 DarkSeaGreen2
|
||||||
|
155 205 155 DarkSeaGreen3
|
||||||
|
105 139 105 DarkSeaGreen4
|
||||||
|
84 255 159 SeaGreen1
|
||||||
|
78 238 148 SeaGreen2
|
||||||
|
67 205 128 SeaGreen3
|
||||||
|
46 139 87 SeaGreen4
|
||||||
|
154 255 154 PaleGreen1
|
||||||
|
144 238 144 PaleGreen2
|
||||||
|
124 205 124 PaleGreen3
|
||||||
|
84 139 84 PaleGreen4
|
||||||
|
0 255 127 SpringGreen1
|
||||||
|
0 238 118 SpringGreen2
|
||||||
|
0 205 102 SpringGreen3
|
||||||
|
0 139 69 SpringGreen4
|
||||||
|
0 255 0 green1
|
||||||
|
0 238 0 green2
|
||||||
|
0 205 0 green3
|
||||||
|
0 139 0 green4
|
||||||
|
127 255 0 chartreuse1
|
||||||
|
118 238 0 chartreuse2
|
||||||
|
102 205 0 chartreuse3
|
||||||
|
69 139 0 chartreuse4
|
||||||
|
192 255 62 OliveDrab1
|
||||||
|
179 238 58 OliveDrab2
|
||||||
|
154 205 50 OliveDrab3
|
||||||
|
105 139 34 OliveDrab4
|
||||||
|
202 255 112 DarkOliveGreen1
|
||||||
|
188 238 104 DarkOliveGreen2
|
||||||
|
162 205 90 DarkOliveGreen3
|
||||||
|
110 139 61 DarkOliveGreen4
|
||||||
|
255 246 143 khaki1
|
||||||
|
238 230 133 khaki2
|
||||||
|
205 198 115 khaki3
|
||||||
|
139 134 78 khaki4
|
||||||
|
255 236 139 LightGoldenrod1
|
||||||
|
238 220 130 LightGoldenrod2
|
||||||
|
205 190 112 LightGoldenrod3
|
||||||
|
139 129 76 LightGoldenrod4
|
||||||
|
255 255 224 LightYellow1
|
||||||
|
238 238 209 LightYellow2
|
||||||
|
205 205 180 LightYellow3
|
||||||
|
139 139 122 LightYellow4
|
||||||
|
255 255 0 yellow1
|
||||||
|
238 238 0 yellow2
|
||||||
|
205 205 0 yellow3
|
||||||
|
139 139 0 yellow4
|
||||||
|
255 215 0 gold1
|
||||||
|
238 201 0 gold2
|
||||||
|
205 173 0 gold3
|
||||||
|
139 117 0 gold4
|
||||||
|
255 193 37 goldenrod1
|
||||||
|
238 180 34 goldenrod2
|
||||||
|
205 155 29 goldenrod3
|
||||||
|
139 105 20 goldenrod4
|
||||||
|
255 185 15 DarkGoldenrod1
|
||||||
|
238 173 14 DarkGoldenrod2
|
||||||
|
205 149 12 DarkGoldenrod3
|
||||||
|
139 101 8 DarkGoldenrod4
|
||||||
|
255 193 193 RosyBrown1
|
||||||
|
238 180 180 RosyBrown2
|
||||||
|
205 155 155 RosyBrown3
|
||||||
|
139 105 105 RosyBrown4
|
||||||
|
255 106 106 IndianRed1
|
||||||
|
238 99 99 IndianRed2
|
||||||
|
205 85 85 IndianRed3
|
||||||
|
139 58 58 IndianRed4
|
||||||
|
255 130 71 sienna1
|
||||||
|
238 121 66 sienna2
|
||||||
|
205 104 57 sienna3
|
||||||
|
139 71 38 sienna4
|
||||||
|
255 211 155 burlywood1
|
||||||
|
238 197 145 burlywood2
|
||||||
|
205 170 125 burlywood3
|
||||||
|
139 115 85 burlywood4
|
||||||
|
255 231 186 wheat1
|
||||||
|
238 216 174 wheat2
|
||||||
|
205 186 150 wheat3
|
||||||
|
139 126 102 wheat4
|
||||||
|
255 165 79 tan1
|
||||||
|
238 154 73 tan2
|
||||||
|
205 133 63 tan3
|
||||||
|
139 90 43 tan4
|
||||||
|
255 127 36 chocolate1
|
||||||
|
238 118 33 chocolate2
|
||||||
|
205 102 29 chocolate3
|
||||||
|
139 69 19 chocolate4
|
||||||
|
255 48 48 firebrick1
|
||||||
|
238 44 44 firebrick2
|
||||||
|
205 38 38 firebrick3
|
||||||
|
139 26 26 firebrick4
|
||||||
|
255 64 64 brown1
|
||||||
|
238 59 59 brown2
|
||||||
|
205 51 51 brown3
|
||||||
|
139 35 35 brown4
|
||||||
|
255 140 105 salmon1
|
||||||
|
238 130 98 salmon2
|
||||||
|
205 112 84 salmon3
|
||||||
|
139 76 57 salmon4
|
||||||
|
255 160 122 LightSalmon1
|
||||||
|
238 149 114 LightSalmon2
|
||||||
|
205 129 98 LightSalmon3
|
||||||
|
139 87 66 LightSalmon4
|
||||||
|
255 165 0 orange1
|
||||||
|
238 154 0 orange2
|
||||||
|
205 133 0 orange3
|
||||||
|
139 90 0 orange4
|
||||||
|
255 127 0 DarkOrange1
|
||||||
|
238 118 0 DarkOrange2
|
||||||
|
205 102 0 DarkOrange3
|
||||||
|
139 69 0 DarkOrange4
|
||||||
|
255 114 86 coral1
|
||||||
|
238 106 80 coral2
|
||||||
|
205 91 69 coral3
|
||||||
|
139 62 47 coral4
|
||||||
|
255 99 71 tomato1
|
||||||
|
238 92 66 tomato2
|
||||||
|
205 79 57 tomato3
|
||||||
|
139 54 38 tomato4
|
||||||
|
255 69 0 OrangeRed1
|
||||||
|
238 64 0 OrangeRed2
|
||||||
|
205 55 0 OrangeRed3
|
||||||
|
139 37 0 OrangeRed4
|
||||||
|
255 0 0 red1
|
||||||
|
238 0 0 red2
|
||||||
|
205 0 0 red3
|
||||||
|
139 0 0 red4
|
||||||
|
255 20 147 DeepPink1
|
||||||
|
238 18 137 DeepPink2
|
||||||
|
205 16 118 DeepPink3
|
||||||
|
139 10 80 DeepPink4
|
||||||
|
255 110 180 HotPink1
|
||||||
|
238 106 167 HotPink2
|
||||||
|
205 96 144 HotPink3
|
||||||
|
139 58 98 HotPink4
|
||||||
|
255 181 197 pink1
|
||||||
|
238 169 184 pink2
|
||||||
|
205 145 158 pink3
|
||||||
|
139 99 108 pink4
|
||||||
|
255 174 185 LightPink1
|
||||||
|
238 162 173 LightPink2
|
||||||
|
205 140 149 LightPink3
|
||||||
|
139 95 101 LightPink4
|
||||||
|
255 130 171 PaleVioletRed1
|
||||||
|
238 121 159 PaleVioletRed2
|
||||||
|
205 104 137 PaleVioletRed3
|
||||||
|
139 71 93 PaleVioletRed4
|
||||||
|
255 52 179 maroon1
|
||||||
|
238 48 167 maroon2
|
||||||
|
205 41 144 maroon3
|
||||||
|
139 28 98 maroon4
|
||||||
|
255 62 150 VioletRed1
|
||||||
|
238 58 140 VioletRed2
|
||||||
|
205 50 120 VioletRed3
|
||||||
|
139 34 82 VioletRed4
|
||||||
|
255 0 255 magenta1
|
||||||
|
238 0 238 magenta2
|
||||||
|
205 0 205 magenta3
|
||||||
|
139 0 139 magenta4
|
||||||
|
255 131 250 orchid1
|
||||||
|
238 122 233 orchid2
|
||||||
|
205 105 201 orchid3
|
||||||
|
139 71 137 orchid4
|
||||||
|
255 187 255 plum1
|
||||||
|
238 174 238 plum2
|
||||||
|
205 150 205 plum3
|
||||||
|
139 102 139 plum4
|
||||||
|
224 102 255 MediumOrchid1
|
||||||
|
209 95 238 MediumOrchid2
|
||||||
|
180 82 205 MediumOrchid3
|
||||||
|
122 55 139 MediumOrchid4
|
||||||
|
191 62 255 DarkOrchid1
|
||||||
|
178 58 238 DarkOrchid2
|
||||||
|
154 50 205 DarkOrchid3
|
||||||
|
104 34 139 DarkOrchid4
|
||||||
|
155 48 255 purple1
|
||||||
|
145 44 238 purple2
|
||||||
|
125 38 205 purple3
|
||||||
|
85 26 139 purple4
|
||||||
|
171 130 255 MediumPurple1
|
||||||
|
159 121 238 MediumPurple2
|
||||||
|
137 104 205 MediumPurple3
|
||||||
|
93 71 139 MediumPurple4
|
||||||
|
255 225 255 thistle1
|
||||||
|
238 210 238 thistle2
|
||||||
|
205 181 205 thistle3
|
||||||
|
139 123 139 thistle4
|
||||||
|
0 0 0 gray0
|
||||||
|
0 0 0 grey0
|
||||||
|
3 3 3 gray1
|
||||||
|
3 3 3 grey1
|
||||||
|
5 5 5 gray2
|
||||||
|
5 5 5 grey2
|
||||||
|
8 8 8 gray3
|
||||||
|
8 8 8 grey3
|
||||||
|
10 10 10 gray4
|
||||||
|
10 10 10 grey4
|
||||||
|
13 13 13 gray5
|
||||||
|
13 13 13 grey5
|
||||||
|
15 15 15 gray6
|
||||||
|
15 15 15 grey6
|
||||||
|
18 18 18 gray7
|
||||||
|
18 18 18 grey7
|
||||||
|
20 20 20 gray8
|
||||||
|
20 20 20 grey8
|
||||||
|
23 23 23 gray9
|
||||||
|
23 23 23 grey9
|
||||||
|
26 26 26 gray10
|
||||||
|
26 26 26 grey10
|
||||||
|
28 28 28 gray11
|
||||||
|
28 28 28 grey11
|
||||||
|
31 31 31 gray12
|
||||||
|
31 31 31 grey12
|
||||||
|
33 33 33 gray13
|
||||||
|
33 33 33 grey13
|
||||||
|
36 36 36 gray14
|
||||||
|
36 36 36 grey14
|
||||||
|
38 38 38 gray15
|
||||||
|
38 38 38 grey15
|
||||||
|
41 41 41 gray16
|
||||||
|
41 41 41 grey16
|
||||||
|
43 43 43 gray17
|
||||||
|
43 43 43 grey17
|
||||||
|
46 46 46 gray18
|
||||||
|
46 46 46 grey18
|
||||||
|
48 48 48 gray19
|
||||||
|
48 48 48 grey19
|
||||||
|
51 51 51 gray20
|
||||||
|
51 51 51 grey20
|
||||||
|
54 54 54 gray21
|
||||||
|
54 54 54 grey21
|
||||||
|
56 56 56 gray22
|
||||||
|
56 56 56 grey22
|
||||||
|
59 59 59 gray23
|
||||||
|
59 59 59 grey23
|
||||||
|
61 61 61 gray24
|
||||||
|
61 61 61 grey24
|
||||||
|
64 64 64 gray25
|
||||||
|
64 64 64 grey25
|
||||||
|
66 66 66 gray26
|
||||||
|
66 66 66 grey26
|
||||||
|
69 69 69 gray27
|
||||||
|
69 69 69 grey27
|
||||||
|
71 71 71 gray28
|
||||||
|
71 71 71 grey28
|
||||||
|
74 74 74 gray29
|
||||||
|
74 74 74 grey29
|
||||||
|
77 77 77 gray30
|
||||||
|
77 77 77 grey30
|
||||||
|
79 79 79 gray31
|
||||||
|
79 79 79 grey31
|
||||||
|
82 82 82 gray32
|
||||||
|
82 82 82 grey32
|
||||||
|
84 84 84 gray33
|
||||||
|
84 84 84 grey33
|
||||||
|
87 87 87 gray34
|
||||||
|
87 87 87 grey34
|
||||||
|
89 89 89 gray35
|
||||||
|
89 89 89 grey35
|
||||||
|
92 92 92 gray36
|
||||||
|
92 92 92 grey36
|
||||||
|
94 94 94 gray37
|
||||||
|
94 94 94 grey37
|
||||||
|
97 97 97 gray38
|
||||||
|
97 97 97 grey38
|
||||||
|
99 99 99 gray39
|
||||||
|
99 99 99 grey39
|
||||||
|
102 102 102 gray40
|
||||||
|
102 102 102 grey40
|
||||||
|
105 105 105 gray41
|
||||||
|
105 105 105 grey41
|
||||||
|
107 107 107 gray42
|
||||||
|
107 107 107 grey42
|
||||||
|
110 110 110 gray43
|
||||||
|
110 110 110 grey43
|
||||||
|
112 112 112 gray44
|
||||||
|
112 112 112 grey44
|
||||||
|
115 115 115 gray45
|
||||||
|
115 115 115 grey45
|
||||||
|
117 117 117 gray46
|
||||||
|
117 117 117 grey46
|
||||||
|
120 120 120 gray47
|
||||||
|
120 120 120 grey47
|
||||||
|
122 122 122 gray48
|
||||||
|
122 122 122 grey48
|
||||||
|
125 125 125 gray49
|
||||||
|
125 125 125 grey49
|
||||||
|
127 127 127 gray50
|
||||||
|
127 127 127 grey50
|
||||||
|
130 130 130 gray51
|
||||||
|
130 130 130 grey51
|
||||||
|
133 133 133 gray52
|
||||||
|
133 133 133 grey52
|
||||||
|
135 135 135 gray53
|
||||||
|
135 135 135 grey53
|
||||||
|
138 138 138 gray54
|
||||||
|
138 138 138 grey54
|
||||||
|
140 140 140 gray55
|
||||||
|
140 140 140 grey55
|
||||||
|
143 143 143 gray56
|
||||||
|
143 143 143 grey56
|
||||||
|
145 145 145 gray57
|
||||||
|
145 145 145 grey57
|
||||||
|
148 148 148 gray58
|
||||||
|
148 148 148 grey58
|
||||||
|
150 150 150 gray59
|
||||||
|
150 150 150 grey59
|
||||||
|
153 153 153 gray60
|
||||||
|
153 153 153 grey60
|
||||||
|
156 156 156 gray61
|
||||||
|
156 156 156 grey61
|
||||||
|
158 158 158 gray62
|
||||||
|
158 158 158 grey62
|
||||||
|
161 161 161 gray63
|
||||||
|
161 161 161 grey63
|
||||||
|
163 163 163 gray64
|
||||||
|
163 163 163 grey64
|
||||||
|
166 166 166 gray65
|
||||||
|
166 166 166 grey65
|
||||||
|
168 168 168 gray66
|
||||||
|
168 168 168 grey66
|
||||||
|
171 171 171 gray67
|
||||||
|
171 171 171 grey67
|
||||||
|
173 173 173 gray68
|
||||||
|
173 173 173 grey68
|
||||||
|
176 176 176 gray69
|
||||||
|
176 176 176 grey69
|
||||||
|
179 179 179 gray70
|
||||||
|
179 179 179 grey70
|
||||||
|
181 181 181 gray71
|
||||||
|
181 181 181 grey71
|
||||||
|
184 184 184 gray72
|
||||||
|
184 184 184 grey72
|
||||||
|
186 186 186 gray73
|
||||||
|
186 186 186 grey73
|
||||||
|
189 189 189 gray74
|
||||||
|
189 189 189 grey74
|
||||||
|
191 191 191 gray75
|
||||||
|
191 191 191 grey75
|
||||||
|
194 194 194 gray76
|
||||||
|
194 194 194 grey76
|
||||||
|
196 196 196 gray77
|
||||||
|
196 196 196 grey77
|
||||||
|
199 199 199 gray78
|
||||||
|
199 199 199 grey78
|
||||||
|
201 201 201 gray79
|
||||||
|
201 201 201 grey79
|
||||||
|
204 204 204 gray80
|
||||||
|
204 204 204 grey80
|
||||||
|
207 207 207 gray81
|
||||||
|
207 207 207 grey81
|
||||||
|
209 209 209 gray82
|
||||||
|
209 209 209 grey82
|
||||||
|
212 212 212 gray83
|
||||||
|
212 212 212 grey83
|
||||||
|
214 214 214 gray84
|
||||||
|
214 214 214 grey84
|
||||||
|
217 217 217 gray85
|
||||||
|
217 217 217 grey85
|
||||||
|
219 219 219 gray86
|
||||||
|
219 219 219 grey86
|
||||||
|
222 222 222 gray87
|
||||||
|
222 222 222 grey87
|
||||||
|
224 224 224 gray88
|
||||||
|
224 224 224 grey88
|
||||||
|
227 227 227 gray89
|
||||||
|
227 227 227 grey89
|
||||||
|
229 229 229 gray90
|
||||||
|
229 229 229 grey90
|
||||||
|
232 232 232 gray91
|
||||||
|
232 232 232 grey91
|
||||||
|
235 235 235 gray92
|
||||||
|
235 235 235 grey92
|
||||||
|
237 237 237 gray93
|
||||||
|
237 237 237 grey93
|
||||||
|
240 240 240 gray94
|
||||||
|
240 240 240 grey94
|
||||||
|
242 242 242 gray95
|
||||||
|
242 242 242 grey95
|
||||||
|
245 245 245 gray96
|
||||||
|
245 245 245 grey96
|
||||||
|
247 247 247 gray97
|
||||||
|
247 247 247 grey97
|
||||||
|
250 250 250 gray98
|
||||||
|
250 250 250 grey98
|
||||||
|
252 252 252 gray99
|
||||||
|
252 252 252 grey99
|
||||||
|
255 255 255 gray100
|
||||||
|
255 255 255 grey100
|
||||||
|
169 169 169 dark grey
|
||||||
|
169 169 169 DarkGrey
|
||||||
|
169 169 169 dark gray
|
||||||
|
169 169 169 DarkGray
|
||||||
|
0 0 139 dark blue
|
||||||
|
0 0 139 DarkBlue
|
||||||
|
0 139 139 dark cyan
|
||||||
|
0 139 139 DarkCyan
|
||||||
|
139 0 139 dark magenta
|
||||||
|
139 0 139 DarkMagenta
|
||||||
|
139 0 0 dark red
|
||||||
|
139 0 0 DarkRed
|
||||||
|
144 238 144 light green
|
||||||
|
144 238 144 LightGreen
|
||||||
|
220 20 60 crimson
|
||||||
|
75 0 130 indigo
|
||||||
|
128 128 0 olive
|
||||||
|
102 51 153 rebecca purple
|
||||||
|
102 51 153 RebeccaPurple
|
||||||
|
192 192 192 silver
|
||||||
|
0 128 128 teal
|
||||||
@@ -0,0 +1,740 @@
|
|||||||
|
//! SGR (Select Graphic Rendition) attribute parsing and types
|
||||||
|
//! (ZIG/src/terminal/sgr.zig).
|
||||||
|
//!
|
||||||
|
//! C-ABI helpers of the Zig version (`Attribute.C`, `cval`, `lib.Enum`
|
||||||
|
//! `Tag`) are intentionally not ported.
|
||||||
|
|
||||||
|
use crate::color::{Name, Rgb};
|
||||||
|
use crate::parser::{MAX_PARAMS, SepList};
|
||||||
|
|
||||||
|
/// Attribute type for SGR
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Attribute<'a> {
|
||||||
|
/// Unset all attributes
|
||||||
|
Unset,
|
||||||
|
|
||||||
|
/// Unknown attribute, the raw CSI command parameters are here.
|
||||||
|
Unknown(Unknown<'a>),
|
||||||
|
|
||||||
|
/// Bold the text.
|
||||||
|
Bold,
|
||||||
|
ResetBold,
|
||||||
|
|
||||||
|
/// Italic text.
|
||||||
|
Italic,
|
||||||
|
ResetItalic,
|
||||||
|
|
||||||
|
/// Faint/dim text.
|
||||||
|
/// Note: reset faint is the same SGR code as reset bold
|
||||||
|
Faint,
|
||||||
|
|
||||||
|
/// Underline the text
|
||||||
|
Underline(Underline),
|
||||||
|
UnderlineColor(Rgb),
|
||||||
|
/// `256_underline_color`
|
||||||
|
UnderlineColor256(u8),
|
||||||
|
ResetUnderlineColor,
|
||||||
|
|
||||||
|
// Overline the text
|
||||||
|
Overline,
|
||||||
|
ResetOverline,
|
||||||
|
|
||||||
|
/// Blink the text
|
||||||
|
Blink,
|
||||||
|
ResetBlink,
|
||||||
|
|
||||||
|
/// Invert fg/bg colors.
|
||||||
|
Inverse,
|
||||||
|
ResetInverse,
|
||||||
|
|
||||||
|
/// Invisible
|
||||||
|
Invisible,
|
||||||
|
ResetInvisible,
|
||||||
|
|
||||||
|
/// Strikethrough the text.
|
||||||
|
Strikethrough,
|
||||||
|
ResetStrikethrough,
|
||||||
|
|
||||||
|
/// Set foreground color as RGB values.
|
||||||
|
DirectColorFg(Rgb),
|
||||||
|
|
||||||
|
/// Set background color as RGB values.
|
||||||
|
DirectColorBg(Rgb),
|
||||||
|
|
||||||
|
/// Set the background/foreground as a named color attribute (`8_bg`).
|
||||||
|
Bg8(Name),
|
||||||
|
/// `8_fg`
|
||||||
|
Fg8(Name),
|
||||||
|
|
||||||
|
/// Reset the fg/bg to their default values.
|
||||||
|
ResetFg,
|
||||||
|
ResetBg,
|
||||||
|
|
||||||
|
/// Set the background/foreground as a named bright color attribute
|
||||||
|
/// (`8_bright_bg`).
|
||||||
|
Bg8Bright(Name),
|
||||||
|
/// `8_bright_fg`
|
||||||
|
Fg8Bright(Name),
|
||||||
|
|
||||||
|
/// Set background color as 256-color palette (`256_bg`).
|
||||||
|
Bg256(u8),
|
||||||
|
|
||||||
|
/// Set foreground color as 256-color palette (`256_fg`).
|
||||||
|
Fg256(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Unknown<'a> {
|
||||||
|
/// Full is the full SGR input.
|
||||||
|
pub full: &'a [u16],
|
||||||
|
|
||||||
|
/// Partial is the remaining, where we got hung up.
|
||||||
|
pub partial: &'a [u16],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum Underline {
|
||||||
|
None = 0,
|
||||||
|
Single = 1,
|
||||||
|
Double = 2,
|
||||||
|
Curly = 3,
|
||||||
|
Dotted = 4,
|
||||||
|
Dashed = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parser parses the attributes from a list of SGR parameters. Iterating
|
||||||
|
/// yields attributes until the parameters are exhausted.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct Parser<'a> {
|
||||||
|
pub params: &'a [u16],
|
||||||
|
pub params_sep: SepList,
|
||||||
|
pub idx: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Parser<'a> {
|
||||||
|
/// Empty state parser.
|
||||||
|
pub const EMPTY: Parser<'static> = Parser {
|
||||||
|
params: &[],
|
||||||
|
params_sep: SepList::init_empty(),
|
||||||
|
idx: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn new(params: &'a [u16], params_sep: SepList) -> Self {
|
||||||
|
Self {
|
||||||
|
params,
|
||||||
|
params_sep,
|
||||||
|
idx: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `SepList::is_set` but tolerant of indices past the end.
|
||||||
|
fn sep_is_set(&self, idx: usize) -> bool {
|
||||||
|
idx < MAX_PARAMS && self.params_sep.is_set(idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Next returns the next attribute or None if there are no more attributes.
|
||||||
|
fn next_attr(&mut self) -> Option<Attribute<'a>> {
|
||||||
|
if self.idx >= self.params.len() {
|
||||||
|
// Add one to ensure we don't loop on unset
|
||||||
|
let was_zero = self.idx == 0;
|
||||||
|
self.idx += 1;
|
||||||
|
|
||||||
|
// If we're at index zero it means we must have an empty list
|
||||||
|
// and an empty list implicitly means unset, otherwise we're
|
||||||
|
// done and return None.
|
||||||
|
return if was_zero {
|
||||||
|
Some(Attribute::Unset)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = self.params;
|
||||||
|
let slice = ¶ms[self.idx..];
|
||||||
|
let colon = self.sep_is_set(self.idx);
|
||||||
|
self.idx += 1;
|
||||||
|
|
||||||
|
// Our last one will have an idx be the last value.
|
||||||
|
if slice.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a colon separator then we need to ensure we're
|
||||||
|
// parsing a value that allows it.
|
||||||
|
if colon {
|
||||||
|
match slice[0] {
|
||||||
|
4 | 38 | 48 | 58 => {}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
// Consume all the colon separated
|
||||||
|
// values and return them as unknown.
|
||||||
|
let start = self.idx;
|
||||||
|
while self.sep_is_set(self.idx) {
|
||||||
|
self.idx += 1;
|
||||||
|
}
|
||||||
|
self.idx += 1;
|
||||||
|
return Some(Attribute::Unknown(Unknown {
|
||||||
|
full: params,
|
||||||
|
partial: &slice[..(self.idx - start + 1).min(slice.len())],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match slice[0] {
|
||||||
|
0 => return Some(Attribute::Unset),
|
||||||
|
1 => return Some(Attribute::Bold),
|
||||||
|
2 => return Some(Attribute::Faint),
|
||||||
|
3 => return Some(Attribute::Italic),
|
||||||
|
|
||||||
|
4 => {
|
||||||
|
if colon {
|
||||||
|
// A trailing colon with no following sub-param
|
||||||
|
// (e.g. "ESC[58:4:m") leaves the colon separator
|
||||||
|
// bit set on the last param without adding another
|
||||||
|
// entry, so we can see param 4 with a colon but
|
||||||
|
// nothing after it.
|
||||||
|
if slice.len() < 2 {
|
||||||
|
// fall through to unknown
|
||||||
|
} else if self.is_colon() {
|
||||||
|
self.consume_unknown_colon();
|
||||||
|
} else {
|
||||||
|
self.idx += 1;
|
||||||
|
return Some(Attribute::Underline(match slice[1] {
|
||||||
|
0 => Underline::None,
|
||||||
|
1 => Underline::Single,
|
||||||
|
2 => Underline::Double,
|
||||||
|
3 => Underline::Curly,
|
||||||
|
4 => Underline::Dotted,
|
||||||
|
5 => Underline::Dashed,
|
||||||
|
// For unknown underline styles,
|
||||||
|
// just render a single underline.
|
||||||
|
_ => Underline::Single,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Some(Attribute::Underline(Underline::Single));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
5 | 6 => return Some(Attribute::Blink),
|
||||||
|
7 => return Some(Attribute::Inverse),
|
||||||
|
8 => return Some(Attribute::Invisible),
|
||||||
|
9 => return Some(Attribute::Strikethrough),
|
||||||
|
21 => return Some(Attribute::Underline(Underline::Double)),
|
||||||
|
22 => return Some(Attribute::ResetBold),
|
||||||
|
23 => return Some(Attribute::ResetItalic),
|
||||||
|
24 => return Some(Attribute::Underline(Underline::None)),
|
||||||
|
25 => return Some(Attribute::ResetBlink),
|
||||||
|
27 => return Some(Attribute::ResetInverse),
|
||||||
|
28 => return Some(Attribute::ResetInvisible),
|
||||||
|
29 => return Some(Attribute::ResetStrikethrough),
|
||||||
|
|
||||||
|
30..=37 => return Some(Attribute::Fg8(Name((slice[0] - 30) as u8))),
|
||||||
|
|
||||||
|
38 => {
|
||||||
|
if slice.len() >= 2 {
|
||||||
|
match slice[1] {
|
||||||
|
// `2` indicates direct-color (r, g, b).
|
||||||
|
// We need at least 3 more params for this to make sense.
|
||||||
|
2 => {
|
||||||
|
if let Some(v) =
|
||||||
|
self.parse_direct_color(Attribute::DirectColorFg, slice, colon)
|
||||||
|
{
|
||||||
|
return Some(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `5` indicates indexed color.
|
||||||
|
5 if slice.len() >= 3 => {
|
||||||
|
self.idx += 2;
|
||||||
|
return Some(Attribute::Fg256(slice[2] as u8));
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
39 => return Some(Attribute::ResetFg),
|
||||||
|
|
||||||
|
40..=47 => return Some(Attribute::Bg8(Name((slice[0] - 40) as u8))),
|
||||||
|
|
||||||
|
48 => {
|
||||||
|
if slice.len() >= 2 {
|
||||||
|
match slice[1] {
|
||||||
|
2 => {
|
||||||
|
if let Some(v) =
|
||||||
|
self.parse_direct_color(Attribute::DirectColorBg, slice, colon)
|
||||||
|
{
|
||||||
|
return Some(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
5 if slice.len() >= 3 => {
|
||||||
|
self.idx += 2;
|
||||||
|
return Some(Attribute::Bg256(slice[2] as u8));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
49 => return Some(Attribute::ResetBg),
|
||||||
|
|
||||||
|
53 => return Some(Attribute::Overline),
|
||||||
|
55 => return Some(Attribute::ResetOverline),
|
||||||
|
|
||||||
|
58 => {
|
||||||
|
if slice.len() >= 2 {
|
||||||
|
match slice[1] {
|
||||||
|
2 => {
|
||||||
|
if let Some(v) =
|
||||||
|
self.parse_direct_color(Attribute::UnderlineColor, slice, colon)
|
||||||
|
{
|
||||||
|
return Some(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
5 if slice.len() >= 3 => {
|
||||||
|
self.idx += 2;
|
||||||
|
return Some(Attribute::UnderlineColor256(slice[2] as u8));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
59 => return Some(Attribute::ResetUnderlineColor),
|
||||||
|
|
||||||
|
// 82 instead of 90 to offset to "bright" colors
|
||||||
|
90..=97 => return Some(Attribute::Fg8Bright(Name((slice[0] - 82) as u8))),
|
||||||
|
|
||||||
|
100..=107 => return Some(Attribute::Bg8Bright(Name((slice[0] - 92) as u8))),
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Attribute::Unknown(Unknown {
|
||||||
|
full: params,
|
||||||
|
partial: slice,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_direct_color(
|
||||||
|
&mut self,
|
||||||
|
make: fn(Rgb) -> Attribute<'a>,
|
||||||
|
slice: &[u16],
|
||||||
|
colon: bool,
|
||||||
|
) -> Option<Attribute<'a>> {
|
||||||
|
// Any direct color style must have at least 5 values.
|
||||||
|
if slice.len() < 5 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only used for direct color sets (38, 48, 58) and subparam 2.
|
||||||
|
debug_assert_eq!(slice[1], 2);
|
||||||
|
|
||||||
|
// Note: We truncate because the value should be 0 to 255. If
|
||||||
|
// it isn't, the behavior is undefined so we just... truncate it.
|
||||||
|
|
||||||
|
// If we don't have a colon, then we expect exactly 3 semicolon
|
||||||
|
// separated values.
|
||||||
|
if !colon {
|
||||||
|
self.idx += 4;
|
||||||
|
return Some(make(Rgb {
|
||||||
|
r: slice[2] as u8,
|
||||||
|
g: slice[3] as u8,
|
||||||
|
b: slice[4] as u8,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have a colon, we might have either 5 or 6 values depending
|
||||||
|
// on if the colorspace is present.
|
||||||
|
match self.count_colon() {
|
||||||
|
3 => {
|
||||||
|
self.idx += 4;
|
||||||
|
Some(make(Rgb {
|
||||||
|
r: slice[2] as u8,
|
||||||
|
g: slice[3] as u8,
|
||||||
|
b: slice[4] as u8,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
4 => {
|
||||||
|
self.idx += 5;
|
||||||
|
Some(make(Rgb {
|
||||||
|
r: slice[3] as u8,
|
||||||
|
g: slice[4] as u8,
|
||||||
|
b: slice[5] as u8,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
self.consume_unknown_colon();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the present position has a colon separator.
|
||||||
|
/// This always returns false for the last value since it has no
|
||||||
|
/// separator.
|
||||||
|
fn is_colon(&self) -> bool {
|
||||||
|
self.sep_is_set(self.idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_colon(&self) -> usize {
|
||||||
|
let mut count = 0;
|
||||||
|
let mut idx = self.idx;
|
||||||
|
while idx < self.params.len().saturating_sub(1) && self.sep_is_set(idx) {
|
||||||
|
count += 1;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
count
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consumes all the remaining parameters separated by a colon and
|
||||||
|
/// returns an unknown attribute.
|
||||||
|
fn consume_unknown_colon(&mut self) {
|
||||||
|
let count = self.count_colon();
|
||||||
|
self.idx += count + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for Parser<'a> {
|
||||||
|
type Item = Attribute<'a>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Attribute<'a>> {
|
||||||
|
self.next_attr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use Attribute as A;
|
||||||
|
|
||||||
|
fn rgb(r: u8, g: u8, b: u8) -> Rgb {
|
||||||
|
Rgb { r, g, b }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_parse(params: &[u16]) -> Attribute<'_> {
|
||||||
|
let mut p = Parser::new(params, SepList::init_empty());
|
||||||
|
p.next().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_parse_colon(params: &[u16]) -> Attribute<'_> {
|
||||||
|
let mut p = Parser::new(params, SepList::init_empty());
|
||||||
|
// Mark all parameters except the last as having a colon after.
|
||||||
|
for i in 0..params.len() - 1 {
|
||||||
|
p.params_sep.set(i);
|
||||||
|
}
|
||||||
|
p.next().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sep(idxs: &[usize]) -> SepList {
|
||||||
|
let mut l = SepList::init_empty();
|
||||||
|
for &i in idxs {
|
||||||
|
l.set(i);
|
||||||
|
}
|
||||||
|
l
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_unknown(a: Option<Attribute<'_>>) -> bool {
|
||||||
|
matches!(a, Some(A::Unknown(_)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_parser() {
|
||||||
|
assert_eq!(test_parse(&[]), A::Unset);
|
||||||
|
assert_eq!(test_parse(&[0]), A::Unset);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
test_parse(&[38, 2, 40, 44, 52]),
|
||||||
|
A::DirectColorFg(rgb(40, 44, 52))
|
||||||
|
);
|
||||||
|
assert!(is_unknown(Some(test_parse(&[38, 2, 44, 52]))));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
test_parse(&[48, 2, 40, 44, 52]),
|
||||||
|
A::DirectColorBg(rgb(40, 44, 52))
|
||||||
|
);
|
||||||
|
assert!(is_unknown(Some(test_parse(&[48, 2, 44, 52]))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_parser_multiple() {
|
||||||
|
let mut p = Parser::new(&[0, 38, 2, 40, 44, 52], SepList::init_empty());
|
||||||
|
assert_eq!(p.next(), Some(A::Unset));
|
||||||
|
assert!(matches!(p.next(), Some(A::DirectColorFg(_))));
|
||||||
|
assert_eq!(p.next(), None);
|
||||||
|
assert_eq!(p.next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_unsupported_with_colon() {
|
||||||
|
let mut p = Parser::new(&[0, 4, 1], sep(&[0]));
|
||||||
|
assert!(is_unknown(p.next()));
|
||||||
|
assert_eq!(p.next(), Some(A::Bold));
|
||||||
|
assert_eq!(p.next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_unsupported_with_multiple_colon() {
|
||||||
|
let mut p = Parser::new(&[0, 4, 2, 1], sep(&[0, 1]));
|
||||||
|
assert!(is_unknown(p.next()));
|
||||||
|
assert_eq!(p.next(), Some(A::Bold));
|
||||||
|
assert_eq!(p.next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_bold() {
|
||||||
|
assert_eq!(test_parse(&[1]), A::Bold);
|
||||||
|
assert_eq!(test_parse(&[22]), A::ResetBold);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_italic() {
|
||||||
|
assert_eq!(test_parse(&[3]), A::Italic);
|
||||||
|
assert_eq!(test_parse(&[23]), A::ResetItalic);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_underline() {
|
||||||
|
assert_eq!(test_parse(&[4]), A::Underline(Underline::Single));
|
||||||
|
assert_eq!(test_parse(&[24]), A::Underline(Underline::None));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_underline_styles() {
|
||||||
|
assert_eq!(test_parse_colon(&[4, 2]), A::Underline(Underline::Double));
|
||||||
|
assert_eq!(test_parse_colon(&[4, 0]), A::Underline(Underline::None));
|
||||||
|
assert_eq!(test_parse_colon(&[4, 1]), A::Underline(Underline::Single));
|
||||||
|
assert_eq!(test_parse_colon(&[4, 3]), A::Underline(Underline::Curly));
|
||||||
|
assert_eq!(test_parse_colon(&[4, 4]), A::Underline(Underline::Dotted));
|
||||||
|
assert_eq!(test_parse_colon(&[4, 5]), A::Underline(Underline::Dashed));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_underline_style_with_more() {
|
||||||
|
let mut p = Parser::new(&[4, 2, 1], sep(&[0]));
|
||||||
|
assert!(matches!(p.next(), Some(A::Underline(_))));
|
||||||
|
assert_eq!(p.next(), Some(A::Bold));
|
||||||
|
assert_eq!(p.next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_underline_style_with_too_many_colons() {
|
||||||
|
let mut p = Parser::new(&[4, 2, 3, 1], sep(&[0, 1]));
|
||||||
|
assert!(is_unknown(p.next()));
|
||||||
|
assert_eq!(p.next(), Some(A::Bold));
|
||||||
|
assert_eq!(p.next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_blink() {
|
||||||
|
assert_eq!(test_parse(&[5]), A::Blink);
|
||||||
|
assert_eq!(test_parse(&[6]), A::Blink);
|
||||||
|
assert_eq!(test_parse(&[25]), A::ResetBlink);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_inverse() {
|
||||||
|
assert_eq!(test_parse(&[7]), A::Inverse);
|
||||||
|
assert_eq!(test_parse(&[27]), A::ResetInverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_strikethrough() {
|
||||||
|
assert_eq!(test_parse(&[9]), A::Strikethrough);
|
||||||
|
assert_eq!(test_parse(&[29]), A::ResetStrikethrough);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_8_color() {
|
||||||
|
let mut p = Parser::new(&[31, 43, 90, 103], SepList::init_empty());
|
||||||
|
assert_eq!(p.next(), Some(A::Fg8(Name::RED)));
|
||||||
|
assert_eq!(p.next(), Some(A::Bg8(Name::YELLOW)));
|
||||||
|
assert_eq!(p.next(), Some(A::Fg8Bright(Name::BRIGHT_BLACK)));
|
||||||
|
assert_eq!(p.next(), Some(A::Bg8Bright(Name::BRIGHT_YELLOW)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_256_color() {
|
||||||
|
let mut p = Parser::new(&[38, 5, 161, 48, 5, 236], SepList::init_empty());
|
||||||
|
assert_eq!(p.next(), Some(A::Fg256(161)));
|
||||||
|
assert_eq!(p.next(), Some(A::Bg256(236)));
|
||||||
|
assert_eq!(p.next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_256_color_underline() {
|
||||||
|
let mut p = Parser::new(&[58, 5, 9], SepList::init_empty());
|
||||||
|
assert_eq!(p.next(), Some(A::UnderlineColor256(9)));
|
||||||
|
assert_eq!(p.next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_24_bit_bg_color() {
|
||||||
|
assert_eq!(
|
||||||
|
test_parse_colon(&[48, 2, 1, 2, 3]),
|
||||||
|
A::DirectColorBg(rgb(1, 2, 3))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_underline_color() {
|
||||||
|
assert_eq!(
|
||||||
|
test_parse_colon(&[58, 2, 1, 2, 3]),
|
||||||
|
A::UnderlineColor(rgb(1, 2, 3))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
test_parse_colon(&[58, 2, 0, 1, 2, 3]),
|
||||||
|
A::UnderlineColor(rgb(1, 2, 3))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_reset_underline_color() {
|
||||||
|
let mut p = Parser::new(&[59], SepList::init_empty());
|
||||||
|
assert_eq!(p.next(), Some(A::ResetUnderlineColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_invisible() {
|
||||||
|
let mut p = Parser::new(&[8, 28], SepList::init_empty());
|
||||||
|
assert_eq!(p.next(), Some(A::Invisible));
|
||||||
|
assert_eq!(p.next(), Some(A::ResetInvisible));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_underline_bg_and_fg() {
|
||||||
|
let mut p = Parser::new(
|
||||||
|
&[4, 38, 2, 255, 247, 219, 48, 2, 242, 93, 147, 4],
|
||||||
|
SepList::init_empty(),
|
||||||
|
);
|
||||||
|
assert_eq!(p.next(), Some(A::Underline(Underline::Single)));
|
||||||
|
assert_eq!(p.next(), Some(A::DirectColorFg(rgb(255, 247, 219))));
|
||||||
|
assert_eq!(p.next(), Some(A::DirectColorBg(rgb(242, 93, 147))));
|
||||||
|
assert_eq!(p.next(), Some(A::Underline(Underline::Single)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_direct_color_fg_missing_color() {
|
||||||
|
// This used to crash
|
||||||
|
let p = Parser::new(&[38, 5], SepList::init_empty());
|
||||||
|
for _ in p {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_direct_color_bg_missing_color() {
|
||||||
|
// This used to crash
|
||||||
|
let p = Parser::new(&[48, 5], SepList::init_empty());
|
||||||
|
for _ in p {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_direct_fg_bg_underline_ignore_optional_color_space() {
|
||||||
|
// These behaviors have been verified against xterm.
|
||||||
|
|
||||||
|
// Colon version should skip the optional color space identifier
|
||||||
|
assert_eq!(
|
||||||
|
test_parse_colon(&[38, 2, 0, 1, 2, 3]),
|
||||||
|
A::DirectColorFg(rgb(1, 2, 3))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
test_parse_colon(&[48, 2, 0, 1, 2, 3]),
|
||||||
|
A::DirectColorBg(rgb(1, 2, 3))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
test_parse_colon(&[58, 2, 0, 1, 2, 3]),
|
||||||
|
A::UnderlineColor(rgb(1, 2, 3))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Semicolon version should not parse optional color space identifier
|
||||||
|
assert_eq!(
|
||||||
|
test_parse(&[38, 2, 0, 1, 2, 3]),
|
||||||
|
A::DirectColorFg(rgb(0, 1, 2))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
test_parse(&[48, 2, 0, 1, 2, 3]),
|
||||||
|
A::DirectColorBg(rgb(0, 1, 2))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
test_parse(&[58, 2, 0, 1, 2, 3]),
|
||||||
|
A::UnderlineColor(rgb(0, 1, 2))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_direct_fg_colon_with_too_many_colons() {
|
||||||
|
let mut p = Parser::new(&[38, 2, 0, 1, 2, 3, 4, 1], sep(&[0, 1, 2, 3, 4, 5]));
|
||||||
|
assert!(is_unknown(p.next()));
|
||||||
|
assert_eq!(p.next(), Some(A::Bold));
|
||||||
|
assert_eq!(p.next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_direct_fg_colon_with_colorspace_and_extra_param() {
|
||||||
|
let mut p = Parser::new(&[38, 2, 0, 1, 2, 3, 1], sep(&[0, 1, 2, 3, 4]));
|
||||||
|
assert_eq!(p.next(), Some(A::DirectColorFg(rgb(1, 2, 3))));
|
||||||
|
assert_eq!(p.next(), Some(A::Bold));
|
||||||
|
assert_eq!(p.next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sgr_direct_fg_colon_no_colorspace_and_extra_param() {
|
||||||
|
let mut p = Parser::new(&[38, 2, 1, 2, 3, 1], sep(&[0, 1, 2, 3]));
|
||||||
|
assert_eq!(p.next(), Some(A::DirectColorFg(rgb(1, 2, 3))));
|
||||||
|
assert_eq!(p.next(), Some(A::Bold));
|
||||||
|
assert_eq!(p.next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kakoune sent this complex SGR sequence that caused invalid behavior.
|
||||||
|
#[test]
|
||||||
|
fn sgr_kakoune_input() {
|
||||||
|
// This used to crash
|
||||||
|
let mut p = Parser::new(
|
||||||
|
&[0, 4, 3, 38, 2, 175, 175, 215, 58, 2, 0, 190, 80, 70],
|
||||||
|
sep(&[1, 8, 9, 10, 11, 12]),
|
||||||
|
);
|
||||||
|
assert_eq!(p.next(), Some(A::Unset));
|
||||||
|
assert_eq!(p.next(), Some(A::Underline(Underline::Curly)));
|
||||||
|
assert_eq!(p.next(), Some(A::DirectColorFg(rgb(175, 175, 215))));
|
||||||
|
assert_eq!(p.next(), Some(A::UnderlineColor(rgb(190, 80, 70))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discussion #5930, another input sent by kakoune
|
||||||
|
#[test]
|
||||||
|
fn sgr_kakoune_input_issue_underline_fg_and_bg() {
|
||||||
|
// echo -e "\033[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136mset everything in one sequence, broken\033[m"
|
||||||
|
let mut p = Parser::new(
|
||||||
|
&[
|
||||||
|
4, 3, 38, 2, 51, 51, 51, 48, 2, 170, 170, 170, 58, 2, 255, 97, 136,
|
||||||
|
],
|
||||||
|
sep(&[0]),
|
||||||
|
);
|
||||||
|
assert_eq!(p.next(), Some(A::Underline(Underline::Curly)));
|
||||||
|
assert_eq!(p.next(), Some(A::DirectColorFg(rgb(51, 51, 51))));
|
||||||
|
assert_eq!(p.next(), Some(A::DirectColorBg(rgb(170, 170, 170))));
|
||||||
|
assert_eq!(p.next(), Some(A::UnderlineColor(rgb(255, 97, 136))));
|
||||||
|
assert_eq!(p.next(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fuzz crash: afl-out/stream/default/crashes/id:000021
|
||||||
|
// Input "ESC [ 5 8 : 4 : m" produces params [58, 4] with colon
|
||||||
|
// separator bits set at indices 0 and 1.
|
||||||
|
#[test]
|
||||||
|
fn sgr_underline_colon_with_trailing_separator_and_short_slice() {
|
||||||
|
let mut p = Parser::new(&[58, 4], sep(&[0, 1]));
|
||||||
|
// 58:4 is not a valid underline color (sub-param 4 is not 2 or 5),
|
||||||
|
// so it falls through as unknown.
|
||||||
|
assert!(is_unknown(p.next()));
|
||||||
|
// Param 4 with a trailing colon but no sub-param is malformed,
|
||||||
|
// so it also falls through as unknown rather than panicking.
|
||||||
|
assert!(is_unknown(p.next()));
|
||||||
|
assert_eq!(p.next(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,679 @@
|
|||||||
|
//! This file contains the implementation for tmux control mode
|
||||||
|
//! (ZIG/src/terminal/tmux/control.zig). See tmux(1) for more information on
|
||||||
|
//! control mode. Some basics are documented here but this is not meant to be
|
||||||
|
//! a comprehensive source of protocol documentation.
|
||||||
|
//!
|
||||||
|
//! Deviations from the Zig version:
|
||||||
|
//! - Notifications own their data (`Vec<u8>`) instead of borrowing from the
|
||||||
|
//! parser's buffer, which avoids self-referential lifetimes.
|
||||||
|
//! - Oniguruma regexes are replaced by hand-written matchers that implement
|
||||||
|
//! the same (greedy, backtracking) semantics for the specific patterns.
|
||||||
|
//! - A numeric ID that overflows `usize` is treated as a failed match (Zig
|
||||||
|
//! would hit `unreachable`).
|
||||||
|
|
||||||
|
/// Errors returned by [`Parser::put`].
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("out of memory")]
|
||||||
|
OutOfMemory,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum State {
|
||||||
|
/// Outside of any active notifications. This should drop any output
|
||||||
|
/// unless it is '%' on the first byte of a line. The buffer will be
|
||||||
|
/// cleared when it sees '%', this is so that the previous notification
|
||||||
|
/// data is valid until we receive/process new data.
|
||||||
|
Idle,
|
||||||
|
|
||||||
|
/// We experienced unexpected input and are in a broken state
|
||||||
|
/// so we cannot continue processing. When this state is set,
|
||||||
|
/// the buffer has been freed and must not be accessed.
|
||||||
|
Broken,
|
||||||
|
|
||||||
|
/// Inside an active notification (started with '%').
|
||||||
|
Notification,
|
||||||
|
|
||||||
|
/// Inside a begin/end block.
|
||||||
|
Block,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A tmux control mode parser. This takes in output from tmux control
|
||||||
|
/// mode and parses it into a structured notifications.
|
||||||
|
///
|
||||||
|
/// It is up to the caller to establish the connection to the tmux
|
||||||
|
/// control mode session in some way (e.g. via exec, a network socket,
|
||||||
|
/// whatever). This is fully agnostic to how the data is received and sent.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Parser {
|
||||||
|
/// Current state of the client.
|
||||||
|
state: State,
|
||||||
|
|
||||||
|
/// The buffer used to store in-progress notifications, output, etc.
|
||||||
|
buffer: Vec<u8>,
|
||||||
|
|
||||||
|
/// The maximum size in bytes of the buffer. This is used to limit
|
||||||
|
/// memory usage. If the buffer exceeds this size, the client will
|
||||||
|
/// enter a broken state (the control mode session will be forcibly
|
||||||
|
/// exited and future data dropped).
|
||||||
|
pub max_bytes: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Parser {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parser {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
state: State::Idle,
|
||||||
|
buffer: Vec::new(),
|
||||||
|
max_bytes: 1024 * 1024,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle a byte of input.
|
||||||
|
///
|
||||||
|
/// If we reach our byte limit this will return `OutOfMemory`. It only
|
||||||
|
/// does this on the first time we exceed the limit; subsequent calls
|
||||||
|
/// will return None as we drop all input in a broken state.
|
||||||
|
pub fn put(&mut self, byte: u8) -> Result<Option<Notification>, Error> {
|
||||||
|
// If we're in a broken state, just do nothing.
|
||||||
|
if self.state == State::Broken {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.buffer.len() >= self.max_bytes {
|
||||||
|
self.broken();
|
||||||
|
return Err(Error::OutOfMemory);
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.state {
|
||||||
|
State::Broken => return Ok(None),
|
||||||
|
|
||||||
|
// Waiting for a notification so if the byte is not '%' then
|
||||||
|
// we're in a broken state. Control mode output should always
|
||||||
|
// be wrapped in '%begin/%end' orelse we expect a notification.
|
||||||
|
// Return an exit notification.
|
||||||
|
State::Idle => {
|
||||||
|
if byte == b'%' {
|
||||||
|
self.buffer.clear();
|
||||||
|
self.state = State::Notification;
|
||||||
|
} else {
|
||||||
|
self.broken();
|
||||||
|
return Ok(Some(Notification::Exit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're in a notification and its not a newline then
|
||||||
|
// we accumulate. If it is a newline then we have a
|
||||||
|
// complete notification we need to parse.
|
||||||
|
State::Notification => {
|
||||||
|
if byte == b'\n' {
|
||||||
|
// If parsing failed we do not mark the state as broken
|
||||||
|
// because we may be able to continue parsing other types
|
||||||
|
// of notifications.
|
||||||
|
return Ok(self.parse_notification());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're in a block then we accumulate until we see a newline
|
||||||
|
// and then we check to see if that line ended the block.
|
||||||
|
State::Block => {
|
||||||
|
if byte == b'\n' {
|
||||||
|
let idx = self
|
||||||
|
.buffer
|
||||||
|
.iter()
|
||||||
|
.rposition(|&b| b == b'\n')
|
||||||
|
.map_or(0, |v| v + 1);
|
||||||
|
let line = &self.buffer[idx..];
|
||||||
|
|
||||||
|
if line.starts_with(b"%end") || line.starts_with(b"%error") {
|
||||||
|
let err = line.starts_with(b"%error");
|
||||||
|
let output = trim_right_crlf(&self.buffer[..idx]).to_vec();
|
||||||
|
|
||||||
|
// If it is an error then log it.
|
||||||
|
if err {
|
||||||
|
log::warn!(
|
||||||
|
"tmux control mode error={}",
|
||||||
|
String::from_utf8_lossy(&output)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = State::Idle;
|
||||||
|
return Ok(Some(if err {
|
||||||
|
Notification::BlockErr(output)
|
||||||
|
} else {
|
||||||
|
Notification::BlockEnd(output)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Didn't end the block, continue accumulating.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.buffer.push(byte);
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_notification(&mut self) -> Option<Notification> {
|
||||||
|
debug_assert_eq!(self.state, State::Notification);
|
||||||
|
|
||||||
|
let mut line: &[u8] = &self.buffer;
|
||||||
|
if line.last() == Some(&b'\r') {
|
||||||
|
line = &line[..line.len() - 1];
|
||||||
|
}
|
||||||
|
let cmd_end = line.iter().position(|&b| b == b' ').unwrap_or(line.len());
|
||||||
|
let cmd = &line[..cmd_end];
|
||||||
|
|
||||||
|
// The notification MUST exist because we guard entering the notification
|
||||||
|
// state on seeing at least a '%'.
|
||||||
|
let result: Option<Notification> = match cmd {
|
||||||
|
b"%begin" => {
|
||||||
|
// We don't use the rest of the tokens for now because tmux
|
||||||
|
// claims to guarantee that begin/end are always in order and
|
||||||
|
// never intermixed.
|
||||||
|
|
||||||
|
// Move to block state because we expect a corresponding end/error
|
||||||
|
// and want to accumulate the data.
|
||||||
|
self.state = State::Block;
|
||||||
|
self.buffer.clear();
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
b"%output" => match_output(line),
|
||||||
|
b"%session-changed" => match_session_changed(line),
|
||||||
|
b"%sessions-changed" => {
|
||||||
|
if line == b"%sessions-changed" {
|
||||||
|
Some(Notification::SessionsChanged)
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"failed to match notification cmd=%sessions-changed line=\"{}\"",
|
||||||
|
String::from_utf8_lossy(line)
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"%layout-change" => match_layout_change(line),
|
||||||
|
b"%window-add" => match_window_add(line),
|
||||||
|
b"%window-renamed" => match_window_renamed(line),
|
||||||
|
b"%window-pane-changed" => match_window_pane_changed(line),
|
||||||
|
b"%client-detached" => match_client_detached(line),
|
||||||
|
b"%client-session-changed" => match_client_session_changed(line),
|
||||||
|
_ => {
|
||||||
|
// Unknown notification, log it and return to idle state.
|
||||||
|
log::warn!(
|
||||||
|
"unknown tmux control mode notification={}",
|
||||||
|
String::from_utf8_lossy(cmd)
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if result.is_none() && cmd_is_known(cmd) && cmd != b"%sessions-changed" {
|
||||||
|
log::warn!(
|
||||||
|
"failed to match notification cmd={} line=\"{}\"",
|
||||||
|
String::from_utf8_lossy(cmd),
|
||||||
|
String::from_utf8_lossy(line)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether matched or not, clear the buffer and return to idle. (The
|
||||||
|
// Zig code avoids clearing when slices point into the buffer, but we
|
||||||
|
// own our data so clearing is always safe.)
|
||||||
|
self.buffer.clear();
|
||||||
|
self.state = State::Idle;
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark the tmux state as broken.
|
||||||
|
fn broken(&mut self) {
|
||||||
|
self.state = State::Broken;
|
||||||
|
self.buffer = Vec::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmd_is_known(cmd: &[u8]) -> bool {
|
||||||
|
matches!(
|
||||||
|
cmd,
|
||||||
|
b"%output"
|
||||||
|
| b"%session-changed"
|
||||||
|
| b"%sessions-changed"
|
||||||
|
| b"%layout-change"
|
||||||
|
| b"%window-add"
|
||||||
|
| b"%window-renamed"
|
||||||
|
| b"%window-pane-changed"
|
||||||
|
| b"%client-detached"
|
||||||
|
| b"%client-session-changed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trim_right_crlf(mut s: &[u8]) -> &[u8] {
|
||||||
|
while let Some((&last, rest)) = s.split_last() {
|
||||||
|
if last == b'\r' || last == b'\n' {
|
||||||
|
s = rest;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Hand-written matchers equivalent to the Zig regexes. `.` does not match
|
||||||
|
// '\n' in Oniguruma, but lines never contain '\n' here.
|
||||||
|
|
||||||
|
/// Parse `[0-9]+` at the start of `s`; returns (value, rest).
|
||||||
|
fn digits(s: &[u8]) -> Option<(usize, &[u8])> {
|
||||||
|
let n = s.iter().take_while(|b| b.is_ascii_digit()).count();
|
||||||
|
if n == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let v = std::str::from_utf8(&s[..n]).ok()?.parse::<usize>().ok()?;
|
||||||
|
Some((v, &s[n..]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Strip an exact literal prefix.
|
||||||
|
fn lit<'a>(s: &'a [u8], p: &[u8]) -> Option<&'a [u8]> {
|
||||||
|
s.strip_prefix(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `(.+)` followed by end: non-empty remainder.
|
||||||
|
fn nonempty(s: &[u8]) -> Option<&[u8]> {
|
||||||
|
if s.is_empty() { None } else { Some(s) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_output(line: &[u8]) -> Option<Notification> {
|
||||||
|
// ^%output %([0-9]+) (.+)$
|
||||||
|
let s = lit(line, b"%output %")?;
|
||||||
|
let (id, s) = digits(s)?;
|
||||||
|
let s = lit(s, b" ")?;
|
||||||
|
let data = nonempty(s)?;
|
||||||
|
Some(Notification::Output {
|
||||||
|
pane_id: id,
|
||||||
|
data: data.to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_session_changed(line: &[u8]) -> Option<Notification> {
|
||||||
|
// ^%session-changed \$([0-9]+) (.+)$
|
||||||
|
let s = lit(line, b"%session-changed $")?;
|
||||||
|
let (id, s) = digits(s)?;
|
||||||
|
let s = lit(s, b" ")?;
|
||||||
|
let name = nonempty(s)?;
|
||||||
|
Some(Notification::SessionChanged {
|
||||||
|
id,
|
||||||
|
name: name.to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_layout_change(line: &[u8]) -> Option<Notification> {
|
||||||
|
// ^%layout-change @([0-9]+) (.+) (.+) (.*)$
|
||||||
|
let s = lit(line, b"%layout-change @")?;
|
||||||
|
let (id, s) = digits(s)?;
|
||||||
|
let rest = lit(s, b" ")?;
|
||||||
|
// Greedy group 2, then group 3 (non-empty), then group 4 (any).
|
||||||
|
// The last space starts group 4; group 2 ends at the last space at or
|
||||||
|
// before `last - 2` (so that group 3 is non-empty) and at index >= 1.
|
||||||
|
let last = rest.iter().rposition(|&b| b == b' ')?;
|
||||||
|
if last < 3 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let p = rest[1..=last - 2].iter().rposition(|&b| b == b' ')? + 1;
|
||||||
|
let layout = &rest[..p];
|
||||||
|
let visible = &rest[p + 1..last];
|
||||||
|
let flags = &rest[last + 1..];
|
||||||
|
Some(Notification::LayoutChange {
|
||||||
|
window_id: id,
|
||||||
|
layout: layout.to_vec(),
|
||||||
|
visible_layout: visible.to_vec(),
|
||||||
|
raw_flags: flags.to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_window_add(line: &[u8]) -> Option<Notification> {
|
||||||
|
// ^%window-add @([0-9]+)$
|
||||||
|
let s = lit(line, b"%window-add @")?;
|
||||||
|
let (id, s) = digits(s)?;
|
||||||
|
if !s.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Notification::WindowAdd { id })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_window_renamed(line: &[u8]) -> Option<Notification> {
|
||||||
|
// ^%window-renamed @([0-9]+) (.+)$
|
||||||
|
let s = lit(line, b"%window-renamed @")?;
|
||||||
|
let (id, s) = digits(s)?;
|
||||||
|
let s = lit(s, b" ")?;
|
||||||
|
let name = nonempty(s)?;
|
||||||
|
Some(Notification::WindowRenamed {
|
||||||
|
id,
|
||||||
|
name: name.to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_window_pane_changed(line: &[u8]) -> Option<Notification> {
|
||||||
|
// ^%window-pane-changed @([0-9]+) %([0-9]+)$
|
||||||
|
let s = lit(line, b"%window-pane-changed @")?;
|
||||||
|
let (window_id, s) = digits(s)?;
|
||||||
|
let s = lit(s, b" %")?;
|
||||||
|
let (pane_id, s) = digits(s)?;
|
||||||
|
if !s.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Notification::WindowPaneChanged { window_id, pane_id })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_client_detached(line: &[u8]) -> Option<Notification> {
|
||||||
|
// ^%client-detached (.+)$
|
||||||
|
let s = lit(line, b"%client-detached ")?;
|
||||||
|
let client = nonempty(s)?;
|
||||||
|
Some(Notification::ClientDetached {
|
||||||
|
client: client.to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_client_session_changed(line: &[u8]) -> Option<Notification> {
|
||||||
|
// ^%client-session-changed (.+) \$([0-9]+) (.+)$
|
||||||
|
let s = lit(line, b"%client-session-changed ")?;
|
||||||
|
// Greedy client: try the latest split first.
|
||||||
|
for p in (1..s.len()).rev() {
|
||||||
|
if let Some(r) = s[p..].strip_prefix(b" $")
|
||||||
|
&& let Some((session_id, r)) = digits(r)
|
||||||
|
&& let Some(r) = lit(r, b" ")
|
||||||
|
&& let Some(name) = nonempty(r)
|
||||||
|
{
|
||||||
|
return Some(Notification::ClientSessionChanged {
|
||||||
|
client: s[..p].to_vec(),
|
||||||
|
session_id,
|
||||||
|
name: name.to_vec(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Possible notification types from tmux control mode. These are documented
|
||||||
|
/// in tmux(1). A lot of the simple documentation was copied from that man
|
||||||
|
/// page here.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Notification {
|
||||||
|
/// Entering tmux control mode. This isn't an actual event sent by
|
||||||
|
/// tmux but is one sent by us to indicate that we have detected that
|
||||||
|
/// tmux control mode is starting.
|
||||||
|
Enter,
|
||||||
|
|
||||||
|
/// Exit.
|
||||||
|
///
|
||||||
|
/// NOTE: The tmux protocol contains a "reason" string (human friendly)
|
||||||
|
/// associated with this. We currently drop it because we don't need it
|
||||||
|
/// but this may be something we want to add later.
|
||||||
|
Exit,
|
||||||
|
|
||||||
|
/// Dispatched at the end of a begin/end block with the raw data.
|
||||||
|
/// The control mode parser can't parse the data because it is unaware
|
||||||
|
/// of the command that was sent to trigger this output.
|
||||||
|
BlockEnd(Vec<u8>),
|
||||||
|
BlockErr(Vec<u8>),
|
||||||
|
|
||||||
|
/// Raw output from a pane.
|
||||||
|
Output {
|
||||||
|
pane_id: usize,
|
||||||
|
/// unescaped
|
||||||
|
data: Vec<u8>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The client is now attached to the session with ID session-id, which is
|
||||||
|
/// named name.
|
||||||
|
SessionChanged {
|
||||||
|
id: usize,
|
||||||
|
name: Vec<u8>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A session was created or destroyed.
|
||||||
|
SessionsChanged,
|
||||||
|
|
||||||
|
/// The layout of the window with ID window-id changed.
|
||||||
|
LayoutChange {
|
||||||
|
window_id: usize,
|
||||||
|
layout: Vec<u8>,
|
||||||
|
visible_layout: Vec<u8>,
|
||||||
|
raw_flags: Vec<u8>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The window with ID window-id was linked to the current session.
|
||||||
|
WindowAdd {
|
||||||
|
id: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The window with ID window-id was renamed to name.
|
||||||
|
WindowRenamed {
|
||||||
|
id: usize,
|
||||||
|
name: Vec<u8>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The active pane in the window with ID window-id changed to the pane
|
||||||
|
/// with ID pane-id.
|
||||||
|
WindowPaneChanged {
|
||||||
|
window_id: usize,
|
||||||
|
pane_id: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The client has detached.
|
||||||
|
ClientDetached {
|
||||||
|
client: Vec<u8>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The client is now attached to the session with ID session-id, which is
|
||||||
|
/// named name.
|
||||||
|
ClientSessionChanged {
|
||||||
|
client: Vec<u8>,
|
||||||
|
session_id: usize,
|
||||||
|
name: Vec<u8>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn feed(c: &mut Parser, s: &str) {
|
||||||
|
for b in s.bytes() {
|
||||||
|
assert_eq!(c.put(b).unwrap(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmux_begin_end_empty() {
|
||||||
|
let mut c = Parser::new();
|
||||||
|
feed(&mut c, "%begin 1578922740 269 1\n");
|
||||||
|
feed(&mut c, "%end 1578922740 269 1");
|
||||||
|
let n = c.put(b'\n').unwrap().unwrap();
|
||||||
|
assert_eq!(n, Notification::BlockEnd(vec![]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmux_begin_error_empty() {
|
||||||
|
let mut c = Parser::new();
|
||||||
|
feed(&mut c, "%begin 1578922740 269 1\n");
|
||||||
|
feed(&mut c, "%error 1578922740 269 1");
|
||||||
|
let n = c.put(b'\n').unwrap().unwrap();
|
||||||
|
assert_eq!(n, Notification::BlockErr(vec![]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmux_begin_end_data() {
|
||||||
|
let mut c = Parser::new();
|
||||||
|
feed(&mut c, "%begin 1578922740 269 1\n");
|
||||||
|
feed(&mut c, "hello\nworld\n");
|
||||||
|
feed(&mut c, "%end 1578922740 269 1");
|
||||||
|
let n = c.put(b'\n').unwrap().unwrap();
|
||||||
|
assert_eq!(n, Notification::BlockEnd(b"hello\nworld".to_vec()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmux_output() {
|
||||||
|
let mut c = Parser::new();
|
||||||
|
feed(&mut c, "%output %42 foo bar baz");
|
||||||
|
let n = c.put(b'\n').unwrap().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
n,
|
||||||
|
Notification::Output {
|
||||||
|
pane_id: 42,
|
||||||
|
data: b"foo bar baz".to_vec()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmux_session_changed() {
|
||||||
|
let mut c = Parser::new();
|
||||||
|
feed(&mut c, "%session-changed $42 foo");
|
||||||
|
let n = c.put(b'\n').unwrap().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
n,
|
||||||
|
Notification::SessionChanged {
|
||||||
|
id: 42,
|
||||||
|
name: b"foo".to_vec()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmux_sessions_changed() {
|
||||||
|
let mut c = Parser::new();
|
||||||
|
feed(&mut c, "%sessions-changed");
|
||||||
|
let n = c.put(b'\n').unwrap().unwrap();
|
||||||
|
assert_eq!(n, Notification::SessionsChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmux_sessions_changed_carriage_return() {
|
||||||
|
let mut c = Parser::new();
|
||||||
|
feed(&mut c, "%sessions-changed\r");
|
||||||
|
let n = c.put(b'\n').unwrap().unwrap();
|
||||||
|
assert_eq!(n, Notification::SessionsChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmux_layout_change() {
|
||||||
|
let mut c = Parser::new();
|
||||||
|
feed(
|
||||||
|
&mut c,
|
||||||
|
"%layout-change @2 1234x791,0,0{617x791,0,0,0,617x791,618,0,1} 1234x791,0,0{617x791,0,0,0,617x791,618,0,1} *-",
|
||||||
|
);
|
||||||
|
let n = c.put(b'\n').unwrap().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
n,
|
||||||
|
Notification::LayoutChange {
|
||||||
|
window_id: 2,
|
||||||
|
layout: b"1234x791,0,0{617x791,0,0,0,617x791,618,0,1}".to_vec(),
|
||||||
|
visible_layout: b"1234x791,0,0{617x791,0,0,0,617x791,618,0,1}".to_vec(),
|
||||||
|
raw_flags: b"*-".to_vec(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmux_layout_change_empty_flags() {
|
||||||
|
let mut c = Parser::new();
|
||||||
|
feed(&mut c, "%layout-change @2 a b ");
|
||||||
|
let n = c.put(b'\n').unwrap().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
n,
|
||||||
|
Notification::LayoutChange {
|
||||||
|
window_id: 2,
|
||||||
|
layout: b"a".to_vec(),
|
||||||
|
visible_layout: b"b".to_vec(),
|
||||||
|
raw_flags: vec![],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmux_window_add() {
|
||||||
|
let mut c = Parser::new();
|
||||||
|
feed(&mut c, "%window-add @14");
|
||||||
|
let n = c.put(b'\n').unwrap().unwrap();
|
||||||
|
assert_eq!(n, Notification::WindowAdd { id: 14 });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmux_window_renamed() {
|
||||||
|
let mut c = Parser::new();
|
||||||
|
feed(&mut c, "%window-renamed @42 bar");
|
||||||
|
let n = c.put(b'\n').unwrap().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
n,
|
||||||
|
Notification::WindowRenamed {
|
||||||
|
id: 42,
|
||||||
|
name: b"bar".to_vec()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmux_window_pane_changed() {
|
||||||
|
let mut c = Parser::new();
|
||||||
|
feed(&mut c, "%window-pane-changed @42 %2");
|
||||||
|
let n = c.put(b'\n').unwrap().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
n,
|
||||||
|
Notification::WindowPaneChanged {
|
||||||
|
window_id: 42,
|
||||||
|
pane_id: 2
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmux_client_detached() {
|
||||||
|
let mut c = Parser::new();
|
||||||
|
feed(&mut c, "%client-detached /dev/pts/1");
|
||||||
|
let n = c.put(b'\n').unwrap().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
n,
|
||||||
|
Notification::ClientDetached {
|
||||||
|
client: b"/dev/pts/1".to_vec()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmux_client_session_changed() {
|
||||||
|
let mut c = Parser::new();
|
||||||
|
feed(&mut c, "%client-session-changed /dev/pts/1 $2 mysession");
|
||||||
|
let n = c.put(b'\n').unwrap().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
n,
|
||||||
|
Notification::ClientSessionChanged {
|
||||||
|
client: b"/dev/pts/1".to_vec(),
|
||||||
|
session_id: 2,
|
||||||
|
name: b"mysession".to_vec(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmux_idle_garbage_is_exit_then_broken() {
|
||||||
|
let mut c = Parser::new();
|
||||||
|
assert_eq!(c.put(b'x').unwrap(), Some(Notification::Exit));
|
||||||
|
assert_eq!(c.put(b'%').unwrap(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmux_max_bytes() {
|
||||||
|
let mut c = Parser::new();
|
||||||
|
c.max_bytes = 4;
|
||||||
|
for b in b"%out" {
|
||||||
|
assert_eq!(c.put(*b).unwrap(), None);
|
||||||
|
}
|
||||||
|
assert_eq!(c.put(b'p'), Err(Error::OutOfMemory));
|
||||||
|
assert_eq!(c.put(b'p').unwrap(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
//! Types and functions related to tmux protocols (ZIG/src/terminal/tmux.zig).
|
||||||
|
//!
|
||||||
|
//! Only the pure control-mode parser lives in this crate; layout, output and
|
||||||
|
//! viewer belong to `ghostty-terminal`.
|
||||||
|
|
||||||
|
pub mod control;
|
||||||
|
|
||||||
|
pub use control::{Notification as ControlNotification, Parser as ControlParser};
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
//! DFA-based non-allocating error-replacing UTF-8 decoder
|
||||||
|
//! (ZIG/src/terminal/UTF8Decoder.zig).
|
||||||
|
//!
|
||||||
|
//! This implementation is based largely on the excellent work of
|
||||||
|
//! Bjoern Hoehrmann, with slight modifications to support error-
|
||||||
|
//! replacement.
|
||||||
|
//!
|
||||||
|
//! For details on Bjoern's DFA-based UTF-8 decoder, see
|
||||||
|
//! http://bjoern.hoehrmann.de/utf-8/decoder/dfa (MIT licensed)
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
const CHAR_CLASSES: [u8; 256] = [
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,
|
||||||
|
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
|
||||||
|
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
|
||||||
|
10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8,
|
||||||
|
];
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
const TRANSITIONS: [u8; 108] = [
|
||||||
|
0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12,
|
||||||
|
12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12,
|
||||||
|
12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12,
|
||||||
|
12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12,
|
||||||
|
12,36,12,12,12,12,12,12,12,12,12,12,
|
||||||
|
];
|
||||||
|
|
||||||
|
// DFA states
|
||||||
|
const ACCEPT_STATE: u8 = 0;
|
||||||
|
const REJECT_STATE: u8 = 12;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
pub struct Utf8Decoder {
|
||||||
|
// This is where we accumulate our current codepoint.
|
||||||
|
accumulator: u32,
|
||||||
|
// The internal state of the DFA.
|
||||||
|
state: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Utf8Decoder {
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
accumulator: 0,
|
||||||
|
state: ACCEPT_STATE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes the next byte in the utf-8 sequence and emits a tuple of
|
||||||
|
/// - The codepoint that was generated, if there is one.
|
||||||
|
/// - A boolean that indicates whether the provided byte was consumed.
|
||||||
|
///
|
||||||
|
/// The only case where the byte is not consumed is if an ill-formed
|
||||||
|
/// sequence is reached, in which case a replacement character will be
|
||||||
|
/// emitted and the byte will not be consumed.
|
||||||
|
///
|
||||||
|
/// If the byte is not consumed, the caller is responsible for calling
|
||||||
|
/// again with the same byte before continuing.
|
||||||
|
#[inline]
|
||||||
|
pub fn next(&mut self, byte: u8) -> (Option<u32>, bool) {
|
||||||
|
let char_class = CHAR_CLASSES[byte as usize];
|
||||||
|
|
||||||
|
let initial_state = self.state;
|
||||||
|
|
||||||
|
if self.state == ACCEPT_STATE {
|
||||||
|
self.accumulator = (0xFFu32 >> char_class) & (byte as u32);
|
||||||
|
} else {
|
||||||
|
self.accumulator <<= 6;
|
||||||
|
self.accumulator |= (byte & 0x3F) as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = TRANSITIONS[(self.state + char_class) as usize];
|
||||||
|
|
||||||
|
if self.state == ACCEPT_STATE {
|
||||||
|
let cp = self.accumulator;
|
||||||
|
self.accumulator = 0;
|
||||||
|
|
||||||
|
// Emit the fully decoded codepoint.
|
||||||
|
(Some(cp), true)
|
||||||
|
} else if self.state == REJECT_STATE {
|
||||||
|
self.accumulator = 0;
|
||||||
|
self.state = ACCEPT_STATE;
|
||||||
|
// Emit a replacement character. If we rejected the first byte
|
||||||
|
// in a sequence, then it was consumed, otherwise it was not.
|
||||||
|
(Some(0xFFFD), initial_state == ACCEPT_STATE)
|
||||||
|
} else {
|
||||||
|
// Emit nothing, we're in the middle of a sequence.
|
||||||
|
(None, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ascii() {
|
||||||
|
let mut d = Utf8Decoder::new();
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for &byte in b"Hello, World!" {
|
||||||
|
let res = d.next(byte);
|
||||||
|
assert!(res.1);
|
||||||
|
if let Some(cp) = res.0 {
|
||||||
|
out.push(cp as u8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(out, b"Hello, World!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn well_formed_utf8() {
|
||||||
|
let mut d = Utf8Decoder::new();
|
||||||
|
let mut out = Vec::new();
|
||||||
|
// 4 bytes, 3 bytes, 2 bytes, 1 byte
|
||||||
|
for &byte in "😄✤ÁA".as_bytes() {
|
||||||
|
let mut consumed = false;
|
||||||
|
while !consumed {
|
||||||
|
let res = d.next(byte);
|
||||||
|
consumed = res.1;
|
||||||
|
// There are no errors in this sequence, so
|
||||||
|
// every byte should be consumed first try.
|
||||||
|
assert!(consumed);
|
||||||
|
if let Some(cp) = res.0 {
|
||||||
|
out.push(cp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(out, vec![0x1F604, 0x2724, 0xC1, 0x41]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn partially_invalid_utf8() {
|
||||||
|
let mut d = Utf8Decoder::new();
|
||||||
|
let mut out = Vec::new();
|
||||||
|
// Illegally terminated sequence, valid sequence, illegal surrogate pair.
|
||||||
|
let mut input = vec![0xF0, 0x9F];
|
||||||
|
input.extend("😄".as_bytes());
|
||||||
|
input.extend([0xED, 0xA0, 0x80]);
|
||||||
|
for byte in input {
|
||||||
|
let mut consumed = false;
|
||||||
|
while !consumed {
|
||||||
|
let res = d.next(byte);
|
||||||
|
consumed = res.1;
|
||||||
|
if let Some(cp) = res.0 {
|
||||||
|
out.push(cp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(out, vec![0xFFFD, 0x1F604, 0xFFFD, 0xFFFD, 0xFFFD]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
//! X11 color names (ZIG/src/terminal/x11_color.zig).
|
||||||
|
|
||||||
|
use crate::color::Rgb;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
/// This is the rgb.txt file from the X11 project. This was last sourced
|
||||||
|
/// from this location: https://gitlab.freedesktop.org/xorg/app/rgb
|
||||||
|
/// This data is licensed under the MIT/X11 license while this file is
|
||||||
|
/// licensed under the same license as Ghostty.
|
||||||
|
const DATA: &str = include_str!("res/rgb.txt");
|
||||||
|
|
||||||
|
/// The map of all available X11 colors. Keys are lowercase ASCII; lookups
|
||||||
|
/// are ASCII case-insensitive (see [`get`]).
|
||||||
|
fn map() -> &'static HashMap<String, Rgb> {
|
||||||
|
static MAP: OnceLock<HashMap<String, Rgb>> = OnceLock::new();
|
||||||
|
MAP.get_or_init(|| {
|
||||||
|
let mut m = HashMap::new();
|
||||||
|
// Parse the line. This is not very robust parsing, because we expect
|
||||||
|
// a very exact format for rgb.txt.
|
||||||
|
for line in DATA.split('\n') {
|
||||||
|
if line.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let r: u8 = line[0..3].trim_matches(' ').parse().expect("bad rgb.txt");
|
||||||
|
let g: u8 = line[4..7].trim_matches(' ').parse().expect("bad rgb.txt");
|
||||||
|
let b: u8 = line[8..11].trim_matches(' ').parse().expect("bad rgb.txt");
|
||||||
|
let name = line[12..].trim_matches(|c| c == ' ' || c == '\t' || c == '\n');
|
||||||
|
m.entry(name.to_ascii_lowercase())
|
||||||
|
.or_insert(Rgb { r, g, b });
|
||||||
|
}
|
||||||
|
m
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up an X11 color by name (ASCII case-insensitive).
|
||||||
|
pub fn get(name: &str) -> Option<Rgb> {
|
||||||
|
map().get(&name.to_ascii_lowercase()).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Byte-slice variant of [`get`].
|
||||||
|
pub fn get_bytes(name: &[u8]) -> Option<Rgb> {
|
||||||
|
get(std::str::from_utf8(name).ok()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn x11_color_map() {
|
||||||
|
assert_eq!(None, get("nosuchcolor"));
|
||||||
|
assert_eq!(
|
||||||
|
Rgb {
|
||||||
|
r: 255,
|
||||||
|
g: 255,
|
||||||
|
b: 255
|
||||||
|
},
|
||||||
|
get("white").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Rgb {
|
||||||
|
r: 0,
|
||||||
|
g: 250,
|
||||||
|
b: 154
|
||||||
|
},
|
||||||
|
get("medium spring green").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Rgb {
|
||||||
|
r: 34,
|
||||||
|
g: 139,
|
||||||
|
b: 34
|
||||||
|
},
|
||||||
|
get("ForestGreen").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Rgb {
|
||||||
|
r: 34,
|
||||||
|
g: 139,
|
||||||
|
b: 34
|
||||||
|
},
|
||||||
|
get("FoReStGReen").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(Rgb { r: 0, g: 0, b: 0 }, get("black").unwrap());
|
||||||
|
assert_eq!(Rgb { r: 255, g: 0, b: 0 }, get("red").unwrap());
|
||||||
|
assert_eq!(Rgb { r: 0, g: 255, b: 0 }, get("green").unwrap());
|
||||||
|
assert_eq!(Rgb { r: 0, g: 0, b: 255 }, get("blue").unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
Rgb {
|
||||||
|
r: 124,
|
||||||
|
g: 252,
|
||||||
|
b: 0
|
||||||
|
},
|
||||||
|
get("lawngreen").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Rgb {
|
||||||
|
r: 0,
|
||||||
|
g: 250,
|
||||||
|
b: 154
|
||||||
|
},
|
||||||
|
get("mediumspringgreen").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Rgb {
|
||||||
|
r: 34,
|
||||||
|
g: 139,
|
||||||
|
b: 34
|
||||||
|
},
|
||||||
|
get("forestgreen").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# ghostty-vt W1 notes
|
||||||
|
|
||||||
|
## Ported (Zig -> Rust, under crates/ghostty-vt/src)
|
||||||
|
- Parser.zig -> parser.rs (`Parser`, `State`, `TransitionAction`, `Action`, `Csi`/`Esc`/`Dcs`, `MAX_PARAMS`, `SepList`)
|
||||||
|
- parse_table.zig -> parse_table.rs (`TABLE`, built by `const fn`; identical rule order)
|
||||||
|
- UTF8Decoder.zig -> utf8_decoder.rs (`Utf8Decoder::next -> (Option<u32>, bool)`)
|
||||||
|
- osc.zig -> osc/mod.rs; osc/encoding.zig -> osc/encoding.rs; osc/parsers/*.zig -> osc/parsers/*.rs (all 15)
|
||||||
|
- color.zig -> color.rs; x11_color.zig -> x11_color.rs (res/rgb.txt copied, `include_str!`, lazy map); kitty/color.zig -> kitty/color.rs
|
||||||
|
|
||||||
|
## Deviations / API notes
|
||||||
|
- `Action` owns its data (fixed inline arrays) rather than borrowing from the parser: use `csi.params()`, `csi.intermediates()`, `csi.final_`, `csi.params_sep`. `Print(u32)`. `Parser::next` returns `[Option<Action>; 3]`.
|
||||||
|
- `Parser::new()`; `parser.osc_parser.alloc` is a `bool` ("allocator available", Zig `?Allocator`). Stream must set `osc_parser.alloc = true` as stream.zig does, otherwise OSC 4/5/10-19/104/110-119/133/21 are rejected like in Zig.
|
||||||
|
- OSC `Command` is a Rust enum (`osc::Command`, `osc::Key` via `Command::key()`); `[:0]const u8` fields are `Vec<u8>` (no NUL, not necessarily UTF-8). `Parser::end` returns an owned `Option<Command>`.
|
||||||
|
- Fixed buffer (MAX_BUF=2048, one byte reserved for NUL where Zig writes one) vs allocating writer = one `Vec<u8>` with an optional cap. `Parser::data()` (reserves NUL) / `buffered()` (does not) replace `writer.buffered()`. Remaining edge: osc9/iterm2/text_sizing use `data()` even where Zig writes no NUL for some sub-commands (only differs at exactly 2048 payload bytes).
|
||||||
|
- semantic_prompt: `Option` enum is `OptionKey`; `printf_q_decode`/`url_percent_decode` local copies (TODO(integration): move to/consume from ghostty-os string_encoding). `color::Rgb` (alias `RGB`); `DynamicRgb.override` is `override_`.
|
||||||
|
- Clippy: test modules `#![allow(clippy::unwrap_used)]`.
|
||||||
|
|
||||||
|
## Skipped tests
|
||||||
|
- `ghostty.h Command.ProgressReport.State` (C ABI), `refAllDecls`/`test { _ = ... }` comptime-reference tests, comptime `@sizeOf(Command)` assert.
|
||||||
|
- One iterm2 test is `SkipZigTest` in Zig; kept as `#[ignore]`.
|
||||||
|
- Extra tests added: osc_112 in color parser, printf-q decode in semantic_prompt.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# ghostty-vt W2 notes
|
||||||
|
|
||||||
|
## Ported (Zig -> Rust, all in crates/ghostty-vt/src)
|
||||||
|
- csi.zig -> csi.rs (EraseDisplay, EraseLine/TabClear with `Other(u8)` for the non-exhaustive Zig enums, SizeReportStyle, TitlePushPop)
|
||||||
|
- sgr.zig -> sgr.rs (`Attribute<'a>`, `Parser<'a>` implementing `Iterator`; uses `crate::color::{Name, Rgb}` and `crate::parser::{SepList, MAX_PARAMS}`)
|
||||||
|
- modes.zig -> modes.rs (`Mode` enum, `ENTRIES` table, `ModePacked(u64)`, `ModeState`, `mode_from_int`)
|
||||||
|
- charsets.zig -> charsets.rs (const-built 256-entry tables)
|
||||||
|
- ansi.zig -> ansi.rs; device_status.zig -> device_status.rs; mouse_shape.zig -> mouse_shape.rs
|
||||||
|
- kitty/key.zig: three remaining tests added to existing kitty/key.rs
|
||||||
|
- kitty/graphics_command.zig -> kitty/graphics_command.rs (parser + Response + all command structs; `pub mod graphics_command` in kitty/mod.rs)
|
||||||
|
- apc.zig -> apc.rs; dcs.zig -> dcs.rs; tmux/control.zig -> tmux/control.rs (+ tmux/mod.rs, only control)
|
||||||
|
- Dependencies added: `thiserror`, `base64`.
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
- Non-exhaustive Zig enums (EraseLine, TabClear, RenditionAspect, StatusLineType) are Rust enums with `Other(raw)`; C0 uses `from_int -> Option`.
|
||||||
|
- tmux Notification owns its data (`Vec<u8>`) instead of borrowing the parser buffer. Oniguruma regexes replaced by hand-written matchers with the same greedy semantics. IDs overflowing usize are treated as non-match (Zig: `unreachable`).
|
||||||
|
- DCS/APC handlers drop allocator params; `tmux` state holds `Box<tmux::control::Parser>`.
|
||||||
|
- graphics_command base64: `simd.base64` replaced by `base64` crate (standard alphabet, padding optional, ASCII whitespace ignored like simdutf). zlib is not decoded in the parser (Zig does not either), so no flate2 dependency.
|
||||||
|
- `Parser::complete` moves the decoded payload out (Zig `decodeData` also consumes the buffer).
|
||||||
|
- modes: Zig tests `ModePacked` size == 8; Rust has an equivalent `packed_size` test (u64). Mode bit index = enum discriminant = position in ENTRIES.
|
||||||
|
- sgr uses a bounds-tolerant `sep_is_set` (Zig's StaticBitSet would assert past MAX_PARAMS).
|
||||||
|
- `Attribute` variant names: `Fg8/Bg8/Fg8Bright/Bg8Bright/Fg256/Bg256/UnderlineColor256` for Zig `@"8_fg"` etc.
|
||||||
|
|
||||||
|
## Skipped (Zig-only)
|
||||||
|
- sgr "Attribute C compat", `Attribute.C`/`cval`/`lib.Enum`/`lib.TaggedUnion` (C ABI).
|
||||||
|
- mouse_shape "ghostty.h MouseShape" (C header check) and GObject type registration (belongs to apprt-gtk; `MouseShape` is `repr(i32)` in declaration order).
|
||||||
|
- modes `test { _ = Mode; _ = ModePacked }` and charsets/tests reflection-only bits (replaced by explicit tests).
|
||||||
|
- Allocator-failure aspects (`deinit`, arenas) have no Rust equivalent.
|
||||||
|
|
||||||
|
## Known gaps / integration
|
||||||
|
- dcs.rs uses `crate::parser::Dcs` (W1). `sgr`/`dcs` depend on W1 modules compiling.
|
||||||
|
- tmux layout/output/viewer are NOT here (ghostty-terminal).
|
||||||
Reference in new issue
Block a user