feat(termio): port pty, child command spawning, shell integration
Ports the parts of Ghostty's IO layer that don't depend on the terminal: - pty.zig: pty pair, window size, mode, child-side setsid/TIOCSCTTY; master is CLOEXEC with IUTF8 set - Command.zig: fork/execvpe with Ghostty's exact child setup (stdio dup2, best-effort chdir, rlimit restore, os/runtime pre-exec hooks, cgroup handling) and exit status decoding - shell_integration.zig for bash/zsh/fish/elvish/nushell plus feature flags; scripts copied verbatim to resources/shell-integration/ - terminfo generator for xterm-ghostty, byte-identical to Zig's encode output (golden file produced with the real Zig code) This crate opts into unsafe_code = "deny" (fork/exec need it) with allow + SAFETY comments on minimal items. Integration left: config-derived option types are local stand-ins; cgroup D-Bus scope creation is a closure hook; Exec/Termio/Thread/ stream_handler wait for ghostty-terminal. Pty now closes both fds on drop (Zig never auto-closed the slave) - to re-check when wiring Exec. Test Plan: - cargo test -p ghostty-termio (52 tests, incl. real pty spawn test expecting "hi\r\n31 97\r\n" from `echo hi; stty size`)
This commit is contained in:
1 parent
668c236291
commit
09cbe3bc97
21 files changed
+7120
-28
No files matched your search
Generated
+1215
-24
@@ -17,6 +17,27 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
@@ -160,6 +181,15 @@ dependencies = [
|
||||
"syn 3.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
@@ -172,6 +202,12 @@ version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.23.1"
|
||||
@@ -184,10 +220,10 @@ version = "0.72.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.13.2",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
@@ -198,6 +234,27 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.2"
|
||||
@@ -210,6 +267,15 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.7.0"
|
||||
@@ -229,12 +295,27 @@ version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "by_address"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.8.0"
|
||||
@@ -277,6 +358,20 @@ dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
"itoa",
|
||||
"rustversion",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
@@ -307,6 +402,24 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.2"
|
||||
@@ -316,18 +429,149 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "critical-section"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"crossterm_winapi",
|
||||
"derive_more",
|
||||
"document-features",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csscolorparser"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf"
|
||||
dependencies = [
|
||||
"lab",
|
||||
"phf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
|
||||
dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 3.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 3.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltae"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dlib"
|
||||
version = "0.5.3"
|
||||
@@ -337,6 +581,15 @@ dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||
dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.19.0"
|
||||
@@ -386,6 +639,15 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "euclid"
|
||||
version = "0.22.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.2"
|
||||
@@ -406,6 +668,16 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.5.0"
|
||||
@@ -421,12 +693,35 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filedescriptor"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"thiserror 1.0.69",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
||||
|
||||
[[package]]
|
||||
name = "finl_unicode"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80bb028c8b4148c9ee0cca68fcd9add6044e81d3619f48577ddf13a263d047a2"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.10"
|
||||
@@ -438,6 +733,18 @@ dependencies = [
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "freetype-sys"
|
||||
version = "0.23.0"
|
||||
@@ -492,6 +799,28 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 5.3.0",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
@@ -500,7 +829,7 @@ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"r-efi 6.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -536,9 +865,14 @@ dependencies = [
|
||||
name = "ghostty-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"flate2",
|
||||
"ghostty-config",
|
||||
"ghostty-input",
|
||||
"ghostty-os",
|
||||
"log",
|
||||
"ratatui",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -551,7 +885,7 @@ dependencies = [
|
||||
"ghostty-vt",
|
||||
"log",
|
||||
"onig",
|
||||
"thiserror",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -573,20 +907,20 @@ name = "ghostty-datastruct"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"half",
|
||||
"thiserror",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghostty-font"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.13.2",
|
||||
"freetype-sys",
|
||||
"ghostty-unicode",
|
||||
"harfbuzz-sys",
|
||||
"log",
|
||||
"png",
|
||||
"thiserror",
|
||||
"thiserror 2.0.21",
|
||||
"tiny-skia",
|
||||
"yeslogic-fontconfig-sys",
|
||||
]
|
||||
@@ -597,7 +931,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"ghostty-vt",
|
||||
"indexmap",
|
||||
"thiserror",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -606,10 +940,10 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"gettext-rs",
|
||||
"log",
|
||||
"nix",
|
||||
"nix 0.31.3",
|
||||
"rustix",
|
||||
"temp-env",
|
||||
"thiserror",
|
||||
"thiserror 2.0.21",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
@@ -642,9 +976,13 @@ dependencies = [
|
||||
name = "ghostty-termio"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ghostty-config",
|
||||
"ghostty-os",
|
||||
"ghostty-terminal",
|
||||
"libc",
|
||||
"log",
|
||||
"nix 0.31.3",
|
||||
"rustix",
|
||||
"temp-env",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -655,10 +993,10 @@ version = "0.1.0"
|
||||
name = "ghostty-vt"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.23.1",
|
||||
"log",
|
||||
"memchr",
|
||||
"thiserror",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -692,11 +1030,33 @@ dependencies = [
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
@@ -710,6 +1070,12 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
@@ -717,7 +1083,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.17.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a37b2691796cffeb8a8cd305ac66e65841559f147f4e63231d0eafa4db5384d1"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instability"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c3b5acc1e2fd9375041a388da33d1eb8aed5f7a8c0dd3543e3ea2805adfbe20"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"indoc",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -729,6 +1117,21 @@ dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.106"
|
||||
@@ -740,6 +1143,17 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kasuari"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899"
|
||||
dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
"portable-atomic",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "konst"
|
||||
version = "0.2.20"
|
||||
@@ -755,6 +1169,12 @@ version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37"
|
||||
|
||||
[[package]]
|
||||
name = "lab"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.1"
|
||||
@@ -777,6 +1197,12 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||
|
||||
[[package]]
|
||||
name = "libz-sys"
|
||||
version = "1.1.29"
|
||||
@@ -789,12 +1215,27 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "line-clipping"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e752191d037c44ad111a8caa762921926658402f01cc1253f7bef2020ece4f5e"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||
|
||||
[[package]]
|
||||
name = "locale_config"
|
||||
version = "0.3.0"
|
||||
@@ -823,6 +1264,25 @@ version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.18.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5"
|
||||
dependencies = [
|
||||
"hashbrown 0.17.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mac_address"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3d0581a75c45969c63afd2bb87b1d8b25cfcc556c7a918cf9bb13b0fe8d2381"
|
||||
dependencies = [
|
||||
"nix 0.31.3",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
@@ -838,6 +1298,12 @@ version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "memmem"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
@@ -873,16 +1339,41 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.13.2",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -895,6 +1386,41 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc"
|
||||
version = "0.2.7"
|
||||
@@ -921,7 +1447,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.13.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -939,7 +1465,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.13.2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
@@ -964,7 +1490,7 @@ version = "6.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.13.2",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"onig_sys",
|
||||
@@ -981,6 +1507,15 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
@@ -991,6 +1526,39 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "palette"
|
||||
version = "0.7.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddeed8580d347d2abf3dcf06a5f0b3dc020258338526b277847cd4248a70fc64"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"libm",
|
||||
"palette_derive",
|
||||
"palette_math",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "palette_derive"
|
||||
version = "0.7.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88537020289b719d81be994ccf1bbf4990f477e2f69ee52fe3e45f43a02e56be"
|
||||
dependencies = [
|
||||
"by_address",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "palette_math"
|
||||
version = "0.7.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e6eb142958d64335fb0e345c5b9ead2ecd6fc438c307e9d7d3c4fd428dbaf12"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
@@ -1020,6 +1588,102 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b568374ba38b33a6c627141f891faf16902b08d2db26b8ede1bcb0a15b1919fa"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"psm",
|
||||
"stacker",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_derive"
|
||||
version = "2.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b66e184b924cebaaff20ab2256ca52f12332d528a39aa76553b5d96f92aacf7f"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_generator"
|
||||
version = "2.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a87478d267e4de54a626af9754f2f0f58e927aac6ed0575fe89bc05ad6851694"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_meta"
|
||||
version = "2.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f986f248b4241ac359b831f6139aaa34e03b08a37b6caf7e201a33f95c869e1"
|
||||
dependencies = [
|
||||
"pest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
@@ -1049,7 +1713,7 @@ version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.13.2",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
@@ -1070,6 +1734,18 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
@@ -1098,6 +1774,15 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psm"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
@@ -1107,19 +1792,141 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.30.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d"
|
||||
dependencies = [
|
||||
"instability",
|
||||
"ratatui-core",
|
||||
"ratatui-crossterm",
|
||||
"ratatui-macros",
|
||||
"ratatui-termina",
|
||||
"ratatui-termwiz",
|
||||
"ratatui-widgets",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui-core"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"compact_str",
|
||||
"critical-section",
|
||||
"hashbrown 0.17.1",
|
||||
"itertools 0.14.0",
|
||||
"kasuari",
|
||||
"lru",
|
||||
"palette",
|
||||
"serde",
|
||||
"strum",
|
||||
"thiserror 2.0.21",
|
||||
"unicode-segmentation",
|
||||
"unicode-truncate",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui-crossterm"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossterm",
|
||||
"instability",
|
||||
"ratatui-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui-macros"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814"
|
||||
dependencies = [
|
||||
"ratatui-core",
|
||||
"ratatui-widgets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui-termina"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2"
|
||||
dependencies = [
|
||||
"instability",
|
||||
"ratatui-core",
|
||||
"termina",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui-termwiz"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977"
|
||||
dependencies = [
|
||||
"ratatui-core",
|
||||
"termwiz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui-widgets"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"hashbrown 0.17.1",
|
||||
"indoc",
|
||||
"instability",
|
||||
"itertools 0.14.0",
|
||||
"line-clipping",
|
||||
"ratatui-core",
|
||||
"serde",
|
||||
"strum",
|
||||
"time",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.13.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1157,13 +1964,22 @@ version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.13.2",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
@@ -1176,12 +1992,24 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
@@ -1223,6 +2051,17 @@ dependencies = [
|
||||
"syn 3.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -1235,6 +2074,27 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
@@ -1251,6 +2111,12 @@ version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
@@ -1263,12 +2129,69 @@ version = "1.16.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
|
||||
|
||||
[[package]]
|
||||
name = "stacker"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"psm",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strict-num"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
@@ -1313,19 +2236,115 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termina"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"parking_lot",
|
||||
"rustix",
|
||||
"signal-hook",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminfo"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"nom",
|
||||
"phf",
|
||||
"phf_codegen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termios"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termwiz"
|
||||
version = "0.23.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.13.2",
|
||||
"fancy-regex",
|
||||
"filedescriptor",
|
||||
"finl_unicode",
|
||||
"fixedbitset",
|
||||
"hex",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"memmem",
|
||||
"nix 0.29.0",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"ordered-float",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"phf",
|
||||
"sha2",
|
||||
"signal-hook",
|
||||
"siphasher",
|
||||
"terminfo",
|
||||
"termios",
|
||||
"thiserror 1.0.69",
|
||||
"ucd-trie",
|
||||
"unicode-segmentation",
|
||||
"vtparse",
|
||||
"wezterm-bidi",
|
||||
"wezterm-blob-leases",
|
||||
"wezterm-color-types",
|
||||
"wezterm-dynamic",
|
||||
"wezterm-input-types",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
"thiserror-impl 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1339,6 +2358,27 @@ dependencies = [
|
||||
"syn 3.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"libc",
|
||||
"num-conv",
|
||||
"num_threads",
|
||||
"powerfmt",
|
||||
"serde_core",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.12.0"
|
||||
@@ -1426,6 +2466,18 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.2.1"
|
||||
@@ -1443,18 +2495,49 @@ version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-truncate"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606"
|
||||
dependencies = [
|
||||
"atomic",
|
||||
"getrandom 0.4.3",
|
||||
"js-sys",
|
||||
"serde_core",
|
||||
"wasm-bindgen",
|
||||
@@ -1466,6 +2549,36 @@ version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vtparse"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.4+wasi-0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.129"
|
||||
@@ -1511,6 +2624,78 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wezterm-bidi"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec"
|
||||
dependencies = [
|
||||
"log",
|
||||
"wezterm-dynamic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wezterm-blob-leases"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"mac_address",
|
||||
"sha2",
|
||||
"thiserror 1.0.69",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wezterm-color-types"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296"
|
||||
dependencies = [
|
||||
"csscolorparser",
|
||||
"deltae",
|
||||
"lazy_static",
|
||||
"wezterm-dynamic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wezterm-dynamic"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ordered-float",
|
||||
"strsim",
|
||||
"thiserror 1.0.69",
|
||||
"wezterm-dynamic-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wezterm-dynamic-derive"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wezterm-input-types"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"euclid",
|
||||
"lazy_static",
|
||||
"serde",
|
||||
"wezterm-dynamic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -1661,6 +2846,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "yeslogic-fontconfig-sys"
|
||||
version = "6.0.1"
|
||||
|
||||
@@ -4,10 +4,52 @@ version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
# NOTE: the `ghostty-config` and `ghostty-terminal` dependencies were removed
|
||||
# for now (config is being written concurrently, terminal is in progress).
|
||||
# TODO(integration): re-add them when Exec/Termio/stream_handler are ported and
|
||||
# replace the local stand-ins (shell_integration::{Command, ShellIntegrationFeatures},
|
||||
# command::{LinuxCgroup, GtkSingleInstance}).
|
||||
[dependencies]
|
||||
ghostty-config.workspace = true
|
||||
ghostty-terminal.workspace = true
|
||||
ghostty-os.workspace = true
|
||||
libc = "0.2.190"
|
||||
log = "0.4.34"
|
||||
nix = { version = "0.31.3", features = ["signal"] }
|
||||
rustix = { version = "1.1.5", features = ["fs", "process", "pty", "termios", "stdio"] }
|
||||
thiserror = "2.0.21"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
# Termio forks/execs children and sets up ptys; unsafe is denied by default
|
||||
# and only allowed on minimal, SAFETY-commented items.
|
||||
[lints.rust]
|
||||
unsafe_code = "deny"
|
||||
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
todo = "warn"
|
||||
unwrap_used = "warn"
|
||||
# Porting a low-level terminal emulator involves a lot of intentional
|
||||
# numeric casts and long state-machine functions; these pedantic lints are
|
||||
# noise for this codebase.
|
||||
cast_possible_truncation = "allow"
|
||||
cast_possible_wrap = "allow"
|
||||
cast_sign_loss = "allow"
|
||||
cast_precision_loss = "allow"
|
||||
cast_lossless = "allow"
|
||||
module_name_repetitions = "allow"
|
||||
too_many_lines = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
similar_names = "allow"
|
||||
struct_excessive_bools = "allow"
|
||||
fn_params_excessive_bools = "allow"
|
||||
match_same_arms = "allow"
|
||||
unreadable_literal = "allow"
|
||||
many_single_char_names = "allow"
|
||||
doc_markdown = "allow"
|
||||
must_use_candidate = "allow"
|
||||
return_self_not_must_use = "allow"
|
||||
used_underscore_binding = "allow"
|
||||
large_stack_arrays = "allow"
|
||||
inline_always = "allow"
|
||||
|
||||
[dev-dependencies]
|
||||
temp-env = "0.3.6"
|
||||
@@ -0,0 +1,872 @@
|
||||
//! Command launches sub-processes. This is an alternate implementation to
|
||||
//! `std::process::Command` (mirroring why the Zig one exists) since we need
|
||||
//! precise control over what happens between fork and exec (e.g. attaching
|
||||
//! a shell to a pty, cgroup transitions, best-effort chdir).
|
||||
//!
|
||||
//! Port of `Command.zig` (POSIX/Linux only) plus the Linux cgroup pre-exec /
|
||||
//! post-fork logic from `apprt/gtk/{pre_exec,post_fork}.zig`.
|
||||
//!
|
||||
//! Safety: the child side of [`Command::start`] runs between `fork` and `exec`
|
||||
//! in a possibly multithreaded process. All allocation is done before the
|
||||
//! fork; the child only performs raw syscalls plus the user supplied
|
||||
//! `os_pre_exec` / `rt_pre_exec` hooks (which must therefore be
|
||||
//! async-signal-safe, like their Zig counterparts are expected to be).
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::{CString, NulError, c_char},
|
||||
os::fd::OwnedFd,
|
||||
panic::{AssertUnwindSafe, catch_unwind},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ghostty_os::file::Rlimit;
|
||||
use rustix::{
|
||||
io::Errno,
|
||||
process::{self, WaitOptions},
|
||||
stdio,
|
||||
};
|
||||
|
||||
/// Function executed /in the child process/ after the fork, but before
|
||||
/// exec'ing the command. If the function returns `Some(code)`, the child
|
||||
/// process will be exited with that exit code.
|
||||
///
|
||||
/// Must be async-signal-safe (see module docs).
|
||||
pub type PreExecFn = Box<dyn Fn() -> Option<u8> + Send + Sync>;
|
||||
|
||||
/// Allowable set of errors that can be returned by a post fork function. Any
|
||||
/// errors will result in the failure to create the surface.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||
#[error("post fork hook failed")]
|
||||
pub struct PostForkError;
|
||||
|
||||
/// Function executed /in the parent process/ after the fork. It receives the
|
||||
/// command (the pid is already set).
|
||||
pub type PostForkFn = Box<dyn Fn(&Command) -> Result<(), PostForkError> + Send + Sync>;
|
||||
|
||||
/// Errors from [`Command::start`] (all happen in the parent; the child never
|
||||
/// returns from `start`, it either execs or exits like Zig's
|
||||
/// `ExecFailedInChild` handling).
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum StartError {
|
||||
#[error("argument or environment contains an interior NUL byte")]
|
||||
InvalidArgument(#[from] NulError),
|
||||
#[error("fork failed: {0}")]
|
||||
Fork(Errno),
|
||||
#[error(transparent)]
|
||||
PostFork(#[from] PostForkError),
|
||||
}
|
||||
|
||||
/// The various methods a process may exit.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Exit {
|
||||
/// Exited by normal exit call, value is exit status
|
||||
Exited(u8),
|
||||
|
||||
/// Exited by a signal, value is the signal
|
||||
Signal(u32),
|
||||
|
||||
/// Exited by a stop signal, value is signal
|
||||
Stopped(u32),
|
||||
|
||||
/// Unknown exit reason, value is the status from waitpid
|
||||
Unknown(u32),
|
||||
}
|
||||
|
||||
impl Exit {
|
||||
/// Decode a raw `waitpid` status (`W.IFEXITED` etc).
|
||||
pub fn init(status: u32) -> Exit {
|
||||
let low = status & 0x7f;
|
||||
if low == 0 {
|
||||
Exit::Exited(((status >> 8) & 0xff) as u8)
|
||||
} else if (((low + 1) as u8 as i8) >> 1) > 0 {
|
||||
Exit::Signal(low)
|
||||
} else if (status & 0xff) == 0x7f {
|
||||
Exit::Stopped((status >> 8) & 0xff)
|
||||
} else {
|
||||
Exit::Unknown(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirrors `config.Command`'s relevant enum `LinuxCgroup`.
|
||||
/// TODO(integration): replace with the ghostty-config type.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum LinuxCgroup {
|
||||
Never,
|
||||
Always,
|
||||
#[default]
|
||||
SingleInstance,
|
||||
}
|
||||
|
||||
/// TODO(integration): replace with the ghostty-config type.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum GtkSingleInstance {
|
||||
False,
|
||||
True,
|
||||
#[default]
|
||||
Detect,
|
||||
}
|
||||
|
||||
/// Configuration information needed by the apprt pre exec function
|
||||
/// (`apprt/gtk/pre_exec.zig` `PreExecInfo`). Trivially copyable.
|
||||
/// TODO(integration): build from `ghostty-config::Config` (`init(cfg)`).
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct RtPreExecInfo {
|
||||
pub gtk_single_instance: GtkSingleInstance,
|
||||
pub linux_cgroup: LinuxCgroup,
|
||||
pub linux_cgroup_hard_fail: bool,
|
||||
}
|
||||
|
||||
/// Configuration information needed by the apprt post fork function
|
||||
/// (`apprt/gtk/post_fork.zig` `PostForkInfo`).
|
||||
/// TODO(integration): build from `ghostty-config::Config` (`init(cfg)`).
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct RtPostForkInfo {
|
||||
pub gtk_single_instance: GtkSingleInstance,
|
||||
pub linux_cgroup: LinuxCgroup,
|
||||
pub linux_cgroup_hard_fail: bool,
|
||||
pub linux_cgroup_memory_limit: Option<u64>,
|
||||
pub linux_cgroup_processes_limit: Option<u64>,
|
||||
}
|
||||
|
||||
/// A command to launch.
|
||||
#[derive(Default)]
|
||||
pub struct Command {
|
||||
/// Path to the command to run. This doesn't have to be an absolute path,
|
||||
/// because we use exec functions that search the PATH, if necessary.
|
||||
pub path: String,
|
||||
|
||||
/// Command-line arguments. It is the responsibility of the caller to set
|
||||
/// args[0] to the command. If args is empty then args[0] will
|
||||
/// automatically be set to equal path.
|
||||
pub args: Vec<String>,
|
||||
|
||||
/// Environment variables for the child process. If this is `None`,
|
||||
/// inherits the environment variables from this process. These are the
|
||||
/// exact environment variables to set; these are /not/ merged.
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
|
||||
/// Working directory to change to in the child process. If not set, the
|
||||
/// working directory of the calling process is preserved. Failure to
|
||||
/// change directory is ignored (as in Ghostty).
|
||||
pub cwd: Option<String>,
|
||||
|
||||
/// The file handle to set for stdin/out/err. If this isn't set, we do
|
||||
/// nothing explicitly so it is up to the behavior of the operating system.
|
||||
/// (The fds are duplicated onto 0/1/2 in the child; the caller keeps its
|
||||
/// own handles by passing `try_clone()`d fds.)
|
||||
pub stdin: Option<OwnedFd>,
|
||||
pub stdout: Option<OwnedFd>,
|
||||
pub stderr: Option<OwnedFd>,
|
||||
|
||||
/// Rlimits to restore in the child (Ghostty raises RLIMIT_NOFILE at
|
||||
/// startup; replaces `global_state.rlimits.restore()`).
|
||||
/// TODO(integration): fill from the global state.
|
||||
pub restore_rlimit: Option<Rlimit>,
|
||||
|
||||
/// Executed /in the child process/ after fork but before exec. Useful to
|
||||
/// setup some state in the child before the exec process takes over, such
|
||||
/// as signal handlers, setsid, setuid, etc.
|
||||
pub os_pre_exec: Option<PreExecFn>,
|
||||
|
||||
/// Same as `os_pre_exec` but runs afterwards and is meant for the apprt
|
||||
/// (e.g. [`linux_cgroup_pre_exec`]).
|
||||
pub rt_pre_exec: Option<PreExecFn>,
|
||||
|
||||
/// Executed /in the parent process/ after the fork.
|
||||
pub rt_post_fork: Option<PostForkFn>,
|
||||
|
||||
/// Process ID is set after start is called.
|
||||
pub pid: Option<i32>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Command {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Command")
|
||||
.field("path", &self.path)
|
||||
.field("args", &self.args)
|
||||
.field("cwd", &self.cwd)
|
||||
.field("pid", &self.pid)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
/// Everything that must be allocated before forking.
|
||||
struct Prepared {
|
||||
path: CString,
|
||||
argv: Vec<CString>,
|
||||
envp: Vec<CString>,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn new(path: impl Into<String>, args: &[&str]) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
args: args.iter().map(|s| (*s).to_owned()).collect(),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare(&self) -> Result<Prepared, NulError> {
|
||||
let path = CString::new(self.path.as_str())?;
|
||||
let argv = if self.args.is_empty() {
|
||||
vec![path.clone()]
|
||||
} else {
|
||||
self.args
|
||||
.iter()
|
||||
.map(|a| CString::new(a.as_str()))
|
||||
.collect::<Result<_, _>>()?
|
||||
};
|
||||
let envp = match &self.env {
|
||||
Some(map) => map
|
||||
.iter()
|
||||
.map(|(k, v)| CString::new(format!("{k}={v}")))
|
||||
.collect::<Result<_, _>>()?,
|
||||
// Inherit: snapshot our environment before the fork (nothing may
|
||||
// allocate in the child).
|
||||
None => std::env::vars_os()
|
||||
.map(|(k, v)| {
|
||||
let mut b = k.into_encoded_bytes();
|
||||
b.push(b'=');
|
||||
b.extend_from_slice(v.as_encoded_bytes());
|
||||
CString::new(b)
|
||||
})
|
||||
.collect::<Result<_, _>>()?,
|
||||
};
|
||||
Ok(Prepared { path, argv, envp })
|
||||
}
|
||||
|
||||
/// Start the subprocess. This returns immediately once the child is
|
||||
/// started.
|
||||
///
|
||||
/// After this is successful, `self.pid` is available.
|
||||
pub fn start(&mut self) -> Result<(), StartError> {
|
||||
// IMPORTANT: do all allocation prior to the fork().
|
||||
let prep = self.prepare()?;
|
||||
let cwd = self.cwd.as_deref().and_then(|c| CString::new(c).ok());
|
||||
|
||||
let mut argv_ptrs: Vec<*const c_char> = prep.argv.iter().map(|c| c.as_ptr()).collect();
|
||||
argv_ptrs.push(std::ptr::null());
|
||||
let mut envp_ptrs: Vec<*const c_char> = prep.envp.iter().map(|c| c.as_ptr()).collect();
|
||||
envp_ptrs.push(std::ptr::null());
|
||||
|
||||
let pid = fork().map_err(StartError::Fork)?;
|
||||
if pid != 0 {
|
||||
// Parent, return immediately.
|
||||
self.pid = Some(pid);
|
||||
if let Some(f) = &self.rt_post_fork {
|
||||
f(self)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// We are the child. This never returns.
|
||||
self.run_child(&prep, &argv_ptrs, &envp_ptrs, cwd.as_deref())
|
||||
}
|
||||
|
||||
/// The child side of `start`: set up fds, chdir, rlimits, hooks, exec.
|
||||
fn run_child(
|
||||
&self,
|
||||
prep: &Prepared,
|
||||
argv: &[*const c_char],
|
||||
envp: &[*const c_char],
|
||||
cwd: Option<&std::ffi::CStr>,
|
||||
) -> ! {
|
||||
// Setup our file descriptors for std streams. Any failure here is
|
||||
// equivalent to Zig's `ExecFailedInChild` -> the child exits 1.
|
||||
if let Some(fd) = &self.stdin
|
||||
&& setup_fd(|f| stdio::dup2_stdin(f), fd).is_err()
|
||||
{
|
||||
exit_immediately(1);
|
||||
}
|
||||
if let Some(fd) = &self.stdout
|
||||
&& setup_fd(|f| stdio::dup2_stdout(f), fd).is_err()
|
||||
{
|
||||
exit_immediately(1);
|
||||
}
|
||||
if let Some(fd) = &self.stderr
|
||||
&& setup_fd(|f| stdio::dup2_stderr(f), fd).is_err()
|
||||
{
|
||||
exit_immediately(1);
|
||||
}
|
||||
|
||||
// Setup our working directory. This can fail if we don't have
|
||||
// permission to go to this directory or if due to race conditions it
|
||||
// doesn't exist or any various other reasons. We don't want to crash
|
||||
// the entire process if this fails so we ignore it. We don't log
|
||||
// because that'll show up in the output.
|
||||
if let Some(cwd) = cwd {
|
||||
let _ = process::chdir(cwd);
|
||||
}
|
||||
|
||||
// Restore any rlimits that were set by Ghostty. This might fail but
|
||||
// any failures are ignored (its best effort).
|
||||
if let Some(lim) = self.restore_rlimit {
|
||||
ghostty_os::restore_max_files(lim);
|
||||
}
|
||||
|
||||
// If there are pre exec callbacks, call them now.
|
||||
for hook in [&self.os_pre_exec, &self.rt_pre_exec].into_iter().flatten() {
|
||||
match catch_unwind(AssertUnwindSafe(hook)) {
|
||||
Ok(Some(code)) => exit_immediately(code),
|
||||
Ok(None) => {}
|
||||
Err(_) => exit_immediately(1),
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, replace our process.
|
||||
// Note: we must use the "p"-variant of exec here because we do not
|
||||
// guarantee our command is looked up already in the path.
|
||||
let err = execvpe(prep, argv, envp);
|
||||
|
||||
// If we are executing this code, the exec failed. We're in the child
|
||||
// process so there isn't much we can do. We try to output something
|
||||
// reasonable, then exit (Zig returns ExecFailedInChild which the
|
||||
// caller turns into exit(1)).
|
||||
if err == Errno::NOENT {
|
||||
write_stderr(
|
||||
b"Requested executable not found. Please verify the command is on\n\
|
||||
the PATH and try again.\n",
|
||||
);
|
||||
} else {
|
||||
let mut buf = [0u8; 96];
|
||||
let mut n = 0;
|
||||
let prefix = b"exec syscall failed with unexpected error: errno=";
|
||||
buf[..prefix.len()].copy_from_slice(prefix);
|
||||
n += prefix.len();
|
||||
n += format_u32(err.raw_os_error() as u32, &mut buf[n..]);
|
||||
buf[n] = b'\n';
|
||||
n += 1;
|
||||
write_stderr(&buf[..n]);
|
||||
}
|
||||
exit_immediately(1)
|
||||
}
|
||||
|
||||
/// Wait for the command to exit and return information about how it
|
||||
/// exited. With `block == false` this still loops until the child has a
|
||||
/// status to report (see the comment in the body).
|
||||
pub fn wait(&self, block: bool) -> Result<Exit, Errno> {
|
||||
let raw = self.pid.expect("wait called before start");
|
||||
// SAFETY of unwrap-free construction: pids are nonzero after fork.
|
||||
let pid = process::Pid::from_raw(raw).ok_or(Errno::SRCH)?;
|
||||
let opts = if block {
|
||||
WaitOptions::empty()
|
||||
} else {
|
||||
// We specify NOHANG because its not our fault if the process we
|
||||
// launch for the tty doesn't properly waitpid its children. We
|
||||
// don't want to hang the terminal over it. When NOHANG is
|
||||
// specified, waitpid returns "nothing" if the process doesn't
|
||||
// have a status to report. When that happens, it is as though the
|
||||
// wait call has not been performed, so we need to keep trying
|
||||
// until we get a status, otherwise we end up with zombie
|
||||
// processes.
|
||||
WaitOptions::NOHANG
|
||||
};
|
||||
loop {
|
||||
match process::waitpid(Some(pid), opts) {
|
||||
Ok(Some((_pid, status))) => return Ok(Exit::init(status.as_raw() as u32)),
|
||||
Ok(None) => std::thread::yield_now(),
|
||||
Err(Errno::INTR) => {}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_fd(dup: impl Fn(&OwnedFd) -> rustix::io::Result<()>, src: &OwnedFd) -> Result<(), Errno> {
|
||||
// dup2 (like Zig's dup3 with flags=0) leaves the target without
|
||||
// CLOEXEC: we do NOT want this file descriptor to be closed on exec.
|
||||
loop {
|
||||
match dup(src) {
|
||||
Err(Errno::INTR) => {}
|
||||
r => return r,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Exit the current (forked child) process immediately without running
|
||||
/// destructors or atexit handlers (`_exit`). Use this from `os_pre_exec` /
|
||||
/// `rt_pre_exec` hooks instead of `std::process::exit`.
|
||||
#[allow(unsafe_code)]
|
||||
pub fn exit_immediately(code: u8) -> ! {
|
||||
// SAFETY: _exit is async-signal-safe and has no preconditions.
|
||||
unsafe { libc::_exit(i32::from(code)) }
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
fn fork() -> Result<i32, Errno> {
|
||||
// SAFETY: the child branch (in `Command::start`) only performs
|
||||
// async-signal-safe operations on memory allocated before the fork and
|
||||
// then execs or `_exit`s; it never returns into Rust code that could
|
||||
// allocate or take locks.
|
||||
let pid = unsafe { libc::fork() };
|
||||
if pid < 0 {
|
||||
Err(Errno::from_raw_os_error(
|
||||
std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
|
||||
))
|
||||
} else {
|
||||
Ok(pid)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
fn execvpe(prep: &Prepared, argv: &[*const c_char], envp: &[*const c_char]) -> Errno {
|
||||
debug_assert!(argv.last().is_some_and(|p| p.is_null()));
|
||||
debug_assert!(envp.last().is_some_and(|p| p.is_null()));
|
||||
// SAFETY: path is a valid NUL-terminated string; argv/envp are
|
||||
// NULL-terminated arrays of pointers into CStrings (owned by `prep`) that
|
||||
// outlive the call. execvpe only returns on failure.
|
||||
unsafe {
|
||||
libc::execvpe(prep.path.as_ptr(), argv.as_ptr(), envp.as_ptr());
|
||||
}
|
||||
Errno::from_raw_os_error(errno_now())
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
fn errno_now() -> i32 {
|
||||
// SAFETY: __errno_location returns a valid thread-local pointer.
|
||||
unsafe { *libc::__errno_location() }
|
||||
}
|
||||
|
||||
fn write_stderr(msg: &[u8]) {
|
||||
// Best effort, no allocation.
|
||||
let _ = rustix::io::write(borrow_stderr(), msg);
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
fn borrow_stderr() -> rustix::fd::BorrowedFd<'static> {
|
||||
// SAFETY: STDERR_FILENO is valid for the life of the process (or the
|
||||
// write simply fails with EBADF).
|
||||
unsafe { rustix::fd::BorrowedFd::borrow_raw(libc::STDERR_FILENO) }
|
||||
}
|
||||
|
||||
/// Format `v` in decimal into `out`, returning the number of bytes written.
|
||||
fn format_u32(mut v: u32, out: &mut [u8]) -> usize {
|
||||
let mut tmp = [0u8; 10];
|
||||
let mut i = tmp.len();
|
||||
loop {
|
||||
i -= 1;
|
||||
tmp[i] = b'0' + (v % 10) as u8;
|
||||
v /= 10;
|
||||
if v == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let n = tmp.len() - i;
|
||||
out[..n].copy_from_slice(&tmp[i..]);
|
||||
n
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Linux cgroup transitions (apprt/gtk/pre_exec.zig, post_fork.zig, cgroup.zig)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// The name of the transient systemd scope for the given child pid
|
||||
/// (`apprt/gtk/cgroup.zig` `fmtScope`).
|
||||
pub fn fmt_scope(pid: u32) -> String {
|
||||
format!("app-ghostty-surface-transient-{pid}.scope")
|
||||
}
|
||||
|
||||
/// Wait for the current process' cgroup to end in `expected`. Returns true if
|
||||
/// seen, false on timeout (250ms).
|
||||
fn wait_for_cgroup(pid: u32, expected: &str) -> bool {
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
if start.elapsed() > Duration::from_millis(250) {
|
||||
return false;
|
||||
}
|
||||
if let Some(raw) = ghostty_os::cgroup::current(pid)
|
||||
&& let Some(idx) = raw.rfind('/')
|
||||
&& &raw[idx + 1..] == expected
|
||||
{
|
||||
return true;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(25));
|
||||
}
|
||||
}
|
||||
|
||||
/// If we are expecting to be moved to a transient systemd scope, wait to see
|
||||
/// if that happens by checking for the correct name of the current cgroup.
|
||||
/// Wait at most 250ms so that we don't overly delay the soft-fail scenario.
|
||||
///
|
||||
/// If we are configured to hard fail, log an error message and return an
|
||||
/// exit code if we don't detect the move in time.
|
||||
///
|
||||
/// Intended as an `rt_pre_exec` hook (runs in the child; note that, like the
|
||||
/// Zig code, it allocates and sleeps).
|
||||
pub fn linux_cgroup_pre_exec(info: &RtPreExecInfo) -> Option<u8> {
|
||||
match info.linux_cgroup {
|
||||
LinuxCgroup::Always => {}
|
||||
LinuxCgroup::Never => return None,
|
||||
LinuxCgroup::SingleInstance => match info.gtk_single_instance {
|
||||
GtkSingleInstance::True => {}
|
||||
GtkSingleInstance::False => return None,
|
||||
GtkSingleInstance::Detect => {
|
||||
log::error!("gtk-single-instance is set to detect");
|
||||
return Some(127);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
let pid = process::getpid().as_raw_nonzero().get() as u32;
|
||||
let expected = fmt_scope(pid);
|
||||
|
||||
if !wait_for_cgroup(pid, &expected) && info.linux_cgroup_hard_fail {
|
||||
log::error!("transition to new transient systemd scope took too long");
|
||||
return Some(127);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// If we are configured to do so, tell `systemd` to move the new child PID
|
||||
/// into a transient `systemd` scope with the configured resource limits.
|
||||
///
|
||||
/// `create_scope(pid, memory_high, tasks_max)` performs the D-Bus call
|
||||
/// (`cgroup.createScope`); it returns `None` if there is no D-Bus connection
|
||||
/// and `Some(Err(msg))` on failure.
|
||||
/// TODO(integration): wire to the apprt-gtk D-Bus connection.
|
||||
pub fn linux_cgroup_post_fork(
|
||||
cmd: &Command,
|
||||
info: &RtPostForkInfo,
|
||||
create_scope: impl FnOnce(u32, Option<u64>, Option<u64>) -> Option<Result<(), String>>,
|
||||
) -> Result<(), PostForkError> {
|
||||
match info.linux_cgroup {
|
||||
LinuxCgroup::Always => {}
|
||||
LinuxCgroup::Never => return Ok(()),
|
||||
LinuxCgroup::SingleInstance => match info.gtk_single_instance {
|
||||
GtkSingleInstance::True => {}
|
||||
GtkSingleInstance::False => return Ok(()),
|
||||
GtkSingleInstance::Detect => {
|
||||
log::error!("gtk-single-instance is set to detect which should be impossible!");
|
||||
return Err(PostForkError);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
let Some(pid) = cmd.pid else {
|
||||
log::error!("PID of child not known!");
|
||||
return Err(PostForkError);
|
||||
};
|
||||
let pid = pid as u32;
|
||||
let expected = fmt_scope(pid);
|
||||
log::debug!("beginning transition to transient systemd scope {expected}");
|
||||
|
||||
match create_scope(
|
||||
pid,
|
||||
info.linux_cgroup_memory_limit,
|
||||
info.linux_cgroup_processes_limit,
|
||||
) {
|
||||
None => {
|
||||
if info.linux_cgroup_hard_fail {
|
||||
log::error!("dbus connection required for cgroup isolation, exiting");
|
||||
return Err(PostForkError);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
if info.linux_cgroup_hard_fail {
|
||||
log::error!("unable to create transient systemd scope {expected}: {err}");
|
||||
return Err(PostForkError);
|
||||
}
|
||||
log::warn!("unable to create transient systemd scope {expected}: {err}");
|
||||
return Ok(());
|
||||
}
|
||||
Some(Ok(())) => {}
|
||||
}
|
||||
|
||||
if wait_for_cgroup(pid, &expected) {
|
||||
log::debug!("transition to transient systemd scope {expected} complete");
|
||||
} else {
|
||||
if info.linux_cgroup_hard_fail {
|
||||
log::error!("transition to new transient systemd scope {expected} took too long");
|
||||
return Err(PostForkError);
|
||||
}
|
||||
log::warn!("transition to transient systemd scope {expected} took too long");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Read, Seek, SeekFrom},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use std::os::fd::AsRawFd;
|
||||
|
||||
use ghostty_os::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn test_file(td: &TempDir, name: &str) -> File {
|
||||
std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(td.path().join(name))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn read_all(mut f: File) -> String {
|
||||
f.seek(SeekFrom::Start(0)).unwrap();
|
||||
let mut s = String::new();
|
||||
f.read_to_string(&mut s).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn sh(script: &str) -> Command {
|
||||
Command::new("/bin/sh", &["/bin/sh", "-c", script])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_null_delimited_env_map() {
|
||||
// The Zig test checks the null-delimited env block; here the
|
||||
// equivalent is `prepare()`.
|
||||
let mut cmd = Command::new("/bin/sh", &[]);
|
||||
cmd.env = Some(
|
||||
[
|
||||
("HOME", "/home/ifreund"),
|
||||
("WAYLAND_DISPLAY", "wayland-1"),
|
||||
("DISPLAY", ":1"),
|
||||
("DEBUGINFOD_URLS", " "),
|
||||
("XCURSOR_SIZE", "24"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_owned(), v.to_owned()))
|
||||
.collect(),
|
||||
);
|
||||
let prep = cmd.prepare().unwrap();
|
||||
assert_eq!(prep.envp.len(), 5);
|
||||
for target in [
|
||||
"HOME=/home/ifreund",
|
||||
"WAYLAND_DISPLAY=wayland-1",
|
||||
"DISPLAY=:1",
|
||||
"DEBUGINFOD_URLS= ",
|
||||
"XCURSOR_SIZE=24",
|
||||
] {
|
||||
assert!(prep.envp.iter().any(|v| v.to_str().unwrap() == target));
|
||||
}
|
||||
// Empty args fall back to path as args[0].
|
||||
assert_eq!(prep.argv.len(), 1);
|
||||
}
|
||||
|
||||
fn pre_exec_exit_test(cmd: &mut Command) {
|
||||
cmd.start().unwrap();
|
||||
assert!(cmd.pid.is_some());
|
||||
assert_eq!(cmd.wait(true).unwrap(), Exit::Exited(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn os_pre_exec_1() {
|
||||
let mut cmd = Command::new("/bin/sh", &["/bin/sh", "-v"]);
|
||||
// This runs in the child, so we can exit and it won't kill the test
|
||||
// runner.
|
||||
cmd.os_pre_exec = Some(Box::new(|| exit_immediately(42)));
|
||||
pre_exec_exit_test(&mut cmd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn os_pre_exec_2() {
|
||||
let mut cmd = Command::new("/bin/sh", &["/bin/sh", "-v"]);
|
||||
cmd.os_pre_exec = Some(Box::new(|| Some(42)));
|
||||
pre_exec_exit_test(&mut cmd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rt_pre_exec_1() {
|
||||
let mut cmd = Command::new("/bin/sh", &["/bin/sh", "-v"]);
|
||||
cmd.rt_pre_exec = Some(Box::new(|| exit_immediately(42)));
|
||||
pre_exec_exit_test(&mut cmd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rt_pre_exec_2() {
|
||||
let mut cmd = Command::new("/bin/sh", &["/bin/sh", "-v"]);
|
||||
cmd.rt_pre_exec = Some(Box::new(|| Some(42)));
|
||||
pre_exec_exit_test(&mut cmd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rt_post_fork_1() {
|
||||
let mut cmd = sh("sleep 1");
|
||||
cmd.rt_post_fork = Some(Box::new(|_| Err(PostForkError)));
|
||||
assert!(matches!(
|
||||
cmd.start(),
|
||||
Err(StartError::PostFork(PostForkError))
|
||||
));
|
||||
// Reap the child that was started anyway (like the Zig test leaks it).
|
||||
if let Some(pid) = cmd.pid.and_then(process::Pid::from_raw) {
|
||||
let _ = process::kill_process(pid, process::Signal::KILL);
|
||||
}
|
||||
let _ = cmd.wait(true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redirect_stdout_to_file() {
|
||||
let td = TempDir::init().unwrap();
|
||||
let stdout = test_file(&td, "stdout.txt");
|
||||
|
||||
let mut cmd = sh("echo hello");
|
||||
cmd.stdout = Some(stdout.try_clone().unwrap().into());
|
||||
cmd.start().unwrap();
|
||||
assert!(cmd.pid.is_some());
|
||||
assert_eq!(cmd.wait(true).unwrap(), Exit::Exited(0));
|
||||
|
||||
assert!(!read_all(stdout).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_env_vars() {
|
||||
let td = TempDir::init().unwrap();
|
||||
let stdout = test_file(&td, "stdout.txt");
|
||||
|
||||
let mut cmd = sh("echo $VALUE");
|
||||
cmd.stdout = Some(stdout.try_clone().unwrap().into());
|
||||
cmd.env = Some(HashMap::from([("VALUE".to_owned(), "hello".to_owned())]));
|
||||
cmd.start().unwrap();
|
||||
assert_eq!(cmd.wait(true).unwrap(), Exit::Exited(0));
|
||||
|
||||
assert_eq!("hello\n", read_all(stdout));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_working_directory() {
|
||||
let td = TempDir::init().unwrap();
|
||||
let stdout = test_file(&td, "stdout.txt");
|
||||
|
||||
let mut cmd = sh("pwd");
|
||||
cmd.stdout = Some(stdout.try_clone().unwrap().into());
|
||||
cmd.cwd = Some("/tmp".to_owned());
|
||||
cmd.start().unwrap();
|
||||
assert_eq!(cmd.wait(true).unwrap(), Exit::Exited(0));
|
||||
|
||||
assert_eq!("/tmp\n", read_all(stdout));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_cwd_is_ignored() {
|
||||
let td = TempDir::init().unwrap();
|
||||
let stdout = test_file(&td, "stdout.txt");
|
||||
|
||||
let mut cmd = sh("echo ok");
|
||||
cmd.stdout = Some(stdout.try_clone().unwrap().into());
|
||||
cmd.cwd = Some("/definitely/not/a/dir".to_owned());
|
||||
cmd.start().unwrap();
|
||||
assert_eq!(cmd.wait(true).unwrap(), Exit::Exited(0));
|
||||
assert_eq!("ok\n", read_all(stdout));
|
||||
}
|
||||
|
||||
// Validate an exec failure correctly terminates the child (in Zig:
|
||||
// error.ExecFailedInChild must be handled or a second copy of the process
|
||||
// keeps running; here the child can never return into the caller).
|
||||
#[test]
|
||||
fn posix_fork_handles_execve_failure() {
|
||||
let td = TempDir::init().unwrap();
|
||||
let stdout = test_file(&td, "stdout.txt");
|
||||
let stderr = test_file(&td, "stderr.txt");
|
||||
|
||||
let mut cmd = Command::new("/not/a/binary", &["/not/a/binary", ""]);
|
||||
cmd.stdout = Some(stdout.try_clone().unwrap().into());
|
||||
cmd.stderr = Some(stderr.try_clone().unwrap().into());
|
||||
cmd.cwd = Some("/bin".to_owned());
|
||||
cmd.start().unwrap();
|
||||
assert!(cmd.pid.is_some());
|
||||
assert_eq!(cmd.wait(true).unwrap(), Exit::Exited(1));
|
||||
assert!(read_all(stderr).starts_with("Requested executable not found."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wait_nonblocking_eventually_reports() {
|
||||
let mut cmd = sh("exit 3");
|
||||
cmd.start().unwrap();
|
||||
assert_eq!(cmd.wait(false).unwrap(), Exit::Exited(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_signal_and_decode() {
|
||||
let mut cmd = sh("kill -TERM $$");
|
||||
cmd.start().unwrap();
|
||||
assert_eq!(cmd.wait(true).unwrap(), Exit::Signal(15));
|
||||
|
||||
assert_eq!(Exit::init(0), Exit::Exited(0));
|
||||
assert_eq!(Exit::init(42 << 8), Exit::Exited(42));
|
||||
assert_eq!(Exit::init(9), Exit::Signal(9));
|
||||
assert_eq!(Exit::init((19 << 8) | 0x7f), Exit::Stopped(19));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_fork_sees_pid() {
|
||||
let seen = Arc::new(std::sync::Mutex::new(None));
|
||||
let seen2 = Arc::clone(&seen);
|
||||
let mut cmd = sh("true");
|
||||
cmd.rt_post_fork = Some(Box::new(move |c| {
|
||||
*seen2.lock().unwrap() = c.pid;
|
||||
Ok(())
|
||||
}));
|
||||
cmd.start().unwrap();
|
||||
assert_eq!(*seen.lock().unwrap(), cmd.pid);
|
||||
cmd.wait(true).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stdio_fds_are_not_cloexec_in_child_and_closed_others() {
|
||||
// The master end of a pty is CLOEXEC so the child must not see it.
|
||||
let pty = crate::pty::Pty::open(crate::pty::Winsize::default()).unwrap();
|
||||
let master = pty.master.as_raw_fd();
|
||||
let td = TempDir::init().unwrap();
|
||||
let stdout = test_file(&td, "stdout.txt");
|
||||
let mut cmd = sh(&format!(
|
||||
"[ -e /proc/self/fd/{master} ] && echo leaked || echo ok"
|
||||
));
|
||||
cmd.stdout = Some(stdout.try_clone().unwrap().into());
|
||||
cmd.start().unwrap();
|
||||
cmd.wait(true).unwrap();
|
||||
assert_eq!("ok\n", read_all(stdout));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cgroup_pre_exec_modes() {
|
||||
let never = RtPreExecInfo {
|
||||
linux_cgroup: LinuxCgroup::Never,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(linux_cgroup_pre_exec(&never), None);
|
||||
let detect = RtPreExecInfo::default();
|
||||
assert_eq!(linux_cgroup_pre_exec(&detect), Some(127));
|
||||
let single_false = RtPreExecInfo {
|
||||
gtk_single_instance: GtkSingleInstance::False,
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(linux_cgroup_pre_exec(&single_false), None);
|
||||
assert_eq!(fmt_scope(1234), "app-ghostty-surface-transient-1234.scope");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cgroup_post_fork_modes() {
|
||||
let mut cmd = sh("true");
|
||||
cmd.pid = Some(1);
|
||||
let never = RtPostForkInfo {
|
||||
linux_cgroup: LinuxCgroup::Never,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(linux_cgroup_post_fork(&cmd, &never, |_, _, _| unreachable!()).is_ok());
|
||||
let detect = RtPostForkInfo::default();
|
||||
assert!(linux_cgroup_post_fork(&cmd, &detect, |_, _, _| unreachable!()).is_err());
|
||||
// No dbus: soft fail ok, hard fail err.
|
||||
let always = RtPostForkInfo {
|
||||
linux_cgroup: LinuxCgroup::Always,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(linux_cgroup_post_fork(&cmd, &always, |_, _, _| None).is_ok());
|
||||
let hard = RtPostForkInfo {
|
||||
linux_cgroup_hard_fail: true,
|
||||
..always
|
||||
};
|
||||
assert!(linux_cgroup_post_fork(&cmd, &hard, |_, _, _| None).is_err());
|
||||
assert!(linux_cgroup_post_fork(&cmd, &hard, |_, _, _| Some(Err("x".into()))).is_err());
|
||||
}
|
||||
}
|
||||
@@ -1 +1,25 @@
|
||||
//! ghostty-termio: see docs/rewrite/ARCHITECTURE.md for the module map.
|
||||
//!
|
||||
//! Ported so far (Linux only): `pty.zig`, `Command.zig`,
|
||||
//! `termio/shell_integration.zig`, the `terminfo/*` source generator.
|
||||
//! See `docs/rewrite/notes/ghostty-termio-IO1.md`.
|
||||
//!
|
||||
//! This crate may use limited `unsafe` (fork/exec in `command`, a few
|
||||
//! async-signal-safe libc calls in `pty`); everything else is safe.
|
||||
|
||||
#![cfg_attr(
|
||||
test,
|
||||
allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::assert_is_empty,
|
||||
clippy::items_after_statements
|
||||
)
|
||||
)]
|
||||
|
||||
pub mod command;
|
||||
pub mod pty;
|
||||
pub mod shell_integration;
|
||||
pub mod terminfo;
|
||||
|
||||
pub use command::Command;
|
||||
pub use pty::{Pty, Winsize};
|
||||
@@ -0,0 +1,303 @@
|
||||
//! Linux PTY creation and management. This is just a thin layer on top
|
||||
//! of Linux syscalls. Port of `pty.zig` (POSIX variant only; the Windows
|
||||
//! and null variants are out of scope).
|
||||
|
||||
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
|
||||
|
||||
use rustix::{fs, process, pty, termios};
|
||||
|
||||
/// Redeclare the winsize struct so we can just use a Rust struct. The
|
||||
/// defaults on this are some reasonable screen size but you should probably
|
||||
/// not use them.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Winsize {
|
||||
pub ws_row: u16,
|
||||
pub ws_col: u16,
|
||||
pub ws_xpixel: u16,
|
||||
pub ws_ypixel: u16,
|
||||
}
|
||||
|
||||
impl Default for Winsize {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ws_row: 100,
|
||||
ws_col: 80,
|
||||
ws_xpixel: 800,
|
||||
ws_ypixel: 600,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Winsize> for termios::Winsize {
|
||||
fn from(w: Winsize) -> Self {
|
||||
Self {
|
||||
ws_row: w.ws_row,
|
||||
ws_col: w.ws_col,
|
||||
ws_xpixel: w.ws_xpixel,
|
||||
ws_ypixel: w.ws_ypixel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<termios::Winsize> for Winsize {
|
||||
fn from(w: termios::Winsize) -> Self {
|
||||
Self {
|
||||
ws_row: w.ws_row,
|
||||
ws_col: w.ws_col,
|
||||
ws_xpixel: w.ws_xpixel,
|
||||
ws_ypixel: w.ws_ypixel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The modes of a pty. Not all of these modes are supported on
|
||||
/// all platforms but all platforms share the same mode struct.
|
||||
///
|
||||
/// The default values of fields in this struct are set to the
|
||||
/// most typical values for a pty.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Mode {
|
||||
/// ICANON on POSIX
|
||||
pub canonical: bool,
|
||||
|
||||
/// ECHO on POSIX
|
||||
pub echo: bool,
|
||||
}
|
||||
|
||||
impl Default for Mode {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
canonical: true,
|
||||
echo: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
#[error("openpty failed")]
|
||||
OpenptyFailed,
|
||||
#[error("get mode failed")]
|
||||
GetModeFailed,
|
||||
#[error("ioctl failed")]
|
||||
IoctlFailed,
|
||||
#[error("operation not supported")]
|
||||
OperationNotSupported,
|
||||
#[error("setsid failed")]
|
||||
ProcessGroupFailed,
|
||||
#[error("setting the controlling terminal failed")]
|
||||
SetControllingTerminalFailed,
|
||||
}
|
||||
|
||||
/// A pty master/slave pair.
|
||||
///
|
||||
/// Deviation from Zig: both fds are `OwnedFd` and are closed on drop (the Zig
|
||||
/// struct never closes the slave automatically). To close the slave early
|
||||
/// (as Exec does after the fork) use [`Pty::close_slave`] or destructure.
|
||||
#[derive(Debug)]
|
||||
pub struct Pty {
|
||||
pub master: OwnedFd,
|
||||
/// `None` once closed with [`Pty::close_slave`].
|
||||
pub slave: Option<OwnedFd>,
|
||||
}
|
||||
|
||||
impl Pty {
|
||||
/// Open a new PTY with the given initial size.
|
||||
pub fn open(size: Winsize) -> Result<Pty, Error> {
|
||||
// Equivalent of glibc openpty(): the master is CLOEXEC (Ghostty sets
|
||||
// this explicitly: only the slave fd should be inherited by the
|
||||
// child), the slave is not.
|
||||
let master = pty::openpt(
|
||||
pty::OpenptFlags::RDWR | pty::OpenptFlags::NOCTTY | pty::OpenptFlags::CLOEXEC,
|
||||
)
|
||||
.map_err(|_| Error::OpenptyFailed)?;
|
||||
pty::grantpt(&master).map_err(|_| Error::OpenptyFailed)?;
|
||||
pty::unlockpt(&master).map_err(|_| Error::OpenptyFailed)?;
|
||||
let name = pty::ptsname(&master, Vec::new()).map_err(|_| Error::OpenptyFailed)?;
|
||||
let slave = fs::open(
|
||||
name.as_c_str(),
|
||||
fs::OFlags::RDWR | fs::OFlags::NOCTTY,
|
||||
fs::Mode::empty(),
|
||||
)
|
||||
.map_err(|_| Error::OpenptyFailed)?;
|
||||
|
||||
termios::tcsetwinsize(&master, size.into()).map_err(|_| Error::OpenptyFailed)?;
|
||||
|
||||
// Enable UTF-8 mode. I think this is on by default on Linux but it
|
||||
// is NOT on by default on macOS so we ensure that it is always set.
|
||||
let mut attrs = termios::tcgetattr(&master).map_err(|_| Error::OpenptyFailed)?;
|
||||
attrs.input_modes.insert(termios::InputModes::IUTF8);
|
||||
termios::tcsetattr(&master, termios::OptionalActions::Now, &attrs)
|
||||
.map_err(|_| Error::OpenptyFailed)?;
|
||||
|
||||
Ok(Pty {
|
||||
master,
|
||||
slave: Some(slave),
|
||||
})
|
||||
}
|
||||
|
||||
/// Close the slave side (the parent does this after forking the child).
|
||||
pub fn close_slave(&mut self) {
|
||||
self.slave = None;
|
||||
}
|
||||
|
||||
/// Get the modes of the pty (read from the master).
|
||||
pub fn get_mode(&self) -> Result<Mode, Error> {
|
||||
let attrs = termios::tcgetattr(&self.master).map_err(|_| Error::GetModeFailed)?;
|
||||
Ok(Mode {
|
||||
canonical: attrs.local_modes.contains(termios::LocalModes::ICANON),
|
||||
echo: attrs.local_modes.contains(termios::LocalModes::ECHO),
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the size of the pty.
|
||||
pub fn get_size(&self) -> Result<Winsize, Error> {
|
||||
termios::tcgetwinsize(&self.master)
|
||||
.map(Into::into)
|
||||
.map_err(|_| Error::IoctlFailed)
|
||||
}
|
||||
|
||||
/// Set the size of the pty.
|
||||
pub fn set_size(&mut self, size: Winsize) -> Result<(), Error> {
|
||||
termios::tcsetwinsize(&self.master, size.into()).map_err(|_| Error::IoctlFailed)
|
||||
}
|
||||
|
||||
/// Raw fds for use in the child's pre-exec hook (see [`child_pre_exec`]).
|
||||
pub fn raw_fds(&self) -> (RawFd, Option<RawFd>) {
|
||||
(
|
||||
self.master.as_raw_fd(),
|
||||
self.slave.as_ref().map(AsRawFd::as_raw_fd),
|
||||
)
|
||||
}
|
||||
|
||||
/// This should be called prior to exec in the forked child process
|
||||
/// in order to setup the tty properly. See [`child_pre_exec`].
|
||||
pub fn child_pre_exec(&self) -> Result<(), Error> {
|
||||
let (master, slave) = self.raw_fds();
|
||||
child_pre_exec(master, slave.unwrap_or(-1))
|
||||
}
|
||||
}
|
||||
|
||||
/// Signals reset to their default disposition in the child.
|
||||
const RESET_SIGNALS: [nix::sys::signal::Signal; 13] = {
|
||||
use nix::sys::signal::Signal as S;
|
||||
[
|
||||
S::SIGABRT,
|
||||
S::SIGALRM,
|
||||
S::SIGBUS,
|
||||
S::SIGCHLD,
|
||||
S::SIGFPE,
|
||||
S::SIGHUP,
|
||||
S::SIGILL,
|
||||
S::SIGINT,
|
||||
S::SIGPIPE,
|
||||
S::SIGSEGV,
|
||||
S::SIGTRAP,
|
||||
S::SIGTERM,
|
||||
S::SIGQUIT,
|
||||
]
|
||||
};
|
||||
|
||||
/// Child-side setup of the tty: reset signals, `setsid`, `TIOCSCTTY` on the
|
||||
/// slave and close the master/slave fds. Operates on raw fd numbers so it can
|
||||
/// be used from a post-fork hook (`Command::os_pre_exec`) without touching
|
||||
/// any `OwnedFd` destructors. Everything in here is async-signal-safe (no
|
||||
/// allocation, no locks).
|
||||
///
|
||||
/// A negative `slave` skips the controlling-terminal step and the close.
|
||||
pub fn child_pre_exec(master: RawFd, slave: RawFd) -> Result<(), Error> {
|
||||
use nix::sys::signal::{SaFlags, SigAction, SigHandler, SigSet};
|
||||
|
||||
// Reset our signals
|
||||
let sa = SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty());
|
||||
for sig in RESET_SIGNALS {
|
||||
reset_signal(sig, &sa);
|
||||
}
|
||||
|
||||
// Create a new process group
|
||||
process::setsid().map_err(|_| Error::ProcessGroupFailed)?;
|
||||
|
||||
if slave >= 0 {
|
||||
// Set controlling terminal
|
||||
set_controlling_terminal(slave)?;
|
||||
}
|
||||
|
||||
// Can close master/slave pair now
|
||||
close_raw(slave);
|
||||
close_raw(master);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
fn reset_signal(sig: nix::sys::signal::Signal, sa: &nix::sys::signal::SigAction) {
|
||||
// SAFETY: sigaction(2) is async-signal-safe and SIG_DFL installs no
|
||||
// Rust handler code.
|
||||
let _ = unsafe { nix::sys::signal::sigaction(sig, sa) };
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
fn set_controlling_terminal(slave: RawFd) -> Result<(), Error> {
|
||||
use std::os::fd::BorrowedFd;
|
||||
// SAFETY: the fd is owned by the caller and stays open for the duration of
|
||||
// this call; we only borrow it for one ioctl.
|
||||
let fd = unsafe { BorrowedFd::borrow_raw(slave) };
|
||||
process::ioctl_tiocsctty(fd).map_err(|err| {
|
||||
log::error!("error setting controlling terminal errno={err:?}");
|
||||
Error::SetControllingTerminalFailed
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
fn close_raw(fd: RawFd) {
|
||||
if fd < 0 {
|
||||
return;
|
||||
}
|
||||
// SAFETY: closing a raw fd number we were told to close; in the child
|
||||
// the `OwnedFd`s that nominally own these are never dropped (the child
|
||||
// execs or `_exit`s), so there is no double close.
|
||||
unsafe { rustix::io::close(fd) };
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pty_size() {
|
||||
let mut ws = Winsize {
|
||||
ws_row: 50,
|
||||
ws_col: 80,
|
||||
ws_xpixel: 1,
|
||||
ws_ypixel: 1,
|
||||
};
|
||||
|
||||
let mut pty = Pty::open(ws).unwrap();
|
||||
|
||||
// Initialize size should match what we gave it
|
||||
assert_eq!(ws, pty.get_size().unwrap());
|
||||
|
||||
// Can set and read new sizes
|
||||
ws.ws_row *= 2;
|
||||
pty.set_size(ws).unwrap();
|
||||
assert_eq!(ws, pty.get_size().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_mode_and_utf8() {
|
||||
let pty = Pty::open(Winsize::default()).unwrap();
|
||||
let mode = pty.get_mode().unwrap();
|
||||
assert_eq!(mode, Mode::default());
|
||||
let attrs = termios::tcgetattr(&pty.master).unwrap();
|
||||
assert!(attrs.input_modes.contains(termios::InputModes::IUTF8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn master_is_cloexec_slave_is_not() {
|
||||
let pty = Pty::open(Winsize::default()).unwrap();
|
||||
let m = rustix::io::fcntl_getfd(&pty.master).unwrap();
|
||||
assert!(m.contains(rustix::io::FdFlags::CLOEXEC));
|
||||
let s = rustix::io::fcntl_getfd(pty.slave.as_ref().unwrap()).unwrap();
|
||||
assert!(!s.contains(rustix::io::FdFlags::CLOEXEC));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,893 @@
|
||||
//! Shell integration setup. Port of `termio/shell_integration.zig`.
|
||||
//!
|
||||
//! The shell scripts live in `resources/shell-integration/` at the workspace
|
||||
//! root (copied verbatim from `src/shell-integration/`).
|
||||
|
||||
use std::{collections::HashMap, path::Path};
|
||||
|
||||
use ghostty_os::{env::prepend_env, homedir, shell::ShellCommandBuilder};
|
||||
|
||||
/// The environment passed to the child (Zig `std.process.EnvMap`).
|
||||
pub type EnvMap = HashMap<String, String>;
|
||||
|
||||
/// Shell types we support
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Shell {
|
||||
Bash,
|
||||
Elvish,
|
||||
Fish,
|
||||
Nushell,
|
||||
Zsh,
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
pub const ALL: [Shell; 5] = [
|
||||
Shell::Bash,
|
||||
Shell::Elvish,
|
||||
Shell::Fish,
|
||||
Shell::Nushell,
|
||||
Shell::Zsh,
|
||||
];
|
||||
|
||||
/// The name of the shell (Zig `@tagName`), also the directory name inside
|
||||
/// `shell-integration/`.
|
||||
pub fn tag_name(self) -> &'static str {
|
||||
match self {
|
||||
Shell::Bash => "bash",
|
||||
Shell::Elvish => "elvish",
|
||||
Shell::Fish => "fish",
|
||||
Shell::Nushell => "nushell",
|
||||
Shell::Zsh => "zsh",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Local stand-in for `config.Command`.
|
||||
/// TODO(integration): replace with `ghostty_config::Command`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Command {
|
||||
/// A shell command line, split with `ArgIteratorGeneral` rules.
|
||||
Shell(String),
|
||||
/// Directly executed argv.
|
||||
Direct(Vec<String>),
|
||||
}
|
||||
|
||||
impl Command {
|
||||
/// The arguments of the command. For shell commands this is a best
|
||||
/// effort parse equivalent to Zig's `std.process.ArgIteratorGeneral(.{})`
|
||||
/// (whitespace separated, double quotes group, backslashes only special
|
||||
/// before a quote).
|
||||
pub fn arg_iterator(&self) -> std::vec::IntoIter<String> {
|
||||
match self {
|
||||
Command::Direct(v) => v.clone().into_iter(),
|
||||
Command::Shell(s) => split_args_general(s).into_iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `std.process.ArgIteratorGeneral(.{})` (Windows command-line rules).
|
||||
fn split_args_general(cmd_line: &str) -> Vec<String> {
|
||||
let b = cmd_line.as_bytes();
|
||||
let mut i = 0;
|
||||
let mut out = Vec::new();
|
||||
loop {
|
||||
// Skip whitespace
|
||||
while i < b.len() && (b[i] == b' ' || b[i] == b'\t') {
|
||||
i += 1;
|
||||
}
|
||||
if i >= b.len() {
|
||||
return out;
|
||||
}
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
let mut backslashes = 0usize;
|
||||
let mut in_quote = false;
|
||||
loop {
|
||||
let c = b.get(i).copied();
|
||||
match c {
|
||||
None => {
|
||||
buf.extend(std::iter::repeat_n(b'\\', backslashes));
|
||||
break;
|
||||
}
|
||||
Some(b' ' | b'\t') if !in_quote => {
|
||||
buf.extend(std::iter::repeat_n(b'\\', backslashes));
|
||||
i += 1;
|
||||
break;
|
||||
}
|
||||
Some(b'\\') => {
|
||||
backslashes += 1;
|
||||
i += 1;
|
||||
}
|
||||
Some(b'"') => {
|
||||
let real = backslashes.is_multiple_of(2);
|
||||
buf.extend(std::iter::repeat_n(b'\\', backslashes / 2));
|
||||
backslashes = 0;
|
||||
if real {
|
||||
in_quote = !in_quote;
|
||||
} else {
|
||||
buf.push(b'"');
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
Some(ch) => {
|
||||
buf.extend(std::iter::repeat_n(b'\\', backslashes));
|
||||
backslashes = 0;
|
||||
buf.push(ch);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push(String::from_utf8_lossy(&buf).into_owned());
|
||||
}
|
||||
}
|
||||
|
||||
/// Local stand-in for `config.ShellIntegrationFeatures` (same defaults).
|
||||
/// TODO(integration): replace with `ghostty_config::ShellIntegrationFeatures`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ShellIntegrationFeatures {
|
||||
pub cursor: bool,
|
||||
pub sudo: bool,
|
||||
pub title: bool,
|
||||
pub ssh_env: bool,
|
||||
pub ssh_terminfo: bool,
|
||||
pub path: bool,
|
||||
}
|
||||
|
||||
impl Default for ShellIntegrationFeatures {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cursor: true,
|
||||
sudo: false,
|
||||
title: true,
|
||||
ssh_env: false,
|
||||
ssh_terminfo: false,
|
||||
path: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShellIntegrationFeatures {
|
||||
/// All features disabled (`std.mem.zeroes`).
|
||||
pub const NONE: Self = Self {
|
||||
cursor: false,
|
||||
sudo: false,
|
||||
title: false,
|
||||
ssh_env: false,
|
||||
ssh_terminfo: false,
|
||||
path: false,
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("unable to determine home directory: {0}")]
|
||||
Home(#[from] homedir::Error),
|
||||
}
|
||||
|
||||
/// The result of setting up a shell integration.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ShellIntegration {
|
||||
/// The successfully-integrated shell.
|
||||
pub shell: Shell,
|
||||
|
||||
/// The command to use to start the shell with the integration.
|
||||
/// In most cases this is identical to the command given but for
|
||||
/// bash in particular it may be different.
|
||||
pub command: Command,
|
||||
}
|
||||
|
||||
/// Set up the command execution environment for automatic
|
||||
/// integrated shell integration and return a ShellIntegration
|
||||
/// struct describing the integration. If integration fails
|
||||
/// (shell type couldn't be detected, etc.), this will return None.
|
||||
pub fn setup(
|
||||
resource_dir: &str,
|
||||
command: &Command,
|
||||
env: &mut EnvMap,
|
||||
force_shell: Option<Shell>,
|
||||
) -> Result<Option<ShellIntegration>, Error> {
|
||||
let Some(shell) = force_shell.or_else(|| detect_shell(command)) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let new_command = match shell {
|
||||
Shell::Bash => setup_bash(command, resource_dir, env)?,
|
||||
Shell::Nushell => setup_nushell(command, resource_dir, env),
|
||||
Shell::Zsh => setup_zsh(command, resource_dir, env),
|
||||
Shell::Elvish | Shell::Fish => {
|
||||
if !setup_xdg_data_dirs(resource_dir, env) {
|
||||
return Ok(None);
|
||||
}
|
||||
Some(command.clone())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(new_command.map(|command| ShellIntegration { shell, command }))
|
||||
}
|
||||
|
||||
fn basename(path: &str) -> &str {
|
||||
// std.fs.path.basename: strips trailing separators.
|
||||
let trimmed = path.trim_end_matches('/');
|
||||
match trimmed.rfind('/') {
|
||||
Some(i) => &trimmed[i + 1..],
|
||||
None => trimmed,
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_shell(command: &Command) -> Option<Shell> {
|
||||
let mut it = command.arg_iterator();
|
||||
let arg0 = it.next()?;
|
||||
let exe = basename(&arg0);
|
||||
|
||||
match exe {
|
||||
// (The Darwin /bin/bash special case does not apply on Linux.)
|
||||
"bash" => Some(Shell::Bash),
|
||||
"elvish" => Some(Shell::Elvish),
|
||||
"fish" => Some(Shell::Fish),
|
||||
"nu" => Some(Shell::Nushell),
|
||||
"zsh" => Some(Shell::Zsh),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set up the shell integration features environment variable.
|
||||
pub fn setup_features(env: &mut EnvMap, features: ShellIntegrationFeatures, cursor_blink: bool) {
|
||||
// Sorted (case-insensitively) so that the output is deterministic.
|
||||
let fields: [(&str, bool); 6] = [
|
||||
("cursor", features.cursor),
|
||||
("path", features.path),
|
||||
("ssh-env", features.ssh_env),
|
||||
("ssh-terminfo", features.ssh_terminfo),
|
||||
("sudo", features.sudo),
|
||||
("title", features.title),
|
||||
];
|
||||
|
||||
let mut buf = String::new();
|
||||
for (name, enabled) in fields {
|
||||
if enabled {
|
||||
if !buf.is_empty() {
|
||||
buf.push(',');
|
||||
}
|
||||
buf.push_str(name);
|
||||
|
||||
if name == "cursor" {
|
||||
buf.push_str(if cursor_blink { ":blink" } else { ":steady" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !buf.is_empty() {
|
||||
env.insert("GHOSTTY_SHELL_FEATURES".to_owned(), buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// Setup the bash automatic shell integration. This works by
|
||||
/// starting bash in POSIX mode and using the ENV environment
|
||||
/// variable to load our bash integration script. This prevents
|
||||
/// bash from loading its normal startup files, which becomes
|
||||
/// our script's responsibility (along with disabling POSIX
|
||||
/// mode).
|
||||
///
|
||||
/// This returns a new shell command string that enables the integration
|
||||
/// or None if integration failed.
|
||||
fn setup_bash(
|
||||
command: &Command,
|
||||
resource_dir: &str,
|
||||
env: &mut EnvMap,
|
||||
) -> Result<Option<Command>, Error> {
|
||||
let mut cmd = ShellCommandBuilder::new();
|
||||
|
||||
// Iterator that yields each argument in the original command line.
|
||||
let mut iter = command.arg_iterator();
|
||||
|
||||
// Start accumulating arguments with the executable and initial flags.
|
||||
match iter.next() {
|
||||
Some(exe) => cmd.append_arg(&exe),
|
||||
None => return Ok(None),
|
||||
}
|
||||
cmd.append_arg("--posix");
|
||||
|
||||
// Stores the list of intercepted command line flags that will be passed
|
||||
// to our shell integration script: --norc --noprofile
|
||||
// We always include at least "1" so the script can differentiate between
|
||||
// being manually sourced or automatically injected (from here).
|
||||
let mut inject = String::from("1");
|
||||
|
||||
// Walk through the rest of the given arguments. If we see an option that
|
||||
// would require complex or unsupported integration behavior, we bail out
|
||||
// and skip loading our shell integration. Users can still manually source
|
||||
// the shell integration script.
|
||||
//
|
||||
// Unsupported options:
|
||||
// -c -c is always non-interactive
|
||||
// --posix POSIX mode (a la /bin/sh)
|
||||
let mut rcfile: Option<String> = None;
|
||||
while let Some(arg) = iter.next() {
|
||||
if arg == "--posix" {
|
||||
return Ok(None);
|
||||
} else if arg == "--norc" {
|
||||
inject.push_str(" --norc");
|
||||
} else if arg == "--noprofile" {
|
||||
inject.push_str(" --noprofile");
|
||||
} else if arg == "--rcfile" || arg == "--init-file" {
|
||||
rcfile = iter.next();
|
||||
} else if arg.len() > 1 && arg.starts_with('-') && !arg[1..].starts_with('-') {
|
||||
// '-c command' is always non-interactive
|
||||
if arg.contains('c') {
|
||||
return Ok(None);
|
||||
}
|
||||
cmd.append_arg(&arg);
|
||||
} else if arg == "-" || arg == "--" {
|
||||
// All remaining arguments should be passed directly to the shell
|
||||
// command. We shouldn't perform any further option processing.
|
||||
cmd.append_arg(&arg);
|
||||
for remaining in iter.by_ref() {
|
||||
cmd.append_arg(&remaining);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
cmd.append_arg(&arg);
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve an existing ENV value. We're about to overwrite it.
|
||||
if let Some(v) = env.get("ENV").cloned() {
|
||||
env.insert("GHOSTTY_BASH_ENV".to_owned(), v);
|
||||
}
|
||||
|
||||
// Set our new ENV to point to our integration script.
|
||||
let script_path = format!("{resource_dir}/shell-integration/bash/ghostty.bash");
|
||||
match std::fs::File::open(&script_path) {
|
||||
Ok(_) => {
|
||||
env.insert("ENV".to_owned(), script_path);
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("unable to open {script_path}: {err}");
|
||||
env.remove("GHOSTTY_BASH_ENV");
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
env.insert("GHOSTTY_BASH_INJECT".to_owned(), inject);
|
||||
if let Some(v) = rcfile {
|
||||
env.insert("GHOSTTY_BASH_RCFILE".to_owned(), v);
|
||||
}
|
||||
|
||||
// In POSIX mode, HISTFILE defaults to ~/.sh_history, so unless we're
|
||||
// staying in POSIX mode (--posix), change it back to ~/.bash_history.
|
||||
if !env.contains_key("HISTFILE")
|
||||
&& let Some(home) = homedir::home()?
|
||||
{
|
||||
env.insert("HISTFILE".to_owned(), format!("{home}/.bash_history"));
|
||||
env.insert("GHOSTTY_BASH_UNEXPORT_HISTFILE".to_owned(), "1".to_owned());
|
||||
}
|
||||
|
||||
// Return a copy of our modified command line to use as the shell command.
|
||||
Ok(Some(Command::Shell(cmd.written().to_owned())))
|
||||
}
|
||||
|
||||
/// Setup automatic shell integration for shells that include
|
||||
/// their modules from paths in `XDG_DATA_DIRS` env variable.
|
||||
///
|
||||
/// The shell-integration path is prepended to `XDG_DATA_DIRS`.
|
||||
/// It is also saved in the `GHOSTTY_SHELL_INTEGRATION_XDG_DIR` variable
|
||||
/// so that the shell can refer to it and safely remove this directory
|
||||
/// from `XDG_DATA_DIRS` when integration is complete.
|
||||
fn setup_xdg_data_dirs(resource_dir: &str, env: &mut EnvMap) -> bool {
|
||||
// Get our path to the shell integration directory.
|
||||
let integ_path = format!("{resource_dir}/shell-integration");
|
||||
if let Err(err) = std::fs::read_dir(&integ_path) {
|
||||
log::warn!("unable to open {integ_path}: {err}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set an env var so we can remove this from XDG_DATA_DIRS later.
|
||||
// This happens in the shell integration config itself. We do this
|
||||
// so that our modifications don't interfere with other commands.
|
||||
env.insert(
|
||||
"GHOSTTY_SHELL_INTEGRATION_XDG_DIR".to_owned(),
|
||||
integ_path.clone(),
|
||||
);
|
||||
|
||||
// If no XDG_DATA_DIRS set use the default value as specified.
|
||||
// This ensures that the default directories aren't lost by setting
|
||||
// our desired integration dir directly. See #2711.
|
||||
// <https://specifications.freedesktop.org/basedir-spec/0.6/#variables>
|
||||
let key = "XDG_DATA_DIRS";
|
||||
let current = env
|
||||
.get(key)
|
||||
.map_or("/usr/local/share:/usr/share", String::as_str);
|
||||
let new = prepend_env(current, &integ_path);
|
||||
env.insert(key.to_owned(), new);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Set up automatic Nushell shell integration. This works by adding our
|
||||
/// shell resource directory to the `XDG_DATA_DIRS` environment variable,
|
||||
/// which Nushell will use to load `nushell/vendor/autoload/ghostty.nu`.
|
||||
///
|
||||
/// We then add `--execute 'use ghostty ...'` to the nu command line to
|
||||
/// automatically enable our shell features.
|
||||
fn setup_nushell(command: &Command, resource_dir: &str, env: &mut EnvMap) -> Option<Command> {
|
||||
// Add our XDG_DATA_DIRS entry (for nushell/vendor/autoload/). This
|
||||
// makes our 'ghostty' module automatically available, even if any
|
||||
// of the later checks abort the rest of our automatic integration.
|
||||
if !setup_xdg_data_dirs(resource_dir, env) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut cmd = ShellCommandBuilder::new();
|
||||
let mut iter = command.arg_iterator();
|
||||
|
||||
// Start accumulating arguments with the executable and initial flags.
|
||||
cmd.append_arg(&iter.next()?);
|
||||
|
||||
// Tell nu to immediately "use" all of the exported functions in our
|
||||
// 'ghostty' module.
|
||||
//
|
||||
// We can consider making this more specific based on the set of
|
||||
// enabled shell features (e.g. `use ghostty sudo`). At the moment,
|
||||
// shell features are all runtime-guarded in the nushell script.
|
||||
cmd.append_arg("--execute 'use ghostty *'");
|
||||
|
||||
// Walk through the rest of the given arguments. If we see an option that
|
||||
// would require complex or unsupported integration behavior, we bail out
|
||||
// and skip loading our shell integration. Users can still manually source
|
||||
// the shell integration module.
|
||||
//
|
||||
// Unsupported options:
|
||||
// -c / --command -c is always non-interactive
|
||||
// --lsp --lsp starts the language server
|
||||
while let Some(arg) = iter.next() {
|
||||
if arg == "--command" || arg == "--lsp" {
|
||||
return None;
|
||||
} else if arg.len() > 1 && arg.starts_with('-') && !arg[1..].starts_with('-') {
|
||||
if arg.contains('c') {
|
||||
return None;
|
||||
}
|
||||
cmd.append_arg(&arg);
|
||||
} else if arg == "-" || arg == "--" {
|
||||
// All remaining arguments should be passed directly to the shell
|
||||
// command. We shouldn't perform any further option processing.
|
||||
cmd.append_arg(&arg);
|
||||
for remaining in iter.by_ref() {
|
||||
cmd.append_arg(&remaining);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
cmd.append_arg(&arg);
|
||||
}
|
||||
}
|
||||
|
||||
// Return a copy of our modified command line to use as the shell command.
|
||||
Some(Command::Shell(cmd.written().to_owned()))
|
||||
}
|
||||
|
||||
/// Setup the zsh automatic shell integration. This works by setting
|
||||
/// ZDOTDIR to our resources dir so that zsh will load our config. This
|
||||
/// config then loads the true user config.
|
||||
fn setup_zsh(command: &Command, resource_dir: &str, env: &mut EnvMap) -> Option<Command> {
|
||||
// Preserve an existing ZDOTDIR value. We're about to overwrite it.
|
||||
if let Some(old) = env.get("ZDOTDIR").cloned() {
|
||||
env.insert("GHOSTTY_ZSH_ZDOTDIR".to_owned(), old);
|
||||
}
|
||||
|
||||
// Set our new ZDOTDIR to point to our shell resource directory.
|
||||
let integ_path = format!("{resource_dir}/shell-integration/zsh");
|
||||
if !Path::new(&integ_path).is_dir() {
|
||||
// Mirrors openDirAbsolute failing (missing or not a directory).
|
||||
log::warn!("unable to open {integ_path}");
|
||||
return None;
|
||||
}
|
||||
env.insert("ZDOTDIR".to_owned(), integ_path);
|
||||
|
||||
Some(command.clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ghostty_os::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Test helper that creates a temporary resources directory with shell
|
||||
/// integration paths.
|
||||
struct TmpResourcesDir {
|
||||
_tmp: TempDir,
|
||||
path: String,
|
||||
shell_path: String,
|
||||
}
|
||||
|
||||
impl TmpResourcesDir {
|
||||
fn new(shell: Shell) -> Self {
|
||||
let tmp = TempDir::init().unwrap();
|
||||
let path = std::fs::canonicalize(tmp.path())
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let rel = format!("shell-integration/{}", shell.tag_name());
|
||||
std::fs::create_dir_all(format!("{path}/{rel}")).unwrap();
|
||||
let shell_path = format!("{path}/{rel}");
|
||||
if shell == Shell::Bash {
|
||||
std::fs::write(format!("{path}/shell-integration/bash/ghostty.bash"), "").unwrap();
|
||||
}
|
||||
Self {
|
||||
_tmp: tmp,
|
||||
path,
|
||||
shell_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sh(s: &str) -> Command {
|
||||
Command::Shell(s.to_owned())
|
||||
}
|
||||
|
||||
fn shell_str(c: Option<&Command>) -> &str {
|
||||
match c.unwrap() {
|
||||
Command::Shell(s) => s,
|
||||
Command::Direct(_) => panic!("expected shell"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_shell() {
|
||||
let mut env = EnvMap::new();
|
||||
for shell in Shell::ALL {
|
||||
let res = TmpResourcesDir::new(shell);
|
||||
let result = setup(&res.path, &sh("sh"), &mut env, Some(shell)).unwrap();
|
||||
assert_eq!(shell, result.unwrap().shell);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_integration_failure() {
|
||||
let mut env = EnvMap::new();
|
||||
let result = setup("/nonexistent", &sh("sh"), &mut env, None).unwrap();
|
||||
assert!(result.is_none());
|
||||
assert_eq!(0, env.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_shell_cases() {
|
||||
assert_eq!(detect_shell(&sh("sh")), None);
|
||||
assert_eq!(detect_shell(&sh("bash")), Some(Shell::Bash));
|
||||
assert_eq!(detect_shell(&sh("elvish")), Some(Shell::Elvish));
|
||||
assert_eq!(detect_shell(&sh("fish")), Some(Shell::Fish));
|
||||
assert_eq!(detect_shell(&sh("nu")), Some(Shell::Nushell));
|
||||
assert_eq!(detect_shell(&sh("zsh")), Some(Shell::Zsh));
|
||||
|
||||
assert_eq!(detect_shell(&sh("bash -c 'command'")), Some(Shell::Bash));
|
||||
assert_eq!(detect_shell(&sh("\"/a b/bash\"")), Some(Shell::Bash));
|
||||
assert_eq!(detect_shell(&sh("/usr/bin/zsh")), Some(Shell::Zsh));
|
||||
assert_eq!(
|
||||
detect_shell(&Command::Direct(vec!["/bin/fish".into()])),
|
||||
Some(Shell::Fish)
|
||||
);
|
||||
assert_eq!(detect_shell(&sh("")), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arg_iterator_shell_and_direct() {
|
||||
let v: Vec<_> = sh("echo hello world").arg_iterator().collect();
|
||||
assert_eq!(v, ["echo", "hello", "world"]);
|
||||
let v: Vec<_> = Command::Direct(vec!["echo".into(), "hello world".into()])
|
||||
.arg_iterator()
|
||||
.collect();
|
||||
assert_eq!(v, ["echo", "hello world"]);
|
||||
let v: Vec<_> = sh("a \"b c\"\td\\\"e").arg_iterator().collect();
|
||||
assert_eq!(v, ["a", "b c", "d\"e"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setup_features_all_variants() {
|
||||
// all features enabled
|
||||
let mut env = EnvMap::new();
|
||||
setup_features(
|
||||
&mut env,
|
||||
ShellIntegrationFeatures {
|
||||
cursor: true,
|
||||
sudo: true,
|
||||
title: true,
|
||||
ssh_env: true,
|
||||
ssh_terminfo: true,
|
||||
path: true,
|
||||
},
|
||||
true,
|
||||
);
|
||||
assert_eq!(
|
||||
"cursor:blink,path,ssh-env,ssh-terminfo,sudo,title",
|
||||
env["GHOSTTY_SHELL_FEATURES"]
|
||||
);
|
||||
|
||||
// all features disabled
|
||||
let mut env = EnvMap::new();
|
||||
setup_features(&mut env, ShellIntegrationFeatures::NONE, true);
|
||||
assert!(!env.contains_key("GHOSTTY_SHELL_FEATURES"));
|
||||
|
||||
// mixed features
|
||||
let mut env = EnvMap::new();
|
||||
setup_features(
|
||||
&mut env,
|
||||
ShellIntegrationFeatures {
|
||||
sudo: true,
|
||||
ssh_env: true,
|
||||
..ShellIntegrationFeatures::NONE
|
||||
},
|
||||
true,
|
||||
);
|
||||
assert_eq!("ssh-env,sudo", env["GHOSTTY_SHELL_FEATURES"]);
|
||||
|
||||
// blinking cursor
|
||||
let only_cursor = ShellIntegrationFeatures {
|
||||
cursor: true,
|
||||
..ShellIntegrationFeatures::NONE
|
||||
};
|
||||
let mut env = EnvMap::new();
|
||||
setup_features(&mut env, only_cursor, true);
|
||||
assert_eq!("cursor:blink", env["GHOSTTY_SHELL_FEATURES"]);
|
||||
|
||||
// steady cursor
|
||||
let mut env = EnvMap::new();
|
||||
setup_features(&mut env, only_cursor, false);
|
||||
assert_eq!("cursor:steady", env["GHOSTTY_SHELL_FEATURES"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_features() {
|
||||
let mut env = EnvMap::new();
|
||||
setup_features(&mut env, ShellIntegrationFeatures::default(), true);
|
||||
assert_eq!("cursor:blink,path,title", env["GHOSTTY_SHELL_FEATURES"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bash() {
|
||||
let res = TmpResourcesDir::new(Shell::Bash);
|
||||
let mut env = EnvMap::new();
|
||||
|
||||
let command = setup_bash(&sh("bash"), &res.path, &mut env).unwrap();
|
||||
assert_eq!("bash --posix", shell_str(command.as_ref()));
|
||||
assert_eq!("1", env["GHOSTTY_BASH_INJECT"]);
|
||||
assert_eq!(format!("{}/ghostty.bash", res.shell_path), env["ENV"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bash_unsupported_options() {
|
||||
let res = TmpResourcesDir::new(Shell::Bash);
|
||||
for cmdline in [
|
||||
"bash --posix",
|
||||
"bash --rcfile script.sh --posix",
|
||||
"bash --init-file script.sh --posix",
|
||||
"bash -c script.sh",
|
||||
"bash -ic script.sh",
|
||||
] {
|
||||
let mut env = EnvMap::new();
|
||||
assert!(
|
||||
setup_bash(&sh(cmdline), &res.path, &mut env)
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(0, env.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bash_inject_flags() {
|
||||
let res = TmpResourcesDir::new(Shell::Bash);
|
||||
|
||||
let mut env = EnvMap::new();
|
||||
let command = setup_bash(&sh("bash --norc"), &res.path, &mut env).unwrap();
|
||||
assert_eq!("bash --posix", shell_str(command.as_ref()));
|
||||
assert_eq!("1 --norc", env["GHOSTTY_BASH_INJECT"]);
|
||||
|
||||
let mut env = EnvMap::new();
|
||||
let command = setup_bash(&sh("bash --noprofile"), &res.path, &mut env).unwrap();
|
||||
assert_eq!("bash --posix", shell_str(command.as_ref()));
|
||||
assert_eq!("1 --noprofile", env["GHOSTTY_BASH_INJECT"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bash_rcfile() {
|
||||
let res = TmpResourcesDir::new(Shell::Bash);
|
||||
let mut env = EnvMap::new();
|
||||
|
||||
let command = setup_bash(&sh("bash --rcfile profile.sh"), &res.path, &mut env).unwrap();
|
||||
assert_eq!("bash --posix", shell_str(command.as_ref()));
|
||||
assert_eq!("profile.sh", env["GHOSTTY_BASH_RCFILE"]);
|
||||
|
||||
let command = setup_bash(&sh("bash --init-file profile.sh"), &res.path, &mut env).unwrap();
|
||||
assert_eq!("bash --posix", shell_str(command.as_ref()));
|
||||
assert_eq!("profile.sh", env["GHOSTTY_BASH_RCFILE"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bash_histfile() {
|
||||
let res = TmpResourcesDir::new(Shell::Bash);
|
||||
|
||||
// HISTFILE unset (needs a home dir; set HOME so it is deterministic)
|
||||
temp_env::with_var("HOME", Some("/home/test"), || {
|
||||
let mut env = EnvMap::new();
|
||||
setup_bash(&sh("bash"), &res.path, &mut env).unwrap();
|
||||
assert!(env["HISTFILE"].ends_with(".bash_history"));
|
||||
assert_eq!("1", env["GHOSTTY_BASH_UNEXPORT_HISTFILE"]);
|
||||
});
|
||||
|
||||
// HISTFILE set
|
||||
let mut env = EnvMap::new();
|
||||
env.insert("HISTFILE".into(), "my_history".into());
|
||||
setup_bash(&sh("bash"), &res.path, &mut env).unwrap();
|
||||
assert_eq!("my_history", env["HISTFILE"]);
|
||||
assert!(!env.contains_key("GHOSTTY_BASH_UNEXPORT_HISTFILE"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bash_env() {
|
||||
let res = TmpResourcesDir::new(Shell::Bash);
|
||||
let mut env = EnvMap::new();
|
||||
env.insert("ENV".into(), "env.sh".into());
|
||||
|
||||
setup_bash(&sh("bash"), &res.path, &mut env).unwrap();
|
||||
assert_eq!("env.sh", env["GHOSTTY_BASH_ENV"]);
|
||||
assert_eq!(format!("{}/ghostty.bash", res.shell_path), env["ENV"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bash_additional_arguments() {
|
||||
let res = TmpResourcesDir::new(Shell::Bash);
|
||||
let mut env = EnvMap::new();
|
||||
|
||||
let command = setup_bash(&sh("bash - --arg file1 file2"), &res.path, &mut env).unwrap();
|
||||
assert_eq!(
|
||||
"bash --posix - --arg file1 file2",
|
||||
shell_str(command.as_ref())
|
||||
);
|
||||
|
||||
let command = setup_bash(&sh("bash -- --arg file1 file2"), &res.path, &mut env).unwrap();
|
||||
assert_eq!(
|
||||
"bash --posix -- --arg file1 file2",
|
||||
shell_str(command.as_ref())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bash_missing_resources() {
|
||||
let tmp = TempDir::init().unwrap();
|
||||
let dir = std::fs::canonicalize(tmp.path()).unwrap();
|
||||
let mut env = EnvMap::new();
|
||||
assert!(
|
||||
setup_bash(&sh("bash"), dir.to_str().unwrap(), &mut env)
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(0, env.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xdg_empty_xdg_data_dirs() {
|
||||
let res = TmpResourcesDir::new(Shell::Fish);
|
||||
let mut env = EnvMap::new();
|
||||
|
||||
assert!(setup_xdg_data_dirs(&res.path, &mut env));
|
||||
assert_eq!(
|
||||
format!("{}/shell-integration", res.path),
|
||||
env["GHOSTTY_SHELL_INTEGRATION_XDG_DIR"]
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}/shell-integration:/usr/local/share:/usr/share", res.path),
|
||||
env["XDG_DATA_DIRS"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xdg_existing_xdg_data_dirs() {
|
||||
let res = TmpResourcesDir::new(Shell::Fish);
|
||||
let mut env = EnvMap::new();
|
||||
env.insert("XDG_DATA_DIRS".into(), "/opt/share".into());
|
||||
|
||||
assert!(setup_xdg_data_dirs(&res.path, &mut env));
|
||||
assert_eq!(
|
||||
format!("{}/shell-integration", res.path),
|
||||
env["GHOSTTY_SHELL_INTEGRATION_XDG_DIR"]
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}/shell-integration:/opt/share", res.path),
|
||||
env["XDG_DATA_DIRS"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xdg_missing_resources() {
|
||||
let tmp = TempDir::init().unwrap();
|
||||
let dir = std::fs::canonicalize(tmp.path()).unwrap();
|
||||
let mut env = EnvMap::new();
|
||||
assert!(!setup_xdg_data_dirs(dir.to_str().unwrap(), &mut env));
|
||||
assert_eq!(0, env.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nushell() {
|
||||
let res = TmpResourcesDir::new(Shell::Nushell);
|
||||
let mut env = EnvMap::new();
|
||||
|
||||
let command = setup_nushell(&sh("nu"), &res.path, &mut env);
|
||||
assert_eq!("nu --execute 'use ghostty *'", shell_str(command.as_ref()));
|
||||
assert_eq!(
|
||||
format!("{}/shell-integration", res.path),
|
||||
env["GHOSTTY_SHELL_INTEGRATION_XDG_DIR"]
|
||||
);
|
||||
assert!(env["XDG_DATA_DIRS"].starts_with(&format!("{}/shell-integration", res.path)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nushell_unsupported_options() {
|
||||
let res = TmpResourcesDir::new(Shell::Nushell);
|
||||
for cmdline in [
|
||||
"nu --command exit",
|
||||
"nu --lsp",
|
||||
"nu -c script.sh",
|
||||
"nu -ic script.sh",
|
||||
] {
|
||||
let mut env = EnvMap::new();
|
||||
assert!(setup_nushell(&sh(cmdline), &res.path, &mut env).is_none());
|
||||
assert!(env.contains_key("XDG_DATA_DIRS"));
|
||||
assert!(env.contains_key("GHOSTTY_SHELL_INTEGRATION_XDG_DIR"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nushell_missing_resources() {
|
||||
let tmp = TempDir::init().unwrap();
|
||||
let dir = std::fs::canonicalize(tmp.path()).unwrap();
|
||||
let mut env = EnvMap::new();
|
||||
assert!(setup_nushell(&sh("nu"), dir.to_str().unwrap(), &mut env).is_none());
|
||||
assert_eq!(0, env.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zsh() {
|
||||
let res = TmpResourcesDir::new(Shell::Zsh);
|
||||
let mut env = EnvMap::new();
|
||||
|
||||
let command = setup_zsh(&sh("zsh"), &res.path, &mut env);
|
||||
assert_eq!("zsh", shell_str(command.as_ref()));
|
||||
assert_eq!(res.shell_path, env["ZDOTDIR"]);
|
||||
assert!(!env.contains_key("GHOSTTY_ZSH_ZDOTDIR"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zsh_zdotdir() {
|
||||
let res = TmpResourcesDir::new(Shell::Zsh);
|
||||
let mut env = EnvMap::new();
|
||||
env.insert("ZDOTDIR".into(), "$HOME/.config/zsh".into());
|
||||
|
||||
let command = setup_zsh(&sh("zsh"), &res.path, &mut env);
|
||||
assert_eq!("zsh", shell_str(command.as_ref()));
|
||||
assert_eq!(res.shell_path, env["ZDOTDIR"]);
|
||||
assert_eq!("$HOME/.config/zsh", env["GHOSTTY_ZSH_ZDOTDIR"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zsh_missing_resources() {
|
||||
let tmp = TempDir::init().unwrap();
|
||||
let dir = std::fs::canonicalize(tmp.path()).unwrap();
|
||||
let mut env = EnvMap::new();
|
||||
assert!(setup_zsh(&sh("zsh"), dir.to_str().unwrap(), &mut env).is_none());
|
||||
assert_eq!(0, env.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_resources_exist() {
|
||||
// The shipped scripts that setup() points shells at.
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../resources");
|
||||
let root = root.to_str().unwrap();
|
||||
let mut env = EnvMap::new();
|
||||
let r = setup(root, &sh("bash"), &mut env, None).unwrap().unwrap();
|
||||
assert_eq!(r.shell, Shell::Bash);
|
||||
assert!(env["ENV"].ends_with("shell-integration/bash/ghostty.bash"));
|
||||
assert!(
|
||||
Path::new(root)
|
||||
.join("shell-integration/nushell/vendor/autoload/ghostty.nu")
|
||||
.is_file()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,1184 @@
|
||||
//! Ghostty's terminfo entry. Port of `terminfo/ghostty.zig`.
|
||||
|
||||
use super::source::{Capability, Source, Value};
|
||||
|
||||
/// Ghostty's terminfo entry.
|
||||
pub const GHOSTTY: Source = Source {
|
||||
names: &[
|
||||
// We support the "xterm-" prefix because some poorly behaved programs
|
||||
// use this to detect if the terminal supports 256 colors and other
|
||||
// features.
|
||||
// HACK: This is a hack on a hack...we use "xterm-ghostty" to prevent
|
||||
// vim from breaking, and when we do this as the default we break
|
||||
// tcell-based applications (lazygit, aerc, etc). tcell has a bug where
|
||||
// the primary terminfo name must be the value of TERM.
|
||||
// https://github.com/gdamore/tcell/pull/639 fixes the issue but is not
|
||||
// merged yet. Consider switching these in the future.
|
||||
"xterm-ghostty",
|
||||
// The preferred name
|
||||
"ghostty",
|
||||
// Our "formal" name
|
||||
"Ghostty",
|
||||
],
|
||||
|
||||
// NOTE: These capabilities are super underdocumented and I'm not 100%
|
||||
// I've got the list or my understanding of any in this list fully correct.
|
||||
// As we learn more, please update the comments to better explain what
|
||||
// anything means.
|
||||
//
|
||||
// I've marked some capabilities as "???" if I don't understand what they
|
||||
// mean but I just assume I support since other modern terminals do. In
|
||||
// this case, I'd love if anyone could help explain what this means and
|
||||
// verify that Ghostty does indeed support it and if not we can fix it.
|
||||
capabilities: &[
|
||||
// automatic right margin -- when reaching the end of a line, text is
|
||||
// wrapped to the next line.
|
||||
Capability {
|
||||
name: "am",
|
||||
value: Value::Boolean,
|
||||
},
|
||||
// background color erase -- screen is erased with the background color
|
||||
Capability {
|
||||
name: "bce",
|
||||
value: Value::Boolean,
|
||||
},
|
||||
// terminal can change color definitions, i.e. we can change the color
|
||||
// palette.
|
||||
Capability {
|
||||
name: "ccc",
|
||||
value: Value::Boolean,
|
||||
},
|
||||
// supports changing the window title.
|
||||
Capability {
|
||||
name: "hs",
|
||||
value: Value::Boolean,
|
||||
},
|
||||
// terminal has a meta key
|
||||
Capability {
|
||||
name: "km",
|
||||
value: Value::Boolean,
|
||||
},
|
||||
// terminal will not echo input on the screen on its own
|
||||
Capability {
|
||||
name: "mc5i",
|
||||
value: Value::Boolean,
|
||||
},
|
||||
// safe to move (move what?) while in insert/standout mode. (???)
|
||||
Capability {
|
||||
name: "mir",
|
||||
value: Value::Boolean,
|
||||
},
|
||||
Capability {
|
||||
name: "msgr",
|
||||
value: Value::Boolean,
|
||||
},
|
||||
// no pad character (???)
|
||||
Capability {
|
||||
name: "npc",
|
||||
value: Value::Boolean,
|
||||
},
|
||||
// newline ignored after 80 cols (???)
|
||||
Capability {
|
||||
name: "xenl",
|
||||
value: Value::Boolean,
|
||||
},
|
||||
// Terminal supports default colors
|
||||
Capability {
|
||||
name: "AX",
|
||||
value: Value::Boolean,
|
||||
},
|
||||
// Tmux "truecolor" mode. Other programs also use this to detect
|
||||
// if the terminal supports "truecolor". This means that the terminal
|
||||
// can display 24-bit RGB colors.
|
||||
Capability {
|
||||
name: "Tc",
|
||||
value: Value::Boolean,
|
||||
},
|
||||
// Colored underlines. https://sw.kovidgoyal.net/kitty/underlines/
|
||||
Capability {
|
||||
name: "Su",
|
||||
value: Value::Boolean,
|
||||
},
|
||||
// Terminal supports a number of xterm extensions
|
||||
Capability {
|
||||
name: "XT",
|
||||
value: Value::Boolean,
|
||||
},
|
||||
// Full keyboard support using Kitty's keyboard protocol:
|
||||
// https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
||||
Capability {
|
||||
name: "fullkbd",
|
||||
value: Value::Boolean,
|
||||
},
|
||||
// Number of colors in the color palette.
|
||||
Capability {
|
||||
name: "colors",
|
||||
value: Value::Numeric(256),
|
||||
},
|
||||
// Number of columns in a line. Our terminal is variable width on
|
||||
// Window resize but this appears to just be the value set by most
|
||||
// terminals.
|
||||
Capability {
|
||||
name: "cols",
|
||||
value: Value::Numeric(80),
|
||||
},
|
||||
// Initial tabstop interval.
|
||||
Capability {
|
||||
name: "it",
|
||||
value: Value::Numeric(8),
|
||||
},
|
||||
// Number of lines on a page. Similar to cols this is variable width
|
||||
// but this appears to be the value set by most terminals.
|
||||
Capability {
|
||||
name: "lines",
|
||||
value: Value::Numeric(24),
|
||||
},
|
||||
// Number of color pairs on the screen.
|
||||
Capability {
|
||||
name: "pairs",
|
||||
value: Value::Numeric(32767),
|
||||
},
|
||||
// Alternate character set. This is the VT100 alternate character set.
|
||||
// I don't know what the value means, I copied this from Kitty and
|
||||
// verified with some other terminals (looks similar).
|
||||
Capability {
|
||||
name: "acsc",
|
||||
value: Value::String(
|
||||
"++\\,\\,--..00``aaffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~",
|
||||
),
|
||||
},
|
||||
// Curly, dashed, etc underlines
|
||||
Capability {
|
||||
name: "Smulx",
|
||||
value: Value::String("\\E[4:%p1%dm"),
|
||||
},
|
||||
// Colored underlines
|
||||
Capability {
|
||||
name: "Setulc",
|
||||
value: Value::String(
|
||||
"\\E[58:2::%p1%{65536}%/%d:%p1%{256}%/%{255}%&%d:%p1%{255}%&%d%;m",
|
||||
),
|
||||
},
|
||||
// Cursor styles
|
||||
Capability {
|
||||
name: "Ss",
|
||||
value: Value::String("\\E[%p1%d q"),
|
||||
},
|
||||
// Cursor style reset
|
||||
Capability {
|
||||
name: "Se",
|
||||
value: Value::String("\\E[2 q"),
|
||||
},
|
||||
// OSC 52 Clipboard
|
||||
Capability {
|
||||
name: "Ms",
|
||||
value: Value::String("\\E]52;%p1%s;%p2%s\\007"),
|
||||
},
|
||||
// Synchronized output
|
||||
Capability {
|
||||
name: "Sync",
|
||||
value: Value::String("\\E[?2026%?%p1%{1}%-%tl%eh%;"),
|
||||
},
|
||||
// Bracketed paste mode
|
||||
Capability {
|
||||
name: "BD",
|
||||
value: Value::String("\\E[?2004l"),
|
||||
},
|
||||
Capability {
|
||||
name: "BE",
|
||||
value: Value::String("\\E[?2004h"),
|
||||
},
|
||||
// Bracketed paste start/end
|
||||
Capability {
|
||||
name: "PS",
|
||||
value: Value::String("\\E[200~"),
|
||||
},
|
||||
Capability {
|
||||
name: "PE",
|
||||
value: Value::String("\\E[201~"),
|
||||
},
|
||||
// Mouse
|
||||
Capability {
|
||||
name: "XM",
|
||||
value: Value::String("\\E[?1006;1000%?%p1%{1}%=%th%el%;"),
|
||||
},
|
||||
Capability {
|
||||
name: "xm",
|
||||
value: Value::String("\\E[<%i%p3%d;%p1%d;%p2%d;%?%p4%tM%em%;"),
|
||||
},
|
||||
// Secondary device attributes request / response
|
||||
Capability {
|
||||
name: "RV",
|
||||
value: Value::String("\\E[>c"),
|
||||
},
|
||||
Capability {
|
||||
name: "rv",
|
||||
value: Value::String("\\E\\\\[[0-9]+;[0-9]+;[0-9]+c"),
|
||||
},
|
||||
// XTVERSION
|
||||
Capability {
|
||||
name: "XR",
|
||||
value: Value::String("\\E[>0q"),
|
||||
},
|
||||
Capability {
|
||||
name: "xr",
|
||||
value: Value::String("\\EP>\\\\|[ -~]+a\\E\\\\"),
|
||||
},
|
||||
// DECSLRM (Left/Right Margins)
|
||||
Capability {
|
||||
name: "Enmg",
|
||||
value: Value::String("\\E[?69h"),
|
||||
},
|
||||
Capability {
|
||||
name: "Dsmg",
|
||||
value: Value::String("\\E[?69l"),
|
||||
},
|
||||
Capability {
|
||||
name: "Clmg",
|
||||
value: Value::String("\\E[s"),
|
||||
},
|
||||
Capability {
|
||||
name: "Cmg",
|
||||
value: Value::String("\\E[%i%p1%d;%p2%ds"),
|
||||
},
|
||||
// Clear screen. E3 is the extension to clear scrollback
|
||||
Capability {
|
||||
name: "clear",
|
||||
value: Value::String("\\E[H\\E[2J"),
|
||||
},
|
||||
Capability {
|
||||
name: "E3",
|
||||
value: Value::String("\\E[3J"),
|
||||
},
|
||||
// Focus reporting. Introduced in ncurses 6.4-20231028
|
||||
Capability {
|
||||
name: "fe",
|
||||
value: Value::String("\\E[?1004h"),
|
||||
},
|
||||
Capability {
|
||||
name: "fd",
|
||||
value: Value::String("\\E[?1004l"),
|
||||
},
|
||||
Capability {
|
||||
name: "kxIN",
|
||||
value: Value::String("\\E[I"),
|
||||
},
|
||||
Capability {
|
||||
name: "kxOUT",
|
||||
value: Value::String("\\E[O"),
|
||||
},
|
||||
// These are all capabilities that should be pretty straightforward
|
||||
// and map to input sequences.
|
||||
Capability {
|
||||
name: "bel",
|
||||
value: Value::String("^G"),
|
||||
},
|
||||
Capability {
|
||||
name: "blink",
|
||||
value: Value::String("\\E[5m"),
|
||||
},
|
||||
Capability {
|
||||
name: "bold",
|
||||
value: Value::String("\\E[1m"),
|
||||
},
|
||||
Capability {
|
||||
name: "cbt",
|
||||
value: Value::String("\\E[Z"),
|
||||
},
|
||||
Capability {
|
||||
name: "civis",
|
||||
value: Value::String("\\E[?25l"),
|
||||
},
|
||||
Capability {
|
||||
name: "cnorm",
|
||||
value: Value::String("\\E[?12l\\E[?25h"),
|
||||
},
|
||||
Capability {
|
||||
name: "cr",
|
||||
value: Value::String("\\r"),
|
||||
},
|
||||
Capability {
|
||||
name: "csr",
|
||||
value: Value::String("\\E[%i%p1%d;%p2%dr"),
|
||||
},
|
||||
Capability {
|
||||
name: "cub",
|
||||
value: Value::String("\\E[%p1%dD"),
|
||||
},
|
||||
Capability {
|
||||
name: "cub1",
|
||||
value: Value::String("^H"),
|
||||
},
|
||||
Capability {
|
||||
name: "cud",
|
||||
value: Value::String("\\E[%p1%dB"),
|
||||
},
|
||||
Capability {
|
||||
name: "cud1",
|
||||
value: Value::String("^J"),
|
||||
},
|
||||
Capability {
|
||||
name: "cuf",
|
||||
value: Value::String("\\E[%p1%dC"),
|
||||
},
|
||||
Capability {
|
||||
name: "cuf1",
|
||||
value: Value::String("\\E[C"),
|
||||
},
|
||||
Capability {
|
||||
name: "cup",
|
||||
value: Value::String("\\E[%i%p1%d;%p2%dH"),
|
||||
},
|
||||
Capability {
|
||||
name: "cuu",
|
||||
value: Value::String("\\E[%p1%dA"),
|
||||
},
|
||||
Capability {
|
||||
name: "cuu1",
|
||||
value: Value::String("\\E[A"),
|
||||
},
|
||||
Capability {
|
||||
name: "cvvis",
|
||||
value: Value::String("\\E[?12;25h"),
|
||||
},
|
||||
Capability {
|
||||
name: "dch",
|
||||
value: Value::String("\\E[%p1%dP"),
|
||||
},
|
||||
Capability {
|
||||
name: "dch1",
|
||||
value: Value::String("\\E[P"),
|
||||
},
|
||||
Capability {
|
||||
name: "dim",
|
||||
value: Value::String("\\E[2m"),
|
||||
},
|
||||
Capability {
|
||||
name: "dl",
|
||||
value: Value::String("\\E[%p1%dM"),
|
||||
},
|
||||
Capability {
|
||||
name: "dl1",
|
||||
value: Value::String("\\E[M"),
|
||||
},
|
||||
Capability {
|
||||
name: "dsl",
|
||||
value: Value::String("\\E]2;\\007"),
|
||||
},
|
||||
Capability {
|
||||
name: "ech",
|
||||
value: Value::String("\\E[%p1%dX"),
|
||||
},
|
||||
Capability {
|
||||
name: "ed",
|
||||
value: Value::String("\\E[J"),
|
||||
},
|
||||
Capability {
|
||||
name: "el",
|
||||
value: Value::String("\\E[K"),
|
||||
},
|
||||
Capability {
|
||||
name: "el1",
|
||||
value: Value::String("\\E[1K"),
|
||||
},
|
||||
Capability {
|
||||
name: "flash",
|
||||
value: Value::String("\\E[?5h$<100/>\\E[?5l"),
|
||||
},
|
||||
Capability {
|
||||
name: "fsl",
|
||||
value: Value::String("^G"),
|
||||
},
|
||||
Capability {
|
||||
name: "home",
|
||||
value: Value::String("\\E[H"),
|
||||
},
|
||||
Capability {
|
||||
name: "hpa",
|
||||
value: Value::String("\\E[%i%p1%dG"),
|
||||
},
|
||||
Capability {
|
||||
name: "ht",
|
||||
value: Value::String("^I"),
|
||||
},
|
||||
Capability {
|
||||
name: "hts",
|
||||
value: Value::String("\\EH"),
|
||||
},
|
||||
Capability {
|
||||
name: "ich",
|
||||
value: Value::String("\\E[%p1%d@"),
|
||||
},
|
||||
Capability {
|
||||
name: "ich1",
|
||||
value: Value::String("\\E[@"),
|
||||
},
|
||||
Capability {
|
||||
name: "il",
|
||||
value: Value::String("\\E[%p1%dL"),
|
||||
},
|
||||
Capability {
|
||||
name: "il1",
|
||||
value: Value::String("\\E[L"),
|
||||
},
|
||||
Capability {
|
||||
name: "ind",
|
||||
value: Value::String("\\n"),
|
||||
},
|
||||
Capability {
|
||||
name: "indn",
|
||||
value: Value::String("\\E[%p1%dS"),
|
||||
},
|
||||
Capability {
|
||||
name: "initc",
|
||||
value: Value::String(
|
||||
"\\E]4;%p1%d;rgb\\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\\E\\\\",
|
||||
),
|
||||
},
|
||||
Capability {
|
||||
name: "invis",
|
||||
value: Value::String("\\E[8m"),
|
||||
},
|
||||
Capability {
|
||||
name: "oc",
|
||||
value: Value::String("\\E]104\\007"),
|
||||
},
|
||||
Capability {
|
||||
name: "op",
|
||||
value: Value::String("\\E[39;49m"),
|
||||
},
|
||||
Capability {
|
||||
name: "rc",
|
||||
value: Value::String("\\E8"),
|
||||
},
|
||||
Capability {
|
||||
name: "rep",
|
||||
value: Value::String("%p1%c\\E[%p2%{1}%-%db"),
|
||||
},
|
||||
Capability {
|
||||
name: "rev",
|
||||
value: Value::String("\\E[7m"),
|
||||
},
|
||||
Capability {
|
||||
name: "ri",
|
||||
value: Value::String("\\EM"),
|
||||
},
|
||||
Capability {
|
||||
name: "rin",
|
||||
value: Value::String("\\E[%p1%dT"),
|
||||
},
|
||||
Capability {
|
||||
name: "ritm",
|
||||
value: Value::String("\\E[23m"),
|
||||
},
|
||||
Capability {
|
||||
name: "rmacs",
|
||||
value: Value::String("\\E(B"),
|
||||
},
|
||||
Capability {
|
||||
name: "rmam",
|
||||
value: Value::String("\\E[?7l"),
|
||||
},
|
||||
Capability {
|
||||
name: "rmcup",
|
||||
value: Value::String("\\E[?1049l"),
|
||||
},
|
||||
Capability {
|
||||
name: "rmir",
|
||||
value: Value::String("\\E[4l"),
|
||||
},
|
||||
Capability {
|
||||
name: "rmkx",
|
||||
value: Value::String("\\E[?1l\\E>"),
|
||||
},
|
||||
Capability {
|
||||
name: "rmso",
|
||||
value: Value::String("\\E[27m"),
|
||||
},
|
||||
Capability {
|
||||
name: "rmul",
|
||||
value: Value::String("\\E[24m"),
|
||||
},
|
||||
Capability {
|
||||
name: "rmxx",
|
||||
value: Value::String("\\E[29m"),
|
||||
},
|
||||
Capability {
|
||||
name: "setab",
|
||||
value: Value::String(
|
||||
"\\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m",
|
||||
),
|
||||
},
|
||||
Capability {
|
||||
name: "setaf",
|
||||
value: Value::String(
|
||||
"\\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m",
|
||||
),
|
||||
},
|
||||
Capability {
|
||||
name: "setrgbb",
|
||||
value: Value::String("\\E[48:2:%p1%d:%p2%d:%p3%dm"),
|
||||
},
|
||||
Capability {
|
||||
name: "setrgbf",
|
||||
value: Value::String("\\E[38:2:%p1%d:%p2%d:%p3%dm"),
|
||||
},
|
||||
Capability {
|
||||
name: "sgr",
|
||||
value: Value::String(
|
||||
"%?%p9%t\\E(0%e\\E(B%;\\E[0%?%p6%t;1%;%?%p5%t;2%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m",
|
||||
),
|
||||
},
|
||||
Capability {
|
||||
name: "sgr0",
|
||||
value: Value::String("\\E(B\\E[m"),
|
||||
},
|
||||
Capability {
|
||||
name: "sitm",
|
||||
value: Value::String("\\E[3m"),
|
||||
},
|
||||
Capability {
|
||||
name: "smacs",
|
||||
value: Value::String("\\E(0"),
|
||||
},
|
||||
Capability {
|
||||
name: "smam",
|
||||
value: Value::String("\\E[?7h"),
|
||||
},
|
||||
Capability {
|
||||
name: "smcup",
|
||||
value: Value::String("\\E[?1049h"),
|
||||
},
|
||||
Capability {
|
||||
name: "smir",
|
||||
value: Value::String("\\E[4h"),
|
||||
},
|
||||
Capability {
|
||||
name: "smkx",
|
||||
value: Value::String("\\E[?1h\\E="),
|
||||
},
|
||||
Capability {
|
||||
name: "smso",
|
||||
value: Value::String("\\E[7m"),
|
||||
},
|
||||
Capability {
|
||||
name: "smul",
|
||||
value: Value::String("\\E[4m"),
|
||||
},
|
||||
Capability {
|
||||
name: "smxx",
|
||||
value: Value::String("\\E[9m"),
|
||||
},
|
||||
Capability {
|
||||
name: "tbc",
|
||||
value: Value::String("\\E[3g"),
|
||||
},
|
||||
Capability {
|
||||
name: "tsl",
|
||||
value: Value::String("\\E]2;"),
|
||||
},
|
||||
Capability {
|
||||
name: "u6",
|
||||
value: Value::String("\\E[%i%d;%dR"),
|
||||
},
|
||||
Capability {
|
||||
name: "u7",
|
||||
value: Value::String("\\E[6n"),
|
||||
},
|
||||
Capability {
|
||||
name: "u8",
|
||||
value: Value::String("\\E[?%[;0123456789]c"),
|
||||
},
|
||||
Capability {
|
||||
name: "u9",
|
||||
value: Value::String("\\E[c"),
|
||||
},
|
||||
Capability {
|
||||
name: "vpa",
|
||||
value: Value::String("\\E[%i%p1%dd"),
|
||||
},
|
||||
//-----------------------------------------------------------
|
||||
// Completely unvalidated entries that are blindly copied from
|
||||
// other terminals (Kitty, Wezterm, Alacritty) and may or may not
|
||||
// actually work with Ghostty. todo is to validate these!
|
||||
Capability {
|
||||
name: "kDC",
|
||||
value: Value::String("\\E[3;2~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kDC3",
|
||||
value: Value::String("\\E[3;3~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kDC4",
|
||||
value: Value::String("\\E[3;4~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kDC5",
|
||||
value: Value::String("\\E[3;5~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kDC6",
|
||||
value: Value::String("\\E[3;6~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kDC7",
|
||||
value: Value::String("\\E[3;7~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kDN",
|
||||
value: Value::String("\\E[1;2B"),
|
||||
},
|
||||
Capability {
|
||||
name: "kDN3",
|
||||
value: Value::String("\\E[1;3B"),
|
||||
},
|
||||
Capability {
|
||||
name: "kDN4",
|
||||
value: Value::String("\\E[1;4B"),
|
||||
},
|
||||
Capability {
|
||||
name: "kDN5",
|
||||
value: Value::String("\\E[1;5B"),
|
||||
},
|
||||
Capability {
|
||||
name: "kDN6",
|
||||
value: Value::String("\\E[1;6B"),
|
||||
},
|
||||
Capability {
|
||||
name: "kDN7",
|
||||
value: Value::String("\\E[1;7B"),
|
||||
},
|
||||
Capability {
|
||||
name: "kEND",
|
||||
value: Value::String("\\E[1;2F"),
|
||||
},
|
||||
Capability {
|
||||
name: "kEND3",
|
||||
value: Value::String("\\E[1;3F"),
|
||||
},
|
||||
Capability {
|
||||
name: "kEND4",
|
||||
value: Value::String("\\E[1;4F"),
|
||||
},
|
||||
Capability {
|
||||
name: "kEND5",
|
||||
value: Value::String("\\E[1;5F"),
|
||||
},
|
||||
Capability {
|
||||
name: "kEND6",
|
||||
value: Value::String("\\E[1;6F"),
|
||||
},
|
||||
Capability {
|
||||
name: "kEND7",
|
||||
value: Value::String("\\E[1;7F"),
|
||||
},
|
||||
Capability {
|
||||
name: "kHOM",
|
||||
value: Value::String("\\E[1;2H"),
|
||||
},
|
||||
Capability {
|
||||
name: "kHOM3",
|
||||
value: Value::String("\\E[1;3H"),
|
||||
},
|
||||
Capability {
|
||||
name: "kHOM4",
|
||||
value: Value::String("\\E[1;4H"),
|
||||
},
|
||||
Capability {
|
||||
name: "kHOM5",
|
||||
value: Value::String("\\E[1;5H"),
|
||||
},
|
||||
Capability {
|
||||
name: "kHOM6",
|
||||
value: Value::String("\\E[1;6H"),
|
||||
},
|
||||
Capability {
|
||||
name: "kHOM7",
|
||||
value: Value::String("\\E[1;7H"),
|
||||
},
|
||||
Capability {
|
||||
name: "kIC",
|
||||
value: Value::String("\\E[2;2~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kIC3",
|
||||
value: Value::String("\\E[2;3~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kIC4",
|
||||
value: Value::String("\\E[2;4~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kIC5",
|
||||
value: Value::String("\\E[2;5~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kIC6",
|
||||
value: Value::String("\\E[2;6~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kIC7",
|
||||
value: Value::String("\\E[2;7~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kLFT",
|
||||
value: Value::String("\\E[1;2D"),
|
||||
},
|
||||
Capability {
|
||||
name: "kLFT3",
|
||||
value: Value::String("\\E[1;3D"),
|
||||
},
|
||||
Capability {
|
||||
name: "kLFT4",
|
||||
value: Value::String("\\E[1;4D"),
|
||||
},
|
||||
Capability {
|
||||
name: "kLFT5",
|
||||
value: Value::String("\\E[1;5D"),
|
||||
},
|
||||
Capability {
|
||||
name: "kLFT6",
|
||||
value: Value::String("\\E[1;6D"),
|
||||
},
|
||||
Capability {
|
||||
name: "kLFT7",
|
||||
value: Value::String("\\E[1;7D"),
|
||||
},
|
||||
Capability {
|
||||
name: "kNXT",
|
||||
value: Value::String("\\E[6;2~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kNXT3",
|
||||
value: Value::String("\\E[6;3~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kNXT4",
|
||||
value: Value::String("\\E[6;4~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kNXT5",
|
||||
value: Value::String("\\E[6;5~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kNXT6",
|
||||
value: Value::String("\\E[6;6~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kNXT7",
|
||||
value: Value::String("\\E[6;7~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kPRV",
|
||||
value: Value::String("\\E[5;2~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kPRV3",
|
||||
value: Value::String("\\E[5;3~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kPRV4",
|
||||
value: Value::String("\\E[5;4~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kPRV5",
|
||||
value: Value::String("\\E[5;5~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kPRV6",
|
||||
value: Value::String("\\E[5;6~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kPRV7",
|
||||
value: Value::String("\\E[5;7~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kRIT",
|
||||
value: Value::String("\\E[1;2C"),
|
||||
},
|
||||
Capability {
|
||||
name: "kRIT3",
|
||||
value: Value::String("\\E[1;3C"),
|
||||
},
|
||||
Capability {
|
||||
name: "kRIT4",
|
||||
value: Value::String("\\E[1;4C"),
|
||||
},
|
||||
Capability {
|
||||
name: "kRIT5",
|
||||
value: Value::String("\\E[1;5C"),
|
||||
},
|
||||
Capability {
|
||||
name: "kRIT6",
|
||||
value: Value::String("\\E[1;6C"),
|
||||
},
|
||||
Capability {
|
||||
name: "kRIT7",
|
||||
value: Value::String("\\E[1;7C"),
|
||||
},
|
||||
Capability {
|
||||
name: "kUP",
|
||||
value: Value::String("\\E[1;2A"),
|
||||
},
|
||||
Capability {
|
||||
name: "kUP3",
|
||||
value: Value::String("\\E[1;3A"),
|
||||
},
|
||||
Capability {
|
||||
name: "kUP4",
|
||||
value: Value::String("\\E[1;4A"),
|
||||
},
|
||||
Capability {
|
||||
name: "kUP5",
|
||||
value: Value::String("\\E[1;5A"),
|
||||
},
|
||||
Capability {
|
||||
name: "kUP6",
|
||||
value: Value::String("\\E[1;6A"),
|
||||
},
|
||||
Capability {
|
||||
name: "kUP7",
|
||||
value: Value::String("\\E[1;7A"),
|
||||
},
|
||||
Capability {
|
||||
name: "kbs",
|
||||
value: Value::String("^?"),
|
||||
},
|
||||
Capability {
|
||||
name: "kcbt",
|
||||
value: Value::String("\\E[Z"),
|
||||
},
|
||||
Capability {
|
||||
name: "kcub1",
|
||||
value: Value::String("\\EOD"),
|
||||
},
|
||||
Capability {
|
||||
name: "kcud1",
|
||||
value: Value::String("\\EOB"),
|
||||
},
|
||||
Capability {
|
||||
name: "kcuf1",
|
||||
value: Value::String("\\EOC"),
|
||||
},
|
||||
Capability {
|
||||
name: "kcuu1",
|
||||
value: Value::String("\\EOA"),
|
||||
},
|
||||
Capability {
|
||||
name: "kdch1",
|
||||
value: Value::String("\\E[3~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kend",
|
||||
value: Value::String("\\EOF"),
|
||||
},
|
||||
Capability {
|
||||
name: "kent",
|
||||
value: Value::String("\\EOM"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf1",
|
||||
value: Value::String("\\EOP"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf10",
|
||||
value: Value::String("\\E[21~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf11",
|
||||
value: Value::String("\\E[23~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf12",
|
||||
value: Value::String("\\E[24~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf13",
|
||||
value: Value::String("\\E[1;2P"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf14",
|
||||
value: Value::String("\\E[1;2Q"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf15",
|
||||
value: Value::String("\\E[1;2R"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf16",
|
||||
value: Value::String("\\E[1;2S"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf17",
|
||||
value: Value::String("\\E[15;2~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf18",
|
||||
value: Value::String("\\E[17;2~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf19",
|
||||
value: Value::String("\\E[18;2~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf2",
|
||||
value: Value::String("\\EOQ"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf20",
|
||||
value: Value::String("\\E[19;2~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf21",
|
||||
value: Value::String("\\E[20;2~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf22",
|
||||
value: Value::String("\\E[21;2~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf23",
|
||||
value: Value::String("\\E[23;2~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf24",
|
||||
value: Value::String("\\E[24;2~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf25",
|
||||
value: Value::String("\\E[1;5P"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf26",
|
||||
value: Value::String("\\E[1;5Q"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf27",
|
||||
value: Value::String("\\E[1;5R"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf28",
|
||||
value: Value::String("\\E[1;5S"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf29",
|
||||
value: Value::String("\\E[15;5~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf3",
|
||||
value: Value::String("\\EOR"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf30",
|
||||
value: Value::String("\\E[17;5~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf31",
|
||||
value: Value::String("\\E[18;5~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf32",
|
||||
value: Value::String("\\E[19;5~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf33",
|
||||
value: Value::String("\\E[20;5~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf34",
|
||||
value: Value::String("\\E[21;5~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf35",
|
||||
value: Value::String("\\E[23;5~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf36",
|
||||
value: Value::String("\\E[24;5~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf37",
|
||||
value: Value::String("\\E[1;6P"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf38",
|
||||
value: Value::String("\\E[1;6Q"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf39",
|
||||
value: Value::String("\\E[1;6R"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf4",
|
||||
value: Value::String("\\EOS"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf40",
|
||||
value: Value::String("\\E[1;6S"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf41",
|
||||
value: Value::String("\\E[15;6~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf42",
|
||||
value: Value::String("\\E[17;6~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf43",
|
||||
value: Value::String("\\E[18;6~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf44",
|
||||
value: Value::String("\\E[19;6~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf45",
|
||||
value: Value::String("\\E[20;6~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf46",
|
||||
value: Value::String("\\E[21;6~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf47",
|
||||
value: Value::String("\\E[23;6~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf48",
|
||||
value: Value::String("\\E[24;6~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf49",
|
||||
value: Value::String("\\E[1;3P"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf5",
|
||||
value: Value::String("\\E[15~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf50",
|
||||
value: Value::String("\\E[1;3Q"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf51",
|
||||
value: Value::String("\\E[1;3R"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf52",
|
||||
value: Value::String("\\E[1;3S"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf53",
|
||||
value: Value::String("\\E[15;3~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf54",
|
||||
value: Value::String("\\E[17;3~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf55",
|
||||
value: Value::String("\\E[18;3~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf56",
|
||||
value: Value::String("\\E[19;3~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf57",
|
||||
value: Value::String("\\E[20;3~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf58",
|
||||
value: Value::String("\\E[21;3~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf59",
|
||||
value: Value::String("\\E[23;3~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf6",
|
||||
value: Value::String("\\E[17~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf60",
|
||||
value: Value::String("\\E[24;3~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf61",
|
||||
value: Value::String("\\E[1;4P"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf62",
|
||||
value: Value::String("\\E[1;4Q"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf63",
|
||||
value: Value::String("\\E[1;4R"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf7",
|
||||
value: Value::String("\\E[18~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf8",
|
||||
value: Value::String("\\E[19~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kf9",
|
||||
value: Value::String("\\E[20~"),
|
||||
},
|
||||
Capability {
|
||||
name: "khome",
|
||||
value: Value::String("\\EOH"),
|
||||
},
|
||||
Capability {
|
||||
name: "kich1",
|
||||
value: Value::String("\\E[2~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kind",
|
||||
value: Value::String("\\E[1;2B"),
|
||||
},
|
||||
Capability {
|
||||
name: "kmous",
|
||||
value: Value::String("\\E[<"),
|
||||
},
|
||||
Capability {
|
||||
name: "knp",
|
||||
value: Value::String("\\E[6~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kpp",
|
||||
value: Value::String("\\E[5~"),
|
||||
},
|
||||
Capability {
|
||||
name: "kri",
|
||||
value: Value::String("\\E[1;2A"),
|
||||
},
|
||||
Capability {
|
||||
name: "rs1",
|
||||
value: Value::String("\\E]\\E\\\\\\Ec"),
|
||||
},
|
||||
Capability {
|
||||
name: "sc",
|
||||
value: Value::String("\\E7"),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn encode() {
|
||||
let mut buf = Vec::new();
|
||||
GHOSTTY.encode(&mut buf).unwrap();
|
||||
assert!(!buf.is_empty());
|
||||
let s = String::from_utf8(buf).unwrap();
|
||||
assert!(s.starts_with("xterm-ghostty|ghostty|Ghostty,\n\tam,\n\tbce,\n"));
|
||||
assert!(s.ends_with("\tsc=\\E7,\n"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//! Package terminfo provides functionality related to terminfo/termcap files.
|
||||
//!
|
||||
//! At the time of writing this comment, the focus is on generating terminfo
|
||||
//! files so that we can maintain our terminfo in Rust instead of hand-writing
|
||||
//! the archaic (imo) terminfo format by hand. But eventually we may want to
|
||||
//! extract this into a more full-featured library on its own.
|
||||
//!
|
||||
//! Port of `terminfo/main.zig`.
|
||||
|
||||
pub mod ghostty;
|
||||
pub mod source;
|
||||
|
||||
pub use ghostty::GHOSTTY as ghostty;
|
||||
pub use source::Source;
|
||||
@@ -0,0 +1,214 @@
|
||||
//! Terminfo source format. This can be used to encode terminfo files.
|
||||
//! This cannot parse terminfo source files yet because it isn't something
|
||||
//! I need to do but this can be added later.
|
||||
//!
|
||||
//! Background: <https://invisible-island.net/ncurses/man/terminfo.5.html>
|
||||
//!
|
||||
//! Port of `terminfo/Source.zig`.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
|
||||
/// A terminfo source description.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Source {
|
||||
/// The set of names for the terminal. These match the TERM environment
|
||||
/// variable and are used to look up this terminal. Historically, the final
|
||||
/// name in the list was the most common name for the terminal and contains
|
||||
/// spaces and other characters. See terminfo(5) for details.
|
||||
pub names: &'static [&'static str],
|
||||
|
||||
/// The set of capabilities in this terminfo file.
|
||||
pub capabilities: &'static [Capability],
|
||||
}
|
||||
|
||||
/// A capability in a terminfo file. This also includes any "use" capabilities
|
||||
/// since they behave just like other capabilities as documented in terminfo(5).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Capability {
|
||||
/// The name of capability. This is the "Cap-name" value in terminfo(5).
|
||||
pub name: &'static str,
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Value {
|
||||
/// Canceled value, i.e. suffixed with @
|
||||
Canceled,
|
||||
|
||||
/// Boolean values are always true if they exist so there is no value.
|
||||
Boolean,
|
||||
|
||||
/// Numeric values are always "unsigned decimal integers". The size
|
||||
/// of the integer is unspecified in terminfo(5). 32-bits is a common
|
||||
/// integer size but this may be wrong.
|
||||
Numeric(u32),
|
||||
|
||||
String(&'static str),
|
||||
}
|
||||
|
||||
impl Source {
|
||||
/// Encode as a terminfo source file. The encoding is always done in a
|
||||
/// human-readable format with whitespace. Fields are always written in the
|
||||
/// order of the slices on this struct; this will not do any reordering.
|
||||
pub fn encode<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||
// Encode the names in the order specified
|
||||
for (i, name) in self.names.iter().enumerate() {
|
||||
if i != 0 {
|
||||
writer.write_all(b"|")?;
|
||||
}
|
||||
writer.write_all(name.as_bytes())?;
|
||||
}
|
||||
writer.write_all(b",\n")?;
|
||||
|
||||
// Encode each of the capabilities in the order specified
|
||||
for cap in self.capabilities {
|
||||
writer.write_all(b"\t")?;
|
||||
writer.write_all(cap.name.as_bytes())?;
|
||||
match cap.value {
|
||||
Value::Canceled => writer.write_all(b"@")?,
|
||||
Value::Boolean => {}
|
||||
Value::Numeric(v) => write!(writer, "#{v}")?,
|
||||
Value::String(v) => write!(writer, "={v}")?,
|
||||
}
|
||||
writer.write_all(b",\n")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Encode to an owned `String`.
|
||||
pub fn encode_to_string(&self) -> String {
|
||||
let mut buf = Vec::new();
|
||||
// Writing to a Vec cannot fail.
|
||||
let _ = self.encode(&mut buf);
|
||||
String::from_utf8_lossy(&buf).into_owned()
|
||||
}
|
||||
|
||||
/// Returns a map for all of the capabilities in this terminfo. The key is
|
||||
/// the hex-encoded (uppercase) capability name and the value is what
|
||||
/// should be sent as a response to XTGETTCAP.
|
||||
/// Important: the value is the FULL response including the escape
|
||||
/// sequences.
|
||||
///
|
||||
/// Panics on canceled capabilities (unsupported, like the Zig
|
||||
/// `@compileError`) and on `^` control characters that are not at the
|
||||
/// start of a string.
|
||||
pub fn xtgettcap_map(&self) -> HashMap<String, String> {
|
||||
let mut kvs: Vec<(String, String)> = Vec::with_capacity(self.capabilities.len() + 3);
|
||||
|
||||
// We have all of our capabilities plus TN, Co, and RGB which aren't
|
||||
// in the capabilities list but are query-able.
|
||||
kvs.push(("TN".into(), self.names[0].into()));
|
||||
kvs.push(("Co".into(), "256".into()));
|
||||
kvs.push(("RGB".into(), "8".into()));
|
||||
for cap in self.capabilities {
|
||||
let value = match cap.value {
|
||||
Value::Canceled => panic!("canceled not handled yet"),
|
||||
Value::Boolean => String::new(),
|
||||
Value::String(v) => {
|
||||
// If a string contains parameters, then we do not escape
|
||||
// anything within the string. I BELIEVE the history here is
|
||||
// xterm initially only supported specific capabilities and
|
||||
// none had parameters so it returned the tcap encoded form.
|
||||
// Later, Kitty added support for more capabilities some of
|
||||
// which have parameters. But Kitty returned them in
|
||||
// terminfo source format. So we need to handle both cases.
|
||||
if v.contains('%') {
|
||||
v.to_string()
|
||||
} else {
|
||||
// No-parameters. Encode and return.
|
||||
// First replace \E with the escape char (0x1B)
|
||||
let mut result = v.replace("\\E", "\x1b");
|
||||
// Replace '^' with the control char version of that char.
|
||||
while let Some(idx) = result.find('^') {
|
||||
assert!(idx == 0, "handle control-char in middle of string");
|
||||
let c = result.as_bytes()[idx + 1];
|
||||
let replacement = match c {
|
||||
b'?' => 0x7F, // DEL, special cased from ncurses
|
||||
c => c - 64,
|
||||
};
|
||||
let pat = result[idx..idx + 2].to_string();
|
||||
result = result.replace(&pat, &char::from(replacement).to_string());
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
Value::Numeric(v) => v.to_string(),
|
||||
};
|
||||
kvs.push((cap.name.to_string(), value));
|
||||
}
|
||||
|
||||
// Now go through and convert them all to hex-encoded strings.
|
||||
kvs.into_iter()
|
||||
.map(|(k, v)| {
|
||||
let key = hexencode(k.as_bytes());
|
||||
let out = if v.is_empty() {
|
||||
format!("\x1bP1+r{key}\x1b\\")
|
||||
} else {
|
||||
format!("\x1bP1+r{key}={}\x1b\\", hexencode(v.as_bytes()))
|
||||
};
|
||||
(key, out)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Uppercase hex encoding (`std.fmt.bytesToHex(.., .upper)`).
|
||||
pub fn hexencode(input: &[u8]) -> String {
|
||||
use std::fmt::Write;
|
||||
let mut s = String::with_capacity(input.len() * 2);
|
||||
for b in input {
|
||||
let _ = write!(s, "{b:02X}");
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const fn cap(name: &'static str, value: Value) -> Capability {
|
||||
Capability { name, value }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xtgettcap_map() {
|
||||
static SRC: Source = Source {
|
||||
names: &["ghostty", "xterm-ghostty", "Ghostty"],
|
||||
capabilities: &[
|
||||
cap("am", Value::Boolean),
|
||||
cap("colors", Value::Numeric(256)),
|
||||
cap("kx", Value::String("^?")),
|
||||
cap("kbs", Value::String("^H")),
|
||||
cap("kf1", Value::String("\\EOP")),
|
||||
cap("Smulx", Value::String("\\E[4:%p1%dm")),
|
||||
],
|
||||
};
|
||||
|
||||
let map = SRC.xtgettcap_map();
|
||||
assert_eq!("\x1bP1+r616D\x1b\\", map[&hexencode(b"am")]);
|
||||
assert_eq!("\x1bP1+r6B78=7F\x1b\\", map[&hexencode(b"kx")]);
|
||||
assert_eq!("\x1bP1+r6B6273=08\x1b\\", map[&hexencode(b"kbs")]);
|
||||
assert_eq!("\x1bP1+r6B6631=1B4F50\x1b\\", map[&hexencode(b"kf1")]);
|
||||
assert_eq!(
|
||||
"\x1bP1+r536D756C78=5C455B343A25703125646D\x1b\\",
|
||||
map[&hexencode(b"Smulx")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode() {
|
||||
static SRC: Source = Source {
|
||||
names: &["ghostty", "xterm-ghostty", "Ghostty"],
|
||||
capabilities: &[
|
||||
cap("am", Value::Boolean),
|
||||
cap("ccc", Value::Canceled),
|
||||
cap("colors", Value::Numeric(256)),
|
||||
cap("bel", Value::String("^G")),
|
||||
],
|
||||
};
|
||||
|
||||
let expected = "ghostty|xterm-ghostty|Ghostty,\n\tam,\n\tccc@,\n\tcolors#256,\n\tbel=^G,\n";
|
||||
assert_eq!(expected, SRC.encode_to_string());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
xterm-ghostty|ghostty|Ghostty,
|
||||
am,
|
||||
bce,
|
||||
ccc,
|
||||
hs,
|
||||
km,
|
||||
mc5i,
|
||||
mir,
|
||||
msgr,
|
||||
npc,
|
||||
xenl,
|
||||
AX,
|
||||
Tc,
|
||||
Su,
|
||||
XT,
|
||||
fullkbd,
|
||||
colors#256,
|
||||
cols#80,
|
||||
it#8,
|
||||
lines#24,
|
||||
pairs#32767,
|
||||
acsc=++\,\,--..00``aaffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,
|
||||
Smulx=\E[4:%p1%dm,
|
||||
Setulc=\E[58:2::%p1%{65536}%/%d:%p1%{256}%/%{255}%&%d:%p1%{255}%&%d%;m,
|
||||
Ss=\E[%p1%d q,
|
||||
Se=\E[2 q,
|
||||
Ms=\E]52;%p1%s;%p2%s\007,
|
||||
Sync=\E[?2026%?%p1%{1}%-%tl%eh%;,
|
||||
BD=\E[?2004l,
|
||||
BE=\E[?2004h,
|
||||
PS=\E[200~,
|
||||
PE=\E[201~,
|
||||
XM=\E[?1006;1000%?%p1%{1}%=%th%el%;,
|
||||
xm=\E[<%i%p3%d;%p1%d;%p2%d;%?%p4%tM%em%;,
|
||||
RV=\E[>c,
|
||||
rv=\E\\[[0-9]+;[0-9]+;[0-9]+c,
|
||||
XR=\E[>0q,
|
||||
xr=\EP>\\|[ -~]+a\E\\,
|
||||
Enmg=\E[?69h,
|
||||
Dsmg=\E[?69l,
|
||||
Clmg=\E[s,
|
||||
Cmg=\E[%i%p1%d;%p2%ds,
|
||||
clear=\E[H\E[2J,
|
||||
E3=\E[3J,
|
||||
fe=\E[?1004h,
|
||||
fd=\E[?1004l,
|
||||
kxIN=\E[I,
|
||||
kxOUT=\E[O,
|
||||
bel=^G,
|
||||
blink=\E[5m,
|
||||
bold=\E[1m,
|
||||
cbt=\E[Z,
|
||||
civis=\E[?25l,
|
||||
cnorm=\E[?12l\E[?25h,
|
||||
cr=\r,
|
||||
csr=\E[%i%p1%d;%p2%dr,
|
||||
cub=\E[%p1%dD,
|
||||
cub1=^H,
|
||||
cud=\E[%p1%dB,
|
||||
cud1=^J,
|
||||
cuf=\E[%p1%dC,
|
||||
cuf1=\E[C,
|
||||
cup=\E[%i%p1%d;%p2%dH,
|
||||
cuu=\E[%p1%dA,
|
||||
cuu1=\E[A,
|
||||
cvvis=\E[?12;25h,
|
||||
dch=\E[%p1%dP,
|
||||
dch1=\E[P,
|
||||
dim=\E[2m,
|
||||
dl=\E[%p1%dM,
|
||||
dl1=\E[M,
|
||||
dsl=\E]2;\007,
|
||||
ech=\E[%p1%dX,
|
||||
ed=\E[J,
|
||||
el=\E[K,
|
||||
el1=\E[1K,
|
||||
flash=\E[?5h$<100/>\E[?5l,
|
||||
fsl=^G,
|
||||
home=\E[H,
|
||||
hpa=\E[%i%p1%dG,
|
||||
ht=^I,
|
||||
hts=\EH,
|
||||
ich=\E[%p1%d@,
|
||||
ich1=\E[@,
|
||||
il=\E[%p1%dL,
|
||||
il1=\E[L,
|
||||
ind=\n,
|
||||
indn=\E[%p1%dS,
|
||||
initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\,
|
||||
invis=\E[8m,
|
||||
oc=\E]104\007,
|
||||
op=\E[39;49m,
|
||||
rc=\E8,
|
||||
rep=%p1%c\E[%p2%{1}%-%db,
|
||||
rev=\E[7m,
|
||||
ri=\EM,
|
||||
rin=\E[%p1%dT,
|
||||
ritm=\E[23m,
|
||||
rmacs=\E(B,
|
||||
rmam=\E[?7l,
|
||||
rmcup=\E[?1049l,
|
||||
rmir=\E[4l,
|
||||
rmkx=\E[?1l\E>,
|
||||
rmso=\E[27m,
|
||||
rmul=\E[24m,
|
||||
rmxx=\E[29m,
|
||||
setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m,
|
||||
setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m,
|
||||
setrgbb=\E[48:2:%p1%d:%p2%d:%p3%dm,
|
||||
setrgbf=\E[38:2:%p1%d:%p2%d:%p3%dm,
|
||||
sgr=%?%p9%t\E(0%e\E(B%;\E[0%?%p6%t;1%;%?%p5%t;2%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m,
|
||||
sgr0=\E(B\E[m,
|
||||
sitm=\E[3m,
|
||||
smacs=\E(0,
|
||||
smam=\E[?7h,
|
||||
smcup=\E[?1049h,
|
||||
smir=\E[4h,
|
||||
smkx=\E[?1h\E=,
|
||||
smso=\E[7m,
|
||||
smul=\E[4m,
|
||||
smxx=\E[9m,
|
||||
tbc=\E[3g,
|
||||
tsl=\E]2;,
|
||||
u6=\E[%i%d;%dR,
|
||||
u7=\E[6n,
|
||||
u8=\E[?%[;0123456789]c,
|
||||
u9=\E[c,
|
||||
vpa=\E[%i%p1%dd,
|
||||
kDC=\E[3;2~,
|
||||
kDC3=\E[3;3~,
|
||||
kDC4=\E[3;4~,
|
||||
kDC5=\E[3;5~,
|
||||
kDC6=\E[3;6~,
|
||||
kDC7=\E[3;7~,
|
||||
kDN=\E[1;2B,
|
||||
kDN3=\E[1;3B,
|
||||
kDN4=\E[1;4B,
|
||||
kDN5=\E[1;5B,
|
||||
kDN6=\E[1;6B,
|
||||
kDN7=\E[1;7B,
|
||||
kEND=\E[1;2F,
|
||||
kEND3=\E[1;3F,
|
||||
kEND4=\E[1;4F,
|
||||
kEND5=\E[1;5F,
|
||||
kEND6=\E[1;6F,
|
||||
kEND7=\E[1;7F,
|
||||
kHOM=\E[1;2H,
|
||||
kHOM3=\E[1;3H,
|
||||
kHOM4=\E[1;4H,
|
||||
kHOM5=\E[1;5H,
|
||||
kHOM6=\E[1;6H,
|
||||
kHOM7=\E[1;7H,
|
||||
kIC=\E[2;2~,
|
||||
kIC3=\E[2;3~,
|
||||
kIC4=\E[2;4~,
|
||||
kIC5=\E[2;5~,
|
||||
kIC6=\E[2;6~,
|
||||
kIC7=\E[2;7~,
|
||||
kLFT=\E[1;2D,
|
||||
kLFT3=\E[1;3D,
|
||||
kLFT4=\E[1;4D,
|
||||
kLFT5=\E[1;5D,
|
||||
kLFT6=\E[1;6D,
|
||||
kLFT7=\E[1;7D,
|
||||
kNXT=\E[6;2~,
|
||||
kNXT3=\E[6;3~,
|
||||
kNXT4=\E[6;4~,
|
||||
kNXT5=\E[6;5~,
|
||||
kNXT6=\E[6;6~,
|
||||
kNXT7=\E[6;7~,
|
||||
kPRV=\E[5;2~,
|
||||
kPRV3=\E[5;3~,
|
||||
kPRV4=\E[5;4~,
|
||||
kPRV5=\E[5;5~,
|
||||
kPRV6=\E[5;6~,
|
||||
kPRV7=\E[5;7~,
|
||||
kRIT=\E[1;2C,
|
||||
kRIT3=\E[1;3C,
|
||||
kRIT4=\E[1;4C,
|
||||
kRIT5=\E[1;5C,
|
||||
kRIT6=\E[1;6C,
|
||||
kRIT7=\E[1;7C,
|
||||
kUP=\E[1;2A,
|
||||
kUP3=\E[1;3A,
|
||||
kUP4=\E[1;4A,
|
||||
kUP5=\E[1;5A,
|
||||
kUP6=\E[1;6A,
|
||||
kUP7=\E[1;7A,
|
||||
kbs=^?,
|
||||
kcbt=\E[Z,
|
||||
kcub1=\EOD,
|
||||
kcud1=\EOB,
|
||||
kcuf1=\EOC,
|
||||
kcuu1=\EOA,
|
||||
kdch1=\E[3~,
|
||||
kend=\EOF,
|
||||
kent=\EOM,
|
||||
kf1=\EOP,
|
||||
kf10=\E[21~,
|
||||
kf11=\E[23~,
|
||||
kf12=\E[24~,
|
||||
kf13=\E[1;2P,
|
||||
kf14=\E[1;2Q,
|
||||
kf15=\E[1;2R,
|
||||
kf16=\E[1;2S,
|
||||
kf17=\E[15;2~,
|
||||
kf18=\E[17;2~,
|
||||
kf19=\E[18;2~,
|
||||
kf2=\EOQ,
|
||||
kf20=\E[19;2~,
|
||||
kf21=\E[20;2~,
|
||||
kf22=\E[21;2~,
|
||||
kf23=\E[23;2~,
|
||||
kf24=\E[24;2~,
|
||||
kf25=\E[1;5P,
|
||||
kf26=\E[1;5Q,
|
||||
kf27=\E[1;5R,
|
||||
kf28=\E[1;5S,
|
||||
kf29=\E[15;5~,
|
||||
kf3=\EOR,
|
||||
kf30=\E[17;5~,
|
||||
kf31=\E[18;5~,
|
||||
kf32=\E[19;5~,
|
||||
kf33=\E[20;5~,
|
||||
kf34=\E[21;5~,
|
||||
kf35=\E[23;5~,
|
||||
kf36=\E[24;5~,
|
||||
kf37=\E[1;6P,
|
||||
kf38=\E[1;6Q,
|
||||
kf39=\E[1;6R,
|
||||
kf4=\EOS,
|
||||
kf40=\E[1;6S,
|
||||
kf41=\E[15;6~,
|
||||
kf42=\E[17;6~,
|
||||
kf43=\E[18;6~,
|
||||
kf44=\E[19;6~,
|
||||
kf45=\E[20;6~,
|
||||
kf46=\E[21;6~,
|
||||
kf47=\E[23;6~,
|
||||
kf48=\E[24;6~,
|
||||
kf49=\E[1;3P,
|
||||
kf5=\E[15~,
|
||||
kf50=\E[1;3Q,
|
||||
kf51=\E[1;3R,
|
||||
kf52=\E[1;3S,
|
||||
kf53=\E[15;3~,
|
||||
kf54=\E[17;3~,
|
||||
kf55=\E[18;3~,
|
||||
kf56=\E[19;3~,
|
||||
kf57=\E[20;3~,
|
||||
kf58=\E[21;3~,
|
||||
kf59=\E[23;3~,
|
||||
kf6=\E[17~,
|
||||
kf60=\E[24;3~,
|
||||
kf61=\E[1;4P,
|
||||
kf62=\E[1;4Q,
|
||||
kf63=\E[1;4R,
|
||||
kf7=\E[18~,
|
||||
kf8=\E[19~,
|
||||
kf9=\E[20~,
|
||||
khome=\EOH,
|
||||
kich1=\E[2~,
|
||||
kind=\E[1;2B,
|
||||
kmous=\E[<,
|
||||
knp=\E[6~,
|
||||
kpp=\E[5~,
|
||||
kri=\E[1;2A,
|
||||
rs1=\E]\E\\\Ec,
|
||||
sc=\E7,
|
||||
@@ -0,0 +1,100 @@
|
||||
//! Integration test: run a real child attached to a pty.
|
||||
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::Read,
|
||||
os::fd::{AsRawFd, OwnedFd},
|
||||
};
|
||||
|
||||
use ghostty_termio::{Command, Pty, Winsize, command::Exit, pty::child_pre_exec};
|
||||
|
||||
fn dup(fd: &OwnedFd) -> OwnedFd {
|
||||
fd.try_clone().unwrap()
|
||||
}
|
||||
|
||||
/// Read from the master until the child side hangs up (EIO on Linux).
|
||||
fn read_until_hangup(master: OwnedFd) -> String {
|
||||
let mut f = File::from(master);
|
||||
let mut out = Vec::new();
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
match f.read(&mut buf) {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(n) => out.extend_from_slice(&buf[..n]),
|
||||
}
|
||||
}
|
||||
String::from_utf8_lossy(&out).into_owned()
|
||||
}
|
||||
|
||||
fn run_on_pty(script: &str, size: Winsize) -> (String, Exit) {
|
||||
let mut pty = Pty::open(size).unwrap();
|
||||
let slave = pty.slave.take().unwrap();
|
||||
|
||||
let (master_raw, _) = pty.raw_fds();
|
||||
let slave_raw = slave.as_raw_fd();
|
||||
|
||||
let mut cmd = Command::new("/bin/sh", &["/bin/sh", "-c", script]);
|
||||
cmd.stdin = Some(dup(&slave));
|
||||
cmd.stdout = Some(dup(&slave));
|
||||
cmd.stderr = Some(dup(&slave));
|
||||
cmd.os_pre_exec = Some(Box::new(move || {
|
||||
// Make the slave our controlling terminal in a new session; this
|
||||
// also closes the master/slave descriptors in the child.
|
||||
match child_pre_exec(master_raw, slave_raw) {
|
||||
Ok(()) => None,
|
||||
Err(_) => Some(1),
|
||||
}
|
||||
}));
|
||||
cmd.start().unwrap();
|
||||
|
||||
// Close our copies of the slave so we see a hangup when the child exits.
|
||||
drop(cmd.stdin.take());
|
||||
drop(cmd.stdout.take());
|
||||
drop(cmd.stderr.take());
|
||||
drop(slave);
|
||||
|
||||
let exit = cmd.wait(true).unwrap();
|
||||
let out = read_until_hangup(pty.master);
|
||||
(out, exit)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_and_stty_size_on_pty() {
|
||||
let size = Winsize {
|
||||
ws_row: 31,
|
||||
ws_col: 97,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
};
|
||||
let (out, exit) = run_on_pty("echo hi; stty size", size);
|
||||
assert_eq!(exit, Exit::Exited(0));
|
||||
// A pty translates \n to \r\n.
|
||||
assert_eq!(out, "hi\r\n31 97\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_has_controlling_terminal() {
|
||||
// `tty` succeeds only with a terminal on stdin; /dev/tty only opens when
|
||||
// we have a controlling terminal (TIOCSCTTY).
|
||||
let (out, exit) = run_on_pty(
|
||||
"tty >/dev/null && echo tty; echo x > /dev/tty && echo ctty",
|
||||
Winsize::default(),
|
||||
);
|
||||
assert_eq!(exit, Exit::Exited(0));
|
||||
assert!(out.contains("tty\r\n"), "{out:?}");
|
||||
assert!(out.contains("ctty\r\n"), "{out:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_is_visible_to_child() {
|
||||
let size = Winsize {
|
||||
ws_row: 10,
|
||||
ws_col: 20,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
};
|
||||
let (out, _) = run_on_pty("stty size", size);
|
||||
assert_eq!(out, "10 20\r\n");
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//! The terminfo source emitted by the Rust port must be byte-identical to the
|
||||
|
||||
#![allow(clippy::unwrap_used)]
|
||||
//! output of the Zig `terminfo/ghostty.zig` `encode` (golden file generated
|
||||
//! with the Zig reference implementation).
|
||||
|
||||
use ghostty_termio::terminfo;
|
||||
|
||||
#[test]
|
||||
fn terminfo_source_matches_zig_output() {
|
||||
let expected = include_str!("data/xterm-ghostty.terminfo");
|
||||
assert_eq!(expected, terminfo::ghostty.encode_to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminfo_compiles_with_tic_if_available() {
|
||||
let Ok(dir) = std::env::var("CARGO_TARGET_TMPDIR") else {
|
||||
return;
|
||||
};
|
||||
let src = std::path::Path::new(&dir).join("xterm-ghostty.src");
|
||||
std::fs::write(&src, terminfo::ghostty.encode_to_string()).unwrap();
|
||||
let out = std::path::Path::new(&dir).join("terminfo-out");
|
||||
let _ = std::fs::create_dir_all(&out);
|
||||
let Ok(status) = std::process::Command::new("tic")
|
||||
.arg("-x")
|
||||
.arg("-o")
|
||||
.arg(&out)
|
||||
.arg(&src)
|
||||
.status()
|
||||
else {
|
||||
return; // tic not installed
|
||||
};
|
||||
assert!(status.success());
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
# ghostty-termio notes (agent IO1)
|
||||
|
||||
Linux only. Crate lints: `unsafe_code = "deny"` (workspace clippy table copied
|
||||
into the crate's `[lints.clippy]`); `#[allow(unsafe_code)]` only on tiny items
|
||||
with `// SAFETY:` (fork, execvpe, _exit, close, sigaction, borrow_raw).
|
||||
|
||||
## File -> module
|
||||
- `pty.zig` -> `pty.rs` (`Pty`, `Winsize`, `Mode`, `child_pre_exec(master, slave)`)
|
||||
- `Command.zig` (+ `apprt/gtk/{pre_exec,post_fork}.zig` cgroup logic, `cgroup.zig` fmtScope)
|
||||
-> `command.rs`
|
||||
- `termio/shell_integration.zig` -> `shell_integration.rs`
|
||||
- `terminfo/{main,Source,ghostty}.zig` -> `terminfo/{mod,source,ghostty}.rs`
|
||||
(`terminfo::ghostty` is a `const Source`; `encode`, `encode_to_string`,
|
||||
`xtgettcap_map`). Output verified byte-identical with the Zig `encode` (golden
|
||||
file `tests/data/xterm-ghostty.terminfo`, generated with `zig run`).
|
||||
- Shell scripts copied verbatim to `resources/shell-integration/` (workspace root).
|
||||
|
||||
## Not ported (deferred; need ghostty-terminal / renderer)
|
||||
Exec.zig, Termio.zig, Thread.zig, stream_handler.zig, mailbox.zig, message.zig,
|
||||
backend.zig, Options.zig (all reference `terminal`/`renderer`/`apprt` types).
|
||||
|
||||
## Design / deviations
|
||||
- Deps `ghostty-config` and `ghostty-terminal` REMOVED from Cargo.toml (note in file).
|
||||
Deps: ghostty-os, rustix (pty, termios, process, fs, stdio), nix (signal), libc, log, thiserror.
|
||||
- `Command` does its own `fork` + `libc::execvpe` (not std::process::Command) so the
|
||||
child setup is exactly Ghostty's: dup2 stdio (CLOEXEC cleared), best-effort chdir
|
||||
(failure ignored), restore rlimit (`restore_rlimit: Option<Rlimit>` replaces
|
||||
`global_state.rlimits`), `os_pre_exec` then `rt_pre_exec` hooks (`Fn() -> Option<u8>`,
|
||||
`Some(code)` => `_exit(code)`; panics => exit 1), then execvpe. All allocation happens
|
||||
before fork. Exec failure: child prints the same "Requested executable not found" message
|
||||
(other errors: `errno=N` instead of the Zig error name) to stderr and `_exit(1)`; there
|
||||
is no `ExecFailedInChild` error because the child can never return into caller code.
|
||||
`exit_immediately(code)` is provided for hooks that want to exit (use instead of
|
||||
`std::process::exit`).
|
||||
- Signal mask: Zig never reset the mask explicitly in Command either; only `Pty::child_pre_exec`
|
||||
resets 13 signal dispositions to SIG_DFL (same list as Zig), then setsid + TIOCSCTTY and
|
||||
closes slave+master. Close-all-fds is not done (Zig doesn't either; master is CLOEXEC).
|
||||
- stdio fields are `Option<OwnedFd>` (callers pass `try_clone()`d fds); `wait(block)` returns
|
||||
`Result<Exit, Errno>`; non-blocking wait loops until a status is available (as Zig, with a
|
||||
`yield_now`). `setData/getData` dropped (closures capture state). Empty `args` => argv =
|
||||
[path] (what the Zig doc comment promises; Zig code itself passed an empty argv).
|
||||
- Inherited env is snapshotted with `vars_os()` before fork (Zig passes `environ`).
|
||||
- `Pty`: `slave: Option<OwnedFd>`; both fds close on drop (Zig never auto-closed the slave).
|
||||
Master opened CLOEXEC, slave not (like glibc openpty + explicit CLOEXEC). IUTF8 set.
|
||||
- cgroup: `linux_cgroup_pre_exec(&RtPreExecInfo)` (child side, waits <=250ms) and
|
||||
`linux_cgroup_post_fork(cmd, &RtPostForkInfo, create_scope)` where `create_scope` is a
|
||||
closure standing in for the GIO D-Bus `cgroup.createScope`. `fmt_scope(pid)`.
|
||||
Local `LinuxCgroup`/`GtkSingleInstance`/`Rt*Info` types: TODO(integration) from ghostty-config.
|
||||
- shell_integration: local `Command` (Shell/Direct) with `ArgIteratorGeneral` quoting rules,
|
||||
`ShellIntegrationFeatures` (same defaults) - TODO(integration) replace with config types.
|
||||
`EnvMap = HashMap<String,String>`. `setup(resource_dir, &Command, &mut EnvMap, Option<Shell>)`
|
||||
-> `Result<Option<ShellIntegration>, Error>` (Error only from `homedir::home`).
|
||||
Darwin `/bin/bash` special case dropped.
|
||||
|
||||
## Tests (52 total: 47 unit + 5 integration, all passing)
|
||||
Zig tests ported (35): pty 1; Command 10 (createNullDelimitedEnvMap as `prepare()`, os/rt pre exec
|
||||
1+2, rt post fork, redirect stdout, custom env, custom cwd, execve failure); shell_integration 21
|
||||
(force shell, failure, detectShell, setup features, bash x8, xdg x3, nushell x3, zsh x3);
|
||||
terminfo 3 (xtgettcap map, Source encode, ghostty encode).
|
||||
Skipped: Windows/NullPty/Windows env block helpers, `refAllDecls` tests.
|
||||
Extras: pty cloexec/IUTF8/mode, Exit decoding, signal exit, nonblocking wait, invalid cwd ignored,
|
||||
fd leak check, cgroup mode matrices, arg splitter, bundled-resources check, terminfo golden vs Zig,
|
||||
`tic -x` compile of the generated source (skipped if tic absent),
|
||||
`tests/pty_command.rs`: real `/bin/sh -c 'echo hi; stty size'` on a 31x97 pty (output
|
||||
`hi\r\n31 97\r\n`), controlling-tty check, resize check.
|
||||
|
||||
## Known gaps / TODO(integration)
|
||||
- Config-derived types above; `restore_rlimit` from global state; D-Bus scope creation.
|
||||
- `Pty::child_pre_exec` is meant to be called from Exec's `os_pre_exec` (see tests/pty_command.rs
|
||||
for the wiring). Flatpak passwd path in ghostty-os needs Pty (ghostty-os TODO) - now available.
|
||||
- Hooks run after fork in a possibly multithreaded process; `linux_cgroup_pre_exec` allocates
|
||||
and sleeps (same as Zig) - fine on glibc.
|
||||
@@ -0,0 +1,117 @@
|
||||
# Shell Integration Code
|
||||
|
||||
This is the shell-specific shell-integration code that is
|
||||
used for the shell-integration feature set that Ghostty
|
||||
supports.
|
||||
|
||||
This README is meant as developer documentation and not as
|
||||
user documentation. For user documentation, see the main
|
||||
README or [ghostty.org](https://ghostty.org/docs)
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Bash
|
||||
|
||||
Automatic [Bash](https://www.gnu.org/software/bash/) shell integration works by
|
||||
starting Bash in POSIX mode and using the `ENV` environment variable to load
|
||||
our integration script (`bash/ghostty.bash`). This prevents Bash from loading
|
||||
its normal startup files, which becomes our script's responsibility (along with
|
||||
disabling POSIX mode).
|
||||
|
||||
Bash shell integration can also be sourced manually from `bash/ghostty.bash`.
|
||||
This also works for older versions of Bash.
|
||||
|
||||
```bash
|
||||
# Ghostty shell integration for Bash. This must be at the top of your bashrc!
|
||||
if [ -n "${GHOSTTY_RESOURCES_DIR}" ]; then
|
||||
builtin source "${GHOSTTY_RESOURCES_DIR}/shell-integration/bash/ghostty.bash"
|
||||
fi
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> The version of Bash distributed with macOS (`/bin/bash`) does not support
|
||||
> automatic shell integration. You'll need to manually source the shell
|
||||
> integration script (as shown above). You can also install a standard
|
||||
> version of Bash from Homebrew or elsewhere and set it as your shell.
|
||||
|
||||
### Elvish
|
||||
|
||||
For [Elvish](https://elv.sh), `$GHOSTTY_RESOURCES_DIR/src/shell-integration`
|
||||
contains an `./elvish/lib/ghostty-integration.elv` file.
|
||||
|
||||
Elvish, on startup, searches for paths defined in `XDG_DATA_DIRS`
|
||||
variable for `./elvish/lib/*.elv` files and imports them. They are thus
|
||||
made available for use as modules by way of `use <filename>`.
|
||||
|
||||
Ghostty launches Elvish, passing the environment with `XDG_DATA_DIRS` prepended
|
||||
with `$GHOSTTY_RESOURCES_DIR/src/shell-integration`. It contains
|
||||
`./elvish/lib/ghostty-integration.elv`. The user can then import it
|
||||
by `use ghostty-integration` every time after shell startup or
|
||||
autostart integration in `$XDG_CONFIG_HOME/elvish/rc.elv`,
|
||||
which will run the integration routines.
|
||||
|
||||
If you decide to autostart `ghostty-integration` with `rc.elv`, you should
|
||||
detect whether the terminal is Ghostty or not. To do this, add this to the end
|
||||
of your `rc.elv` file:
|
||||
|
||||
```elvish
|
||||
if (eq $E:TERM "xterm-ghostty") {
|
||||
try { use ghostty-integration } catch { }
|
||||
}
|
||||
```
|
||||
|
||||
The [Elvish](https://elv.sh) shell integration is supported by
|
||||
the community and is not officially supported by Ghostty. We distribute
|
||||
it for ease of access and use but do not provide support for it.
|
||||
If you experience issues with the Elvish shell integration, I welcome
|
||||
any contributions to fix them. Thank you!
|
||||
|
||||
### Fish
|
||||
|
||||
For [Fish](https://fishshell.com/), Ghostty prepends to the
|
||||
`XDG_DATA_DIRS` directory. Fish automatically loads configuration
|
||||
files in `<XDG_DATA_DIR>/fish/vendor_conf.d/*.fish` on startup,
|
||||
allowing us to automatically integrate with the shell. For details
|
||||
on the Fish startup process, see the
|
||||
[Fish documentation](https://fishshell.com/docs/current/language.html).
|
||||
|
||||
### Nushell
|
||||
|
||||
For [Nushell](https://www.nushell.sh/), Ghostty prepends to the
|
||||
`XDG_DATA_DIRS` directory, making the `ghostty` module available through
|
||||
Nushell's vendor autoload mechanism. Ghostty then automatically imports
|
||||
the module using the `-e "use ghostty *"` flag when starting Nushell.
|
||||
|
||||
Nushell provides many shell features itself, such as `title` and `cursor`,
|
||||
so our integration focuses on Ghostty-specific features like `sudo`,
|
||||
`ssh-env`, and `ssh-terminfo`.
|
||||
|
||||
The shell integration is automatically enabled when running Nushell in Ghostty,
|
||||
but you can also load it manually is shell integration is disabled:
|
||||
|
||||
```nushell
|
||||
source $GHOSTTY_RESOURCES_DIR/shell-integration/nushell/vendor/autoload/ghostty.nu
|
||||
use ghostty *
|
||||
```
|
||||
|
||||
### Zsh
|
||||
|
||||
Automatic [Zsh](https://www.zsh.org/) integration works by temporarily setting
|
||||
`ZDOTDIR` to our `zsh` directory. An existing `ZDOTDIR` environment variable
|
||||
value will be retained and restored after our shell integration scripts are
|
||||
run.
|
||||
|
||||
However, if `ZDOTDIR` is set in a system-wide file like `/etc/zshenv`, it will
|
||||
override Ghostty's `ZDOTDIR` value, preventing the shell integration from being
|
||||
loaded. In this case, the shell integration needs to be loaded manually.
|
||||
|
||||
To load the Zsh shell integration manually:
|
||||
|
||||
```zsh
|
||||
if [[ -n $GHOSTTY_RESOURCES_DIR ]]; then
|
||||
source "$GHOSTTY_RESOURCES_DIR"/shell-integration/zsh/ghostty-integration
|
||||
fi
|
||||
```
|
||||
|
||||
Shell integration requires Zsh 5.1+.
|
||||
@@ -0,0 +1,382 @@
|
||||
# bash-preexec.sh -- Bash support for ZSH-like 'preexec' and 'precmd' functions.
|
||||
# https://github.com/rcaloras/bash-preexec
|
||||
#
|
||||
#
|
||||
# 'preexec' functions are executed before each interactive command is
|
||||
# executed, with the interactive command as its argument. The 'precmd'
|
||||
# function is executed before each prompt is displayed.
|
||||
#
|
||||
# Author: Ryan Caloras (ryan@bashhub.com)
|
||||
# Forked from Original Author: Glyph Lefkowitz
|
||||
#
|
||||
# V0.6.0
|
||||
#
|
||||
|
||||
# General Usage:
|
||||
#
|
||||
# 1. Source this file at the end of your bash profile so as not to interfere
|
||||
# with anything else that's using PROMPT_COMMAND.
|
||||
#
|
||||
# 2. Add any precmd or preexec functions by appending them to their arrays:
|
||||
# e.g.
|
||||
# precmd_functions+=(my_precmd_function)
|
||||
# precmd_functions+=(some_other_precmd_function)
|
||||
#
|
||||
# preexec_functions+=(my_preexec_function)
|
||||
#
|
||||
# 3. Consider changing anything using the DEBUG trap or PROMPT_COMMAND
|
||||
# to use preexec and precmd instead. Preexisting usages will be
|
||||
# preserved, but doing so manually may be less surprising.
|
||||
#
|
||||
# Note: This module requires two Bash features which you must not otherwise be
|
||||
# using: the "DEBUG" trap, and the "PROMPT_COMMAND" variable. If you override
|
||||
# either of these after bash-preexec has been installed it will most likely break.
|
||||
|
||||
# Tell shellcheck what kind of file this is.
|
||||
# shellcheck shell=bash
|
||||
|
||||
# Make sure this is bash that's running and return otherwise.
|
||||
# Use POSIX syntax for this line:
|
||||
if [ -z "${BASH_VERSION-}" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# We only support Bash 3.1+.
|
||||
# Note: BASH_VERSINFO is first available in Bash-2.0.
|
||||
if [[ -z "${BASH_VERSINFO-}" ]] || (( BASH_VERSINFO[0] < 3 || (BASH_VERSINFO[0] == 3 && BASH_VERSINFO[1] < 1) )); then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Avoid duplicate inclusion
|
||||
if [[ -n "${bash_preexec_imported:-}" || -n "${__bp_imported:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
bash_preexec_imported="defined"
|
||||
|
||||
# WARNING: This variable is no longer used and should not be relied upon.
|
||||
# Use ${bash_preexec_imported} instead.
|
||||
# shellcheck disable=SC2034
|
||||
__bp_imported="${bash_preexec_imported}"
|
||||
|
||||
# Should be available to each precmd and preexec
|
||||
# functions, should they want it. $? and $_ are available as $? and $_, but
|
||||
# $PIPESTATUS is available only in a copy, $BP_PIPESTATUS.
|
||||
# TODO: Figure out how to restore PIPESTATUS before each precmd or preexec
|
||||
# function.
|
||||
__bp_last_ret_value="$?"
|
||||
BP_PIPESTATUS=("${PIPESTATUS[@]}")
|
||||
__bp_last_argument_prev_command="$_"
|
||||
|
||||
__bp_inside_precmd=0
|
||||
__bp_inside_preexec=0
|
||||
|
||||
# Initial PROMPT_COMMAND string that is removed from PROMPT_COMMAND post __bp_install
|
||||
__bp_install_string=$'__bp_trap_string="$(trap -p DEBUG)"\ntrap - DEBUG\n__bp_install'
|
||||
|
||||
# Fails if any of the given variables are readonly
|
||||
# Reference https://stackoverflow.com/a/4441178
|
||||
__bp_require_not_readonly() {
|
||||
local var
|
||||
for var; do
|
||||
if ! ( unset "$var" 2> /dev/null ); then
|
||||
echo "bash-preexec requires write access to ${var}" >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Remove ignorespace and or replace ignoreboth from HISTCONTROL
|
||||
# so we can accurately invoke preexec with a command from our
|
||||
# history even if it starts with a space.
|
||||
__bp_adjust_histcontrol() {
|
||||
local histcontrol
|
||||
histcontrol="${HISTCONTROL:-}"
|
||||
histcontrol="${histcontrol//ignorespace}"
|
||||
# Replace ignoreboth with ignoredups
|
||||
if [[ "$histcontrol" == *"ignoreboth"* ]]; then
|
||||
histcontrol="ignoredups:${histcontrol//ignoreboth}"
|
||||
fi
|
||||
export HISTCONTROL="$histcontrol"
|
||||
}
|
||||
|
||||
# This variable describes whether we are currently in "interactive mode";
|
||||
# i.e. whether this shell has just executed a prompt and is waiting for user
|
||||
# input. It documents whether the current command invoked by the trace hook is
|
||||
# run interactively by the user; it's set immediately after the prompt hook,
|
||||
# and unset as soon as the trace hook is run.
|
||||
__bp_preexec_interactive_mode=""
|
||||
|
||||
# These arrays are used to add functions to be run before, or after, prompts.
|
||||
declare -a precmd_functions
|
||||
declare -a preexec_functions
|
||||
|
||||
# Trims leading and trailing whitespace from $2 and writes it to the variable
|
||||
# name passed as $1
|
||||
__bp_trim_whitespace() {
|
||||
local var=${1:?} text=${2:-}
|
||||
text="${text#"${text%%[![:space:]]*}"}" # remove leading whitespace characters
|
||||
text="${text%"${text##*[![:space:]]}"}" # remove trailing whitespace characters
|
||||
printf -v "$var" '%s' "$text"
|
||||
}
|
||||
|
||||
|
||||
# Trims whitespace and removes any leading or trailing semicolons from $2 and
|
||||
# writes the resulting string to the variable name passed as $1. Used for
|
||||
# manipulating substrings in PROMPT_COMMAND
|
||||
__bp_sanitize_string() {
|
||||
local var=${1:?} text=${2:-} sanitized
|
||||
__bp_trim_whitespace sanitized "$text"
|
||||
sanitized=${sanitized%;}
|
||||
sanitized=${sanitized#;}
|
||||
__bp_trim_whitespace sanitized "$sanitized"
|
||||
printf -v "$var" '%s' "$sanitized"
|
||||
}
|
||||
|
||||
# This function is installed as part of the PROMPT_COMMAND;
|
||||
# It sets a variable to indicate that the prompt was just displayed,
|
||||
# to allow the DEBUG trap to know that the next command is likely interactive.
|
||||
__bp_interactive_mode() {
|
||||
__bp_preexec_interactive_mode="on"
|
||||
}
|
||||
|
||||
|
||||
# This function is installed as part of the PROMPT_COMMAND.
|
||||
# It will invoke any functions defined in the precmd_functions array.
|
||||
__bp_precmd_invoke_cmd() {
|
||||
# Save the returned value from our last command, and from each process in
|
||||
# its pipeline. Note: this MUST be the first thing done in this function.
|
||||
# BP_PIPESTATUS may be unused, ignore
|
||||
# shellcheck disable=SC2034
|
||||
|
||||
__bp_last_ret_value="$?" BP_PIPESTATUS=("${PIPESTATUS[@]}")
|
||||
|
||||
# Don't invoke precmds if we are inside an execution of an "original
|
||||
# prompt command" by another precmd execution loop. This avoids infinite
|
||||
# recursion.
|
||||
if (( __bp_inside_precmd > 0 )); then
|
||||
return
|
||||
fi
|
||||
local __bp_inside_precmd=1
|
||||
|
||||
# Invoke every function defined in our function array.
|
||||
local precmd_function
|
||||
for precmd_function in "${precmd_functions[@]}"; do
|
||||
|
||||
# Only execute this function if it actually exists.
|
||||
# Test existence of functions with: declare -[Ff]
|
||||
if type -t "$precmd_function" 1>/dev/null; then
|
||||
__bp_set_ret_value "$__bp_last_ret_value" "$__bp_last_argument_prev_command"
|
||||
# Quote our function invocation to prevent issues with IFS
|
||||
"$precmd_function"
|
||||
fi
|
||||
done
|
||||
|
||||
__bp_set_ret_value "$__bp_last_ret_value"
|
||||
}
|
||||
|
||||
# Sets a return value in $?. We may want to get access to the $? variable in our
|
||||
# precmd functions. This is available for instance in zsh. We can simulate it in bash
|
||||
# by setting the value here.
|
||||
__bp_set_ret_value() {
|
||||
return ${1:+"$1"}
|
||||
}
|
||||
|
||||
__bp_in_prompt_command() {
|
||||
|
||||
local prompt_command_array IFS=$'\n;'
|
||||
read -rd '' -a prompt_command_array <<< "${PROMPT_COMMAND[*]:-}"
|
||||
|
||||
local trimmed_arg
|
||||
__bp_trim_whitespace trimmed_arg "${1:-}"
|
||||
|
||||
local command trimmed_command
|
||||
for command in "${prompt_command_array[@]:-}"; do
|
||||
__bp_trim_whitespace trimmed_command "$command"
|
||||
if [[ "$trimmed_command" == "$trimmed_arg" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# This function is installed as the DEBUG trap. It is invoked before each
|
||||
# interactive prompt display. Its purpose is to inspect the current
|
||||
# environment to attempt to detect if the current command is being invoked
|
||||
# interactively, and invoke 'preexec' if so.
|
||||
__bp_preexec_invoke_exec() {
|
||||
|
||||
# Save the contents of $_ so that it can be restored later on.
|
||||
# https://stackoverflow.com/questions/40944532/bash-preserve-in-a-debug-trap#40944702
|
||||
__bp_last_argument_prev_command="${1:-}"
|
||||
# Don't invoke preexecs if we are inside of another preexec.
|
||||
if (( __bp_inside_preexec > 0 )); then
|
||||
return
|
||||
fi
|
||||
local __bp_inside_preexec=1
|
||||
|
||||
# Checks if the file descriptor is not standard out (i.e. '1')
|
||||
# __bp_delay_install checks if we're in test. Needed for bats to run.
|
||||
# Prevents preexec from being invoked for functions in PS1
|
||||
if [[ ! -t 1 && -z "${__bp_delay_install:-}" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -n "${COMP_POINT:-}" || -n "${READLINE_POINT:-}" ]]; then
|
||||
# We're in the middle of a completer or a keybinding set up by "bind
|
||||
# -x". This obviously can't be an interactively issued command.
|
||||
return
|
||||
fi
|
||||
if [[ -z "${__bp_preexec_interactive_mode:-}" ]]; then
|
||||
# We're doing something related to displaying the prompt. Let the
|
||||
# prompt set the title instead of me.
|
||||
return
|
||||
else
|
||||
# If we're in a subshell, then the prompt won't be re-displayed to put
|
||||
# us back into interactive mode, so let's not set the variable back.
|
||||
# In other words, if you have a subshell like
|
||||
# (sleep 1; sleep 2)
|
||||
# You want to see the 'sleep 2' as a set_command_title as well.
|
||||
if [[ 0 -eq "${BASH_SUBSHELL:-}" ]]; then
|
||||
__bp_preexec_interactive_mode=""
|
||||
fi
|
||||
fi
|
||||
|
||||
if __bp_in_prompt_command "${BASH_COMMAND:-}"; then
|
||||
# If we're executing something inside our prompt_command then we don't
|
||||
# want to call preexec. Bash prior to 3.1 can't detect this at all :/
|
||||
__bp_preexec_interactive_mode=""
|
||||
return
|
||||
fi
|
||||
|
||||
local this_command
|
||||
this_command=$(LC_ALL=C HISTTIMEFORMAT='' builtin history 1)
|
||||
this_command="${this_command#*[[:digit:]][* ] }"
|
||||
|
||||
# Sanity check to make sure we have something to invoke our function with.
|
||||
if [[ -z "$this_command" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Invoke every function defined in our function array.
|
||||
local preexec_function
|
||||
local preexec_function_ret_value
|
||||
local preexec_ret_value=0
|
||||
for preexec_function in "${preexec_functions[@]:-}"; do
|
||||
|
||||
# Only execute each function if it actually exists.
|
||||
# Test existence of function with: declare -[fF]
|
||||
if type -t "$preexec_function" 1>/dev/null; then
|
||||
__bp_set_ret_value "${__bp_last_ret_value:-}"
|
||||
# Quote our function invocation to prevent issues with IFS
|
||||
"$preexec_function" "$this_command"
|
||||
preexec_function_ret_value="$?"
|
||||
if [[ "$preexec_function_ret_value" != 0 ]]; then
|
||||
preexec_ret_value="$preexec_function_ret_value"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Restore the last argument of the last executed command, and set the return
|
||||
# value of the DEBUG trap to be the return code of the last preexec function
|
||||
# to return an error.
|
||||
# If `extdebug` is enabled a non-zero return value from any preexec function
|
||||
# will cause the user's command not to execute.
|
||||
# Run `shopt -s extdebug` to enable
|
||||
__bp_set_ret_value "$preexec_ret_value" "$__bp_last_argument_prev_command"
|
||||
}
|
||||
|
||||
__bp_install() {
|
||||
# Exit if we already have this installed.
|
||||
if [[ "${PROMPT_COMMAND[*]:-}" == *"__bp_precmd_invoke_cmd"* ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
trap '__bp_preexec_invoke_exec "$_"' DEBUG
|
||||
|
||||
# Preserve any prior DEBUG trap as a preexec function
|
||||
eval "local trap_argv=(${__bp_trap_string:-})"
|
||||
local prior_trap=${trap_argv[2]:-}
|
||||
unset __bp_trap_string
|
||||
if [[ -n "$prior_trap" ]]; then
|
||||
eval '__bp_original_debug_trap() {
|
||||
'"$prior_trap"'
|
||||
}'
|
||||
preexec_functions+=(__bp_original_debug_trap)
|
||||
fi
|
||||
|
||||
# Adjust our HISTCONTROL Variable if needed.
|
||||
#
|
||||
# GHOSTTY: Don't modify HISTCONTROL. This hack is only needed to improve the
|
||||
# accuracy of the command argument passed to the preexec functions, and we
|
||||
# don't use that argument in our bash shell integration script (and nor does
|
||||
# the __bp_original_debug_trap function above, which is the only other active
|
||||
# preexec function).
|
||||
#__bp_adjust_histcontrol
|
||||
|
||||
# Issue #25. Setting debug trap for subshells causes sessions to exit for
|
||||
# backgrounded subshell commands (e.g. (pwd)& ). Believe this is a bug in Bash.
|
||||
#
|
||||
# Disabling this by default. It can be enabled by setting this variable.
|
||||
if [[ -n "${__bp_enable_subshells:-}" ]]; then
|
||||
|
||||
# Set so debug trap will work be invoked in subshells.
|
||||
set -o functrace > /dev/null 2>&1
|
||||
shopt -s extdebug > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
local existing_prompt_command
|
||||
# Remove setting our trap install string and sanitize the existing prompt command string
|
||||
existing_prompt_command="${PROMPT_COMMAND:-}"
|
||||
# Edge case of appending to PROMPT_COMMAND
|
||||
existing_prompt_command="${existing_prompt_command//$__bp_install_string/:}" # no-op
|
||||
existing_prompt_command="${existing_prompt_command//$'\n':$'\n'/$'\n'}" # remove known-token only
|
||||
existing_prompt_command="${existing_prompt_command//$'\n':;/$'\n'}" # remove known-token only
|
||||
__bp_sanitize_string existing_prompt_command "$existing_prompt_command"
|
||||
if [[ "${existing_prompt_command:-:}" == ":" ]]; then
|
||||
existing_prompt_command=
|
||||
fi
|
||||
|
||||
# Install our hooks in PROMPT_COMMAND to allow our trap to know when we've
|
||||
# actually entered something.
|
||||
PROMPT_COMMAND='__bp_precmd_invoke_cmd'
|
||||
PROMPT_COMMAND+=${existing_prompt_command:+$'\n'$existing_prompt_command}
|
||||
if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1) )); then
|
||||
PROMPT_COMMAND+=('__bp_interactive_mode')
|
||||
else
|
||||
# shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0
|
||||
PROMPT_COMMAND+=$'\n__bp_interactive_mode'
|
||||
fi
|
||||
|
||||
# Add two functions to our arrays for convenience
|
||||
# of definition.
|
||||
precmd_functions+=(precmd)
|
||||
preexec_functions+=(preexec)
|
||||
|
||||
# Invoke our two functions manually that were added to $PROMPT_COMMAND
|
||||
__bp_precmd_invoke_cmd
|
||||
__bp_interactive_mode
|
||||
}
|
||||
|
||||
# Sets an installation string as part of our PROMPT_COMMAND to install
|
||||
# after our session has started. This allows bash-preexec to be included
|
||||
# at any point in our bash profile.
|
||||
__bp_install_after_session_init() {
|
||||
# bash-preexec needs to modify these variables in order to work correctly
|
||||
# if it can't, just stop the installation
|
||||
__bp_require_not_readonly PROMPT_COMMAND HISTCONTROL HISTTIMEFORMAT || return
|
||||
|
||||
local sanitized_prompt_command
|
||||
__bp_sanitize_string sanitized_prompt_command "${PROMPT_COMMAND:-}"
|
||||
if [[ -n "$sanitized_prompt_command" ]]; then
|
||||
# shellcheck disable=SC2178 # PROMPT_COMMAND is not an array in bash <= 5.0
|
||||
PROMPT_COMMAND=${sanitized_prompt_command}$'\n'
|
||||
fi
|
||||
# shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0
|
||||
PROMPT_COMMAND+=${__bp_install_string}
|
||||
}
|
||||
|
||||
# Run our install so long as we're not delaying it.
|
||||
if [[ -z "${__bp_delay_install:-}" ]]; then
|
||||
__bp_install_after_session_init
|
||||
fi
|
||||
@@ -0,0 +1,315 @@
|
||||
# Parts of this script are based on Kitty's bash integration. Kitty is
|
||||
# distributed under GPLv3, so this file is also distributed under GPLv3.
|
||||
# The license header is reproduced below:
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# We need to be in interactive mode to proceed.
|
||||
if [[ "$-" != *i* ]]; then builtin return; fi
|
||||
|
||||
# When automatic shell integration is active, we were started in POSIX
|
||||
# mode and need to manually recreate the bash startup sequence.
|
||||
if [ -n "$GHOSTTY_BASH_INJECT" ]; then
|
||||
# Store a temporary copy of our startup flags and unset these global
|
||||
# environment variables so we can safely handle reentrancy.
|
||||
builtin declare __ghostty_bash_flags="$GHOSTTY_BASH_INJECT"
|
||||
builtin unset ENV GHOSTTY_BASH_INJECT
|
||||
|
||||
# Restore an existing ENV that was replaced by the shell integration code.
|
||||
if [[ -n "$GHOSTTY_BASH_ENV" ]]; then
|
||||
builtin export ENV=$GHOSTTY_BASH_ENV
|
||||
builtin unset GHOSTTY_BASH_ENV
|
||||
fi
|
||||
|
||||
# Restore bash's default 'posix' behavior. Also reset 'inherit_errexit',
|
||||
# which doesn't happen as part of the 'posix' reset.
|
||||
builtin set +o posix
|
||||
builtin shopt -u inherit_errexit 2>/dev/null
|
||||
|
||||
# Unexport HISTFILE if it was set by the shell integration code.
|
||||
if [[ -n "$GHOSTTY_BASH_UNEXPORT_HISTFILE" ]]; then
|
||||
builtin export -n HISTFILE
|
||||
builtin unset GHOSTTY_BASH_UNEXPORT_HISTFILE
|
||||
fi
|
||||
|
||||
# Manually source the startup files. See INVOCATION in bash(1) and
|
||||
# run_startup_files() in shell.c in the Bash source code.
|
||||
if builtin shopt -q login_shell; then
|
||||
if [[ $__ghostty_bash_flags != *"--noprofile"* ]]; then
|
||||
[ -r /etc/profile ] && builtin source "/etc/profile"
|
||||
for __ghostty_rcfile in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do
|
||||
[ -r "$__ghostty_rcfile" ] && {
|
||||
builtin source "$__ghostty_rcfile"
|
||||
break
|
||||
}
|
||||
done
|
||||
fi
|
||||
else
|
||||
if [[ $__ghostty_bash_flags != *"--norc"* ]]; then
|
||||
# The location of the system bashrc is determined at bash build
|
||||
# time via -DSYS_BASHRC and can therefore vary across distros:
|
||||
# Arch, Debian, Ubuntu use /etc/bash.bashrc
|
||||
# Fedora uses /etc/bashrc sourced from ~/.bashrc instead of SYS_BASHRC
|
||||
# Void Linux uses /etc/bash/bashrc
|
||||
# Nixos uses /etc/bashrc
|
||||
for __ghostty_rcfile in /etc/bash.bashrc /etc/bash/bashrc /etc/bashrc; do
|
||||
[ -r "$__ghostty_rcfile" ] && {
|
||||
builtin source "$__ghostty_rcfile"
|
||||
break
|
||||
}
|
||||
done
|
||||
if [[ -z "$GHOSTTY_BASH_RCFILE" ]]; then GHOSTTY_BASH_RCFILE="$HOME/.bashrc"; fi
|
||||
[ -r "$GHOSTTY_BASH_RCFILE" ] && builtin source "$GHOSTTY_BASH_RCFILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
builtin unset __ghostty_rcfile
|
||||
builtin unset __ghostty_bash_flags
|
||||
builtin unset GHOSTTY_BASH_RCFILE
|
||||
fi
|
||||
|
||||
# Add Ghostty binary to PATH if the path feature is enabled
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"path"* && -n "$GHOSTTY_BIN_DIR" ]]; then
|
||||
if [[ ":$PATH:" != *":$GHOSTTY_BIN_DIR:"* ]]; then
|
||||
export PATH="$PATH:$GHOSTTY_BIN_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Sudo
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then
|
||||
# Wrap `sudo` command to ensure Ghostty terminfo is preserved.
|
||||
#
|
||||
# This approach supports wrapping a `sudo` alias, but the alias definition
|
||||
# must come _after_ this function is defined. Otherwise, the alias expansion
|
||||
# will take precedence over this function, and it won't be wrapped.
|
||||
function sudo {
|
||||
builtin local sudo_has_sudoedit_flags="no"
|
||||
for arg in "$@"; do
|
||||
# Check if argument is '-e' or '--edit' (sudoedit flags)
|
||||
if [[ "$arg" == "-e" || $arg == "--edit" ]]; then
|
||||
sudo_has_sudoedit_flags="yes"
|
||||
builtin break
|
||||
fi
|
||||
# Check if argument is neither an option nor a key-value pair
|
||||
if [[ "$arg" != -* && "$arg" != *=* ]]; then
|
||||
builtin break
|
||||
fi
|
||||
done
|
||||
if [[ "$sudo_has_sudoedit_flags" == "yes" ]]; then
|
||||
builtin command sudo "$@"
|
||||
else
|
||||
builtin command sudo --preserve-env=TERMINFO "$@"
|
||||
fi
|
||||
}
|
||||
fi
|
||||
|
||||
# SSH Integration
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then
|
||||
function ssh() {
|
||||
builtin local ssh_term ssh_opts
|
||||
ssh_term="xterm-256color"
|
||||
ssh_opts=()
|
||||
|
||||
# Configure environment variables for remote session
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]]; then
|
||||
ssh_opts+=(-o "SetEnv COLORTERM=truecolor")
|
||||
ssh_opts+=(-o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION")
|
||||
fi
|
||||
|
||||
# Install terminfo on remote host if needed
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-terminfo* ]]; then
|
||||
builtin local ssh_user ssh_hostname
|
||||
|
||||
while IFS=' ' read -r ssh_key ssh_value; do
|
||||
case "$ssh_key" in
|
||||
user) ssh_user="$ssh_value" ;;
|
||||
hostname) ssh_hostname="$ssh_value" ;;
|
||||
esac
|
||||
[[ -n "$ssh_user" && -n "$ssh_hostname" ]] && break
|
||||
done < <(builtin command ssh -G "$@" 2>/dev/null)
|
||||
|
||||
if [[ -n "$ssh_hostname" ]]; then
|
||||
builtin local ssh_target="${ssh_user}@${ssh_hostname}"
|
||||
|
||||
# Check if terminfo is already cached
|
||||
if "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --host="$ssh_target" >/dev/null 2>&1; then
|
||||
ssh_term="xterm-ghostty"
|
||||
elif builtin command -v infocmp >/dev/null 2>&1; then
|
||||
builtin local ssh_terminfo ssh_cpath_dir ssh_cpath
|
||||
|
||||
ssh_terminfo=$(infocmp -0 -x xterm-ghostty 2>/dev/null)
|
||||
|
||||
if [[ -n "$ssh_terminfo" ]]; then
|
||||
builtin echo "Setting up xterm-ghostty terminfo on $ssh_hostname..." >&2
|
||||
|
||||
ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$"
|
||||
ssh_cpath="$ssh_cpath_dir/socket"
|
||||
|
||||
if builtin echo "$ssh_terminfo" | builtin command ssh -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" '
|
||||
infocmp xterm-ghostty >/dev/null 2>&1 && exit 0
|
||||
command -v tic >/dev/null 2>&1 || exit 1
|
||||
mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0
|
||||
exit 1
|
||||
' 2>/dev/null; then
|
||||
ssh_term="xterm-ghostty"
|
||||
ssh_opts+=(-o "ControlPath=$ssh_cpath")
|
||||
|
||||
# Cache successful installation
|
||||
"$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true
|
||||
else
|
||||
builtin echo "Warning: Failed to install terminfo." >&2
|
||||
fi
|
||||
else
|
||||
builtin echo "Warning: Could not generate terminfo data." >&2
|
||||
fi
|
||||
else
|
||||
builtin echo "Warning: ghostty command not available for cache management." >&2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Execute SSH with TERM environment variable
|
||||
TERM="$ssh_term" builtin command ssh "${ssh_opts[@]}" "$@"
|
||||
}
|
||||
fi
|
||||
|
||||
# This is set to 1 when we're executing a command so that we don't
|
||||
# send prompt marks multiple times.
|
||||
_ghostty_executing=""
|
||||
_ghostty_last_reported_cwd=""
|
||||
|
||||
function __ghostty_precmd() {
|
||||
local ret="$?"
|
||||
if test "$_ghostty_executing" != "0"; then
|
||||
_GHOSTTY_SAVE_PS1="$PS1"
|
||||
_GHOSTTY_SAVE_PS2="$PS2"
|
||||
|
||||
# Use 133;P (not 133;A) inside PS1 to avoid fresh-line behavior on
|
||||
# readline redraws (e.g., vi mode switches, Ctrl-L). The initial
|
||||
# 133;A with fresh-line is emitted once via printf below.
|
||||
PS1='\[\e]133;P;k=i\a\]'$PS1'\[\e]133;B\a\]'
|
||||
PS2='\[\e]133;P;k=s\a\]'$PS2'\[\e]133;B\a\]'
|
||||
|
||||
# Bash doesn't redraw the leading lines in a multiline prompt so we mark
|
||||
# the start of each line (after each newline) as a secondary prompt. This
|
||||
# correctly handles multiline prompts by setting the first to primary and
|
||||
# the subsequent lines to secondary.
|
||||
#
|
||||
# We only replace the \n prompt escape, not literal newlines ($'\n'),
|
||||
# because literal newlines may appear inside $(...) command substitutions
|
||||
# where inserting escape sequences would break shell syntax.
|
||||
if [[ "$PS1" == *"\n"* ]]; then
|
||||
PS1="${PS1//\\n/\\n$'\\[\\e]133;P;k=s\\a\\]'}"
|
||||
fi
|
||||
|
||||
# Cursor
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then
|
||||
builtin local cursor=5 # blinking bar
|
||||
[[ "$GHOSTTY_SHELL_FEATURES" == *"cursor:steady"* ]] && cursor=6 # steady bar
|
||||
|
||||
[[ "$PS1" != *"\[\e[${cursor} q\]"* ]] && PS1=$PS1"\[\e[${cursor} q\]"
|
||||
[[ "$PS0" != *'\[\e[0 q\]'* ]] && PS0=$PS0'\[\e[0 q\]' # reset
|
||||
fi
|
||||
|
||||
# Title (working directory)
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
|
||||
PS1=$PS1'\[\e]2;\w\a\]'
|
||||
fi
|
||||
fi
|
||||
|
||||
if test "$_ghostty_executing" != ""; then
|
||||
# End of current command. Report its status.
|
||||
builtin printf "\e]133;D;%s;aid=%s\a" "$ret" "$BASHPID"
|
||||
fi
|
||||
|
||||
# Fresh line and start of prompt.
|
||||
builtin printf "\e]133;A;redraw=last;cl=line;aid=%s\a" "$BASHPID"
|
||||
|
||||
# unfortunately bash provides no hooks to detect cwd changes
|
||||
# in particular this means cwd reporting will not happen for a
|
||||
# command like cd /test && cat. PS0 is evaluated before cd is run.
|
||||
if [[ "$_ghostty_last_reported_cwd" != "$PWD" ]]; then
|
||||
_ghostty_last_reported_cwd="$PWD"
|
||||
builtin printf "\e]7;kitty-shell-cwd://%s%s\a" "$HOSTNAME" "$PWD"
|
||||
fi
|
||||
|
||||
_ghostty_executing=0
|
||||
}
|
||||
|
||||
function __ghostty_preexec() {
|
||||
builtin local cmd="$1"
|
||||
|
||||
PS1="$_GHOSTTY_SAVE_PS1"
|
||||
PS2="$_GHOSTTY_SAVE_PS2"
|
||||
|
||||
# Title (current command)
|
||||
if [[ -n $cmd && "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
|
||||
builtin printf "\e]2;%s\a" "${cmd//[[:cntrl:]]/}"
|
||||
fi
|
||||
|
||||
# End of input, start of output.
|
||||
builtin printf "\e]133;C;\a"
|
||||
_ghostty_executing=1
|
||||
}
|
||||
|
||||
if (( BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 4) )); then
|
||||
__ghostty_preexec_hook() {
|
||||
builtin local cmd
|
||||
cmd=$(LC_ALL=C HISTTIMEFORMAT='' builtin history 1)
|
||||
cmd="${cmd#*[[:digit:]][* ] }" # remove leading history number
|
||||
[[ -n "$cmd" ]] && __ghostty_preexec "$cmd"
|
||||
}
|
||||
|
||||
# Use function substitution in 5.3+. Otherwise, use command substitution.
|
||||
# Any output (including escape sequences) goes to the terminal.
|
||||
# Only define if not already set (allows re-sourcing).
|
||||
if [[ -z "${__ghostty_ps0+x}" ]]; then
|
||||
if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 3) )); then
|
||||
# shellcheck disable=SC2016
|
||||
builtin readonly __ghostty_ps0='${ __ghostty_preexec_hook; }'
|
||||
else
|
||||
# shellcheck disable=SC2016
|
||||
builtin readonly __ghostty_ps0='$(__ghostty_preexec_hook >/dev/tty)'
|
||||
fi
|
||||
fi
|
||||
|
||||
__ghostty_hook() {
|
||||
builtin local ret=$?
|
||||
__ghostty_precmd "$ret"
|
||||
if [[ "$PS0" != *"$__ghostty_ps0"* ]]; then
|
||||
PS0=$PS0"${__ghostty_ps0}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Append our hook to PROMPT_COMMAND, preserving its existing type.
|
||||
# shellcheck disable=SC2128,SC2178,SC2179
|
||||
if [[ ";${PROMPT_COMMAND[*]:-};" != *";__ghostty_hook;"* ]]; then
|
||||
if [[ -z "${PROMPT_COMMAND[*]}" ]]; then
|
||||
if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1) )); then
|
||||
PROMPT_COMMAND=(__ghostty_hook)
|
||||
else
|
||||
PROMPT_COMMAND="__ghostty_hook"
|
||||
fi
|
||||
elif [[ $(builtin declare -p PROMPT_COMMAND 2>/dev/null) == "declare -a "* ]]; then
|
||||
PROMPT_COMMAND+=(__ghostty_hook)
|
||||
else
|
||||
[[ "${PROMPT_COMMAND}" =~ \;[[:space:]]*$ ]] || PROMPT_COMMAND+=";"
|
||||
PROMPT_COMMAND+=" __ghostty_hook"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
builtin source "$(dirname -- "${BASH_SOURCE[0]}")/bash-preexec.sh"
|
||||
preexec_functions+=(__ghostty_preexec)
|
||||
precmd_functions+=(__ghostty_precmd)
|
||||
fi
|
||||
@@ -0,0 +1,191 @@
|
||||
{
|
||||
use platform
|
||||
use str
|
||||
|
||||
# Clean up XDG_DATA_DIRS by removing GHOSTTY_SHELL_INTEGRATION_XDG_DIR
|
||||
if (and (has-env GHOSTTY_SHELL_INTEGRATION_XDG_DIR) (has-env XDG_DATA_DIRS)) {
|
||||
set-env XDG_DATA_DIRS (str:replace $E:GHOSTTY_SHELL_INTEGRATION_XDG_DIR":" "" $E:XDG_DATA_DIRS)
|
||||
unset-env GHOSTTY_SHELL_INTEGRATION_XDG_DIR
|
||||
}
|
||||
|
||||
# List of enabled shell integration features
|
||||
var features = [(str:split ',' $E:GHOSTTY_SHELL_FEATURES)]
|
||||
|
||||
# State tracking for semantic prompt sequences
|
||||
# Values: 'prompt-start', 'pre-exec', 'post-exec'
|
||||
fn set-prompt-state {|new| set-env __ghostty_prompt_state $new }
|
||||
|
||||
fn mark-prompt-start {
|
||||
if (not-eq $E:__ghostty_prompt_state 'prompt-start') {
|
||||
printf "\e]133;D;aid="$pid"\a"
|
||||
}
|
||||
set-prompt-state 'prompt-start'
|
||||
printf "\e]133;A;aid="$pid"\a"
|
||||
}
|
||||
|
||||
fn mark-output-start {|_|
|
||||
set-prompt-state 'pre-exec'
|
||||
printf "\e]133;C\a"
|
||||
}
|
||||
|
||||
fn mark-output-end {|cmd-info|
|
||||
set-prompt-state 'post-exec'
|
||||
|
||||
var exit-status = 0
|
||||
|
||||
# in case of error: retrieve exit status,
|
||||
# unless does not exist (= builtin function failure), then default to 1
|
||||
if (not-eq $nil $cmd-info[error]) {
|
||||
set exit-status = 1
|
||||
|
||||
if (has-key $cmd-info[error] reason) {
|
||||
if (has-key $cmd-info[error][reason] exit-status) {
|
||||
set exit-status = $cmd-info[error][reason][exit-status]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printf "\e]133;D;"$exit-status";aid="$pid"\a"
|
||||
}
|
||||
|
||||
# NOTE: OSC 133;B (end of prompt, start of input) cannot be reliably
|
||||
# implemented at the script level in Elvish. The prompt function's output is
|
||||
# escaped, and writing to /dev/tty has timing issues because Elvish renders
|
||||
# its prompts on a background thread. Full semantic prompt support requires a
|
||||
# native implementation: https://github.com/elves/elvish/pull/1917
|
||||
|
||||
fn sudo-with-terminfo {|@args|
|
||||
var sudoedit = $false
|
||||
for arg $args {
|
||||
if (str:has-prefix $arg --) {
|
||||
if (eq $arg --edit) {
|
||||
set sudoedit = $true
|
||||
break
|
||||
}
|
||||
} elif (str:has-prefix $arg -) {
|
||||
if (str:contains (str:trim-prefix $arg -) e) {
|
||||
set sudoedit = $true
|
||||
break
|
||||
}
|
||||
} elif (not (str:contains $arg =)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (not $sudoedit) { set args = [ --preserve-env=TERMINFO $@args ] }
|
||||
(external sudo) $@args
|
||||
}
|
||||
|
||||
fn ssh-integration {|@args|
|
||||
var ssh-term = "xterm-256color"
|
||||
var ssh-opts = []
|
||||
|
||||
# Configure environment variables for remote session
|
||||
if (has-value $features ssh-env) {
|
||||
set ssh-opts = (conj $ssh-opts ^
|
||||
-o "SetEnv COLORTERM=truecolor" ^
|
||||
-o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION")
|
||||
}
|
||||
|
||||
if (has-value $features ssh-terminfo) {
|
||||
var ssh-user = ""
|
||||
var ssh-hostname = ""
|
||||
|
||||
# Parse ssh config
|
||||
for line [((external ssh) -G $@args)] {
|
||||
var parts = [(str:fields $line)]
|
||||
if (> (count $parts) 1) {
|
||||
var ssh-key = $parts[0]
|
||||
var ssh-value = $parts[1]
|
||||
if (eq $ssh-key user) {
|
||||
set ssh-user = $ssh-value
|
||||
} elif (eq $ssh-key hostname) {
|
||||
set ssh-hostname = $ssh-value
|
||||
}
|
||||
if (and (not-eq $ssh-user "") (not-eq $ssh-hostname "")) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (not-eq $ssh-hostname "") {
|
||||
var ghostty = $E:GHOSTTY_BIN_DIR/"ghostty"
|
||||
var ssh-target = $ssh-user"@"$ssh-hostname
|
||||
|
||||
# Check if terminfo is already cached
|
||||
if (bool ?($ghostty +ssh-cache --host=$ssh-target)) {
|
||||
set ssh-term = "xterm-ghostty"
|
||||
} elif (has-external infocmp) {
|
||||
var ssh-terminfo = ((external infocmp) -0 -x xterm-ghostty 2>/dev/null | slurp)
|
||||
|
||||
if (not-eq $ssh-terminfo "") {
|
||||
echo "Setting up xterm-ghostty terminfo on "$ssh-hostname"..." >&2
|
||||
|
||||
use os
|
||||
var ssh-cpath-dir = (os:temp-dir "ghostty-ssh-"$ssh-user".*")
|
||||
var ssh-cpath = $ssh-cpath-dir"/socket"
|
||||
|
||||
if (bool ?(echo $ssh-terminfo | (external ssh) $@ssh-opts -o ControlMaster=yes -o ControlPath=$ssh-cpath -o ControlPersist=60s $@args '
|
||||
infocmp xterm-ghostty >/dev/null 2>&1 && exit 0
|
||||
command -v tic >/dev/null 2>&1 || exit 1
|
||||
mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0
|
||||
exit 1
|
||||
' 2>/dev/null)) {
|
||||
set ssh-term = "xterm-ghostty"
|
||||
set ssh-opts = (conj $ssh-opts -o ControlPath=$ssh-cpath)
|
||||
|
||||
# Cache successful installation
|
||||
$ghostty +ssh-cache --add=$ssh-target >/dev/null
|
||||
} else {
|
||||
echo "Warning: Failed to install terminfo." >&2
|
||||
}
|
||||
} else {
|
||||
echo "Warning: Could not generate terminfo data." >&2
|
||||
}
|
||||
} else {
|
||||
echo "Warning: ghostty command not available for cache management." >&2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with [E:TERM = $ssh-term] {
|
||||
(external ssh) $@ssh-opts $@args
|
||||
}
|
||||
}
|
||||
|
||||
defer {
|
||||
mark-prompt-start
|
||||
}
|
||||
|
||||
set edit:before-readline = (conj $edit:before-readline $mark-prompt-start~)
|
||||
set edit:after-readline = (conj $edit:after-readline $mark-output-start~)
|
||||
set edit:after-command = (conj $edit:after-command $mark-output-end~)
|
||||
|
||||
if (str:contains $E:GHOSTTY_SHELL_FEATURES "cursor") {
|
||||
var cursor = "5" # blinking bar
|
||||
if (has-value $features cursor:steady) {
|
||||
set cursor = "6" # steady bar
|
||||
}
|
||||
|
||||
fn beam { printf "\e["$cursor" q" }
|
||||
fn reset { printf "\e[0 q" }
|
||||
set edit:before-readline = (conj $edit:before-readline $beam~)
|
||||
set edit:after-readline = (conj $edit:after-readline {|_| reset })
|
||||
}
|
||||
if (and (has-value $features path) (has-env GHOSTTY_BIN_DIR)) {
|
||||
if (not (has-value $paths $E:GHOSTTY_BIN_DIR)) {
|
||||
set paths = [$@paths $E:GHOSTTY_BIN_DIR]
|
||||
}
|
||||
}
|
||||
if (and (has-value $features sudo) (not-eq "" $E:TERMINFO) (has-external sudo)) {
|
||||
edit:add-var sudo~ $sudo-with-terminfo~
|
||||
}
|
||||
if (and (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-) (has-external ssh)) {
|
||||
edit:add-var ssh~ $ssh-integration~
|
||||
}
|
||||
|
||||
# Report changes to the current directory.
|
||||
fn report-pwd { printf "\e]7;kitty-shell-cwd://%s%s\a" (platform:hostname) $pwd }
|
||||
set after-chdir = (conj $after-chdir {|_| report-pwd })
|
||||
report-pwd
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
# This shell script aims to be written in a way where it can't really fail
|
||||
# or all failure scenarios are handled, so that we never leave the shell in
|
||||
# a weird state. If you find a way to break this, please report a bug!
|
||||
|
||||
function ghostty_restore_xdg_data_dir -d "restore the original XDG_DATA_DIR value"
|
||||
# If we don't have our own data dir then we don't need to do anything.
|
||||
if not set -q GHOSTTY_SHELL_INTEGRATION_XDG_DIR
|
||||
return
|
||||
end
|
||||
|
||||
# If the data dir isn't set at all then we don't need to do anything.
|
||||
if not set -q XDG_DATA_DIRS
|
||||
return
|
||||
end
|
||||
|
||||
# We need to do this so that XDG_DATA_DIRS turns into an array.
|
||||
set --function --path xdg_data_dirs "$XDG_DATA_DIRS"
|
||||
|
||||
# If our data dir is in the list then remove it.
|
||||
if set --function index (contains --index "$GHOSTTY_SHELL_INTEGRATION_XDG_DIR" $xdg_data_dirs)
|
||||
set --erase --function xdg_data_dirs[$index]
|
||||
end
|
||||
|
||||
# Re-export our data dir
|
||||
if set -q xdg_data_dirs[1]
|
||||
set --global --export --unpath XDG_DATA_DIRS "$xdg_data_dirs"
|
||||
else
|
||||
set --erase --global XDG_DATA_DIRS
|
||||
end
|
||||
|
||||
set --erase GHOSTTY_SHELL_INTEGRATION_XDG_DIR
|
||||
end
|
||||
|
||||
function ghostty_exit -d "exit the shell integration setup"
|
||||
functions -e ghostty_restore_xdg_data_dir
|
||||
functions -e ghostty_exit
|
||||
exit 0
|
||||
end
|
||||
|
||||
# We always try to restore the XDG data dir
|
||||
ghostty_restore_xdg_data_dir
|
||||
|
||||
# If we aren't interactive or we've already run, don't run.
|
||||
status --is-interactive || ghostty_exit
|
||||
|
||||
# We do the full setup on the first prompt render. We do this so that other
|
||||
# shell integrations that setup the prompt and modify things are able to run
|
||||
# first. We want to run _last_.
|
||||
function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
|
||||
functions -e __ghostty_setup
|
||||
|
||||
set --local features (string split , $GHOSTTY_SHELL_FEATURES)
|
||||
|
||||
# Parse the fish version for feature detection.
|
||||
# Default to 0.0 if version is unavailable or malformed.
|
||||
set -l fish_major 0
|
||||
set -l fish_minor 0
|
||||
if set -q version[1]
|
||||
set -l fish_ver (string match -r '(\d+)\.(\d+)' -- $version[1])
|
||||
if set -q fish_ver[2]; and test -n "$fish_ver[2]"
|
||||
set fish_major "$fish_ver[2]"
|
||||
end
|
||||
if set -q fish_ver[3]; and test -n "$fish_ver[3]"
|
||||
set fish_minor "$fish_ver[3]"
|
||||
end
|
||||
end
|
||||
|
||||
# Our OSC133A (prompt start) sequence. If we're using Fish >= 4.1
|
||||
# then it supports click_events so we enable that.
|
||||
set -g __ghostty_prompt_start_mark "\e]133;A\a"
|
||||
if test "$fish_major" -gt 4; or test "$fish_major" -eq 4 -a "$fish_minor" -ge 1
|
||||
set -g __ghostty_prompt_start_mark "\e]133;A;click_events=1\a"
|
||||
end
|
||||
|
||||
if string match -q 'cursor*' -- $features
|
||||
set -l cursor 5 # blinking bar
|
||||
contains cursor:steady $features && set cursor 6 # steady bar
|
||||
|
||||
# Change the cursor to a beam on prompt.
|
||||
function __ghostty_set_cursor_beam --on-event fish_prompt -V cursor -d "Set cursor shape"
|
||||
if not functions -q fish_vi_cursor_handle
|
||||
echo -en "\e[$cursor q"
|
||||
end
|
||||
end
|
||||
function __ghostty_reset_cursor --on-event fish_preexec -d "Reset cursor shape"
|
||||
if not functions -q fish_vi_cursor_handle
|
||||
echo -en "\e[0 q"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Add Ghostty binary to PATH if the path feature is enabled
|
||||
if contains path $features; and test -n "$GHOSTTY_BIN_DIR"
|
||||
fish_add_path --global --path --append "$GHOSTTY_BIN_DIR"
|
||||
end
|
||||
|
||||
# When using sudo shell integration feature, ensure $TERMINFO is set
|
||||
# and `sudo` is not already a function or alias
|
||||
if contains sudo $features; and test -n "$TERMINFO"; and test file = (type -t sudo 2> /dev/null; or echo "x")
|
||||
# Wrap `sudo` command to ensure Ghostty terminfo is preserved
|
||||
function sudo -d "Wrap sudo to preserve terminfo"
|
||||
set --function sudo_has_sudoedit_flags no
|
||||
for arg in $argv
|
||||
# Check if argument is '-e' or '--edit' (sudoedit flags)
|
||||
if string match -q -- -e "$arg"; or string match -q -- --edit "$arg"
|
||||
set --function sudo_has_sudoedit_flags yes
|
||||
break
|
||||
end
|
||||
# Check if argument is neither an option nor a key-value pair
|
||||
if not string match -r -q -- "^-" "$arg"; and not string match -r -q -- "=" "$arg"
|
||||
break
|
||||
end
|
||||
end
|
||||
if test "$sudo_has_sudoedit_flags" = yes
|
||||
command sudo $argv
|
||||
else
|
||||
command sudo --preserve-env=TERMINFO $argv
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# SSH Integration
|
||||
set -l features (string split ',' -- "$GHOSTTY_SHELL_FEATURES")
|
||||
if contains ssh-env $features; or contains ssh-terminfo $features
|
||||
function ssh --wraps=ssh --description "SSH wrapper with Ghostty integration"
|
||||
set -l features (string split ',' -- "$GHOSTTY_SHELL_FEATURES")
|
||||
set -l ssh_term xterm-256color
|
||||
set -l ssh_opts
|
||||
|
||||
# Configure environment variables for remote session
|
||||
if contains ssh-env $features
|
||||
set -a ssh_opts -o "SetEnv COLORTERM=truecolor"
|
||||
set -a ssh_opts -o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION"
|
||||
end
|
||||
|
||||
# Install terminfo on remote host if needed
|
||||
if contains ssh-terminfo $features
|
||||
set -l ssh_user
|
||||
set -l ssh_hostname
|
||||
|
||||
for line in (command ssh -G $argv 2>/dev/null)
|
||||
set -l parts (string split ' ' -- $line)
|
||||
if test (count $parts) -ge 2
|
||||
switch $parts[1]
|
||||
case user
|
||||
set ssh_user $parts[2]
|
||||
case hostname
|
||||
set ssh_hostname $parts[2]
|
||||
end
|
||||
if test -n "$ssh_user"; and test -n "$ssh_hostname"
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if test -n "$ssh_hostname"
|
||||
set -l ssh_target "$ssh_user@$ssh_hostname"
|
||||
|
||||
# Check if terminfo is already cached
|
||||
if test -x "$GHOSTTY_BIN_DIR/ghostty"; and "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --host="$ssh_target" >/dev/null 2>&1
|
||||
set ssh_term xterm-ghostty
|
||||
else if command -q infocmp
|
||||
set -l ssh_terminfo
|
||||
set -l ssh_cpath_dir
|
||||
set -l ssh_cpath
|
||||
|
||||
set ssh_terminfo "$(infocmp -0 -x xterm-ghostty 2>/dev/null)"
|
||||
|
||||
if test -n "$ssh_terminfo"
|
||||
echo "Setting up xterm-ghostty terminfo on $ssh_hostname..." >&2
|
||||
|
||||
set ssh_cpath_dir (mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null; or echo "/tmp/ghostty-ssh-$ssh_user."(random))
|
||||
set ssh_cpath "$ssh_cpath_dir/socket"
|
||||
|
||||
if echo "$ssh_terminfo" | command ssh $ssh_opts -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s $argv '
|
||||
infocmp xterm-ghostty >/dev/null 2>&1 && exit 0
|
||||
command -v tic >/dev/null 2>&1 || exit 1
|
||||
mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0
|
||||
exit 1
|
||||
' 2>/dev/null
|
||||
set ssh_term xterm-ghostty
|
||||
set -a ssh_opts -o "ControlPath=$ssh_cpath"
|
||||
|
||||
# Cache successful installation
|
||||
if test -x "$GHOSTTY_BIN_DIR/ghostty"
|
||||
"$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --add="$ssh_target" >/dev/null 2>&1; or true
|
||||
end
|
||||
else
|
||||
echo "Warning: Failed to install terminfo." >&2
|
||||
end
|
||||
else
|
||||
echo "Warning: Could not generate terminfo data." >&2
|
||||
end
|
||||
else
|
||||
echo "Warning: ghostty command not available for cache management." >&2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Execute SSH with TERM environment variable
|
||||
TERM="$ssh_term" command ssh $ssh_opts $argv
|
||||
end
|
||||
end
|
||||
|
||||
# Setup prompt marking
|
||||
function __ghostty_mark_prompt_start --on-event fish_prompt --on-event fish_posterror
|
||||
# If we never got the output end event, then we need to send it now.
|
||||
if test "$__ghostty_prompt_state" != prompt-start
|
||||
echo -en "\e]133;D\a"
|
||||
end
|
||||
|
||||
set --global __ghostty_prompt_state prompt-start
|
||||
echo -en $__ghostty_prompt_start_mark
|
||||
end
|
||||
|
||||
function __ghostty_mark_output_start --on-event fish_preexec
|
||||
set --global __ghostty_prompt_state pre-exec
|
||||
echo -en "\e]133;C\a"
|
||||
end
|
||||
|
||||
function __ghostty_mark_output_end --on-event fish_postexec
|
||||
set --global __ghostty_prompt_state post-exec
|
||||
echo -en "\e]133;D;$status\a"
|
||||
end
|
||||
|
||||
# Report pwd. This is actually built-in to fish but only for terminals
|
||||
# that match an allowlist and that isn't us.
|
||||
function __update_cwd_osc --on-variable PWD -d 'Notify capable terminals when $PWD changes'
|
||||
if status --is-command-substitution || set -q INSIDE_EMACS
|
||||
return
|
||||
end
|
||||
printf \e\]7\;file://%s%s\a $hostname (string escape --style=url $PWD)
|
||||
end
|
||||
|
||||
# Enable fish to handle reflow because Ghostty clears the prompt on resize.
|
||||
set --global fish_handle_reflow 1
|
||||
|
||||
# Initial calls for first prompt
|
||||
if string match -q 'cursor*' -- $features
|
||||
__ghostty_set_cursor_beam
|
||||
end
|
||||
__ghostty_mark_prompt_start
|
||||
__update_cwd_osc
|
||||
end
|
||||
|
||||
ghostty_exit
|
||||
@@ -0,0 +1,110 @@
|
||||
# Ghostty shell integration
|
||||
export module ghostty {
|
||||
def has_feature [feature: string] {
|
||||
$feature in ($env.GHOSTTY_SHELL_FEATURES | default "" | split row ',')
|
||||
}
|
||||
|
||||
# Wrap `ssh` with Ghostty TERMINFO support
|
||||
export def --wrapped ssh [...args] {
|
||||
mut ssh_env = {}
|
||||
mut ssh_opts = []
|
||||
|
||||
# `ssh-env`: use xterm-256color and propagate COLORTERM/TERM_PROGRAM vars
|
||||
if (has_feature "ssh-env") {
|
||||
$ssh_env.TERM = "xterm-256color"
|
||||
$ssh_opts = [
|
||||
"-o" "SetEnv COLORTERM=truecolor"
|
||||
"-o" "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION"
|
||||
]
|
||||
}
|
||||
|
||||
# `ssh-terminfo`: auto-install xterm-ghostty terminfo on remote hosts
|
||||
if (has_feature "ssh-terminfo") {
|
||||
let ghostty = ($env.GHOSTTY_BIN_DIR? | default "") | path join "ghostty"
|
||||
|
||||
let ssh_cfg = ^ssh -G ...$args
|
||||
| lines
|
||||
| parse "{key} {value}"
|
||||
| where key in ["user" "hostname"]
|
||||
| select key value
|
||||
| transpose -rd
|
||||
| default {user: $env.USER hostname: "localhost"}
|
||||
let ssh_id = $"($ssh_cfg.user)@($ssh_cfg.hostname)"
|
||||
|
||||
if (^$ghostty "+ssh-cache" $"--host=($ssh_id)" | complete | $in.exit_code == 0) {
|
||||
$ssh_env.TERM = "xterm-ghostty"
|
||||
} else {
|
||||
$ssh_env.TERM = "xterm-256color"
|
||||
|
||||
let terminfo = try {
|
||||
^infocmp -0 -x xterm-ghostty
|
||||
} catch {
|
||||
print -e "infocmp failed, using xterm-256color"
|
||||
}
|
||||
|
||||
if ($terminfo | is-not-empty) {
|
||||
print $"Setting up xterm-ghostty terminfo on ($ssh_cfg.hostname)..."
|
||||
|
||||
let ctrl_path = (
|
||||
mktemp -td $"ghostty-ssh-($ssh_cfg.user).XXXXXX"
|
||||
| path join "socket"
|
||||
)
|
||||
|
||||
let remote_args = $ssh_opts ++ [
|
||||
"-o" "ControlMaster=yes"
|
||||
"-o" $"ControlPath=($ctrl_path)"
|
||||
"-o" "ControlPersist=60s"
|
||||
] ++ $args
|
||||
|
||||
$terminfo | ^ssh ...$remote_args '
|
||||
infocmp xterm-ghostty >/dev/null 2>&1 && exit 0
|
||||
command -v tic >/dev/null 2>&1 || exit 1
|
||||
mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0
|
||||
exit 1'
|
||||
| complete
|
||||
| if $in.exit_code == 0 {
|
||||
^$ghostty "+ssh-cache" $"--add=($ssh_id)" e>| print -e
|
||||
$ssh_env.TERM = "xterm-ghostty"
|
||||
$ssh_opts = ($ssh_opts ++ ["-o" $"ControlPath=($ctrl_path)"])
|
||||
} else {
|
||||
print -e "terminfo install failed, using xterm-256color"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ssh_args = $ssh_opts ++ $args
|
||||
with-env $ssh_env {
|
||||
^ssh ...$ssh_args
|
||||
}
|
||||
}
|
||||
|
||||
# Wrap `sudo` to preserve Ghostty's TERMINFO environment variable
|
||||
export def --wrapped sudo [...args] {
|
||||
mut sudo_args = $args
|
||||
|
||||
if (has_feature "sudo") {
|
||||
# Extract just the sudo options (before the command)
|
||||
let sudo_options = (
|
||||
$args | take until {|arg|
|
||||
not (($arg | str starts-with "-") or ($arg | str contains "="))
|
||||
}
|
||||
)
|
||||
|
||||
# Prepend TERMINFO preservation flag if not using sudoedit
|
||||
if (not ("-e" in $sudo_options or "--edit" in $sudo_options)) {
|
||||
$sudo_args = ($args | prepend "--preserve-env=TERMINFO")
|
||||
}
|
||||
}
|
||||
|
||||
^sudo ...$sudo_args
|
||||
}
|
||||
}
|
||||
|
||||
# Clean up XDG_DATA_DIRS by removing GHOSTTY_SHELL_INTEGRATION_XDG_DIR
|
||||
if 'GHOSTTY_SHELL_INTEGRATION_XDG_DIR' in $env {
|
||||
if 'XDG_DATA_DIRS' in $env {
|
||||
$env.XDG_DATA_DIRS = ($env.XDG_DATA_DIRS | str replace $"($env.GHOSTTY_SHELL_INTEGRATION_XDG_DIR):" "")
|
||||
}
|
||||
hide-env GHOSTTY_SHELL_INTEGRATION_XDG_DIR
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
# Based on (started as) a copy of Kitty's zsh integration. Kitty is
|
||||
# distributed under GPLv3, so this file is also distributed under GPLv3.
|
||||
# The license header is reproduced below:
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This script is sourced automatically by zsh when ZDOTDIR is set to this
|
||||
# directory. It therefore assumes it's running within our shell integration
|
||||
# environment and should not be sourced manually (unlike ghostty-integration).
|
||||
#
|
||||
# This file can get sourced with aliases enabled. To avoid alias expansion
|
||||
# we quote everything that can be quoted. Some aliases will still break us
|
||||
# though.
|
||||
|
||||
# Restore the original ZDOTDIR value if GHOSTTY_ZSH_ZDOTDIR is set.
|
||||
# Otherwise, unset the ZDOTDIR that was set during shell injection.
|
||||
if [[ -n "${GHOSTTY_ZSH_ZDOTDIR+X}" ]]; then
|
||||
'builtin' 'export' ZDOTDIR="$GHOSTTY_ZSH_ZDOTDIR"
|
||||
'builtin' 'unset' 'GHOSTTY_ZSH_ZDOTDIR'
|
||||
else
|
||||
'builtin' 'unset' 'ZDOTDIR'
|
||||
fi
|
||||
|
||||
# Use try-always to have the right error code.
|
||||
{
|
||||
# Zsh treats unset ZDOTDIR as if it was HOME. We do the same.
|
||||
#
|
||||
# Source the user's .zshenv before sourcing ghostty-integration because the
|
||||
# former might set fpath and other things without which ghostty-integration
|
||||
# won't work.
|
||||
#
|
||||
# Use typeset in case we are in a function with warn_create_global in
|
||||
# effect. Unlikely but better safe than sorry.
|
||||
'builtin' 'typeset' _ghostty_file=${ZDOTDIR-$HOME}"/.zshenv"
|
||||
# Zsh ignores unreadable rc files. We do the same.
|
||||
# Zsh ignores rc files that are directories, and so does source.
|
||||
[[ ! -r "$_ghostty_file" ]] || 'builtin' 'source' '--' "$_ghostty_file"
|
||||
} always {
|
||||
if [[ -o 'interactive' ]]; then
|
||||
# ${(%):-%x} is the path to the current file.
|
||||
# On top of it we add :A:h to get the directory.
|
||||
'builtin' 'typeset' _ghostty_file="${${(%):-%x}:A:h}"/ghostty-integration
|
||||
if [[ -r "$_ghostty_file" ]]; then
|
||||
'builtin' 'autoload' '-Uz' '--' "$_ghostty_file"
|
||||
"${_ghostty_file:t}"
|
||||
'builtin' 'unfunction' '--' "${_ghostty_file:t}"
|
||||
fi
|
||||
fi
|
||||
'builtin' 'unset' '_ghostty_file'
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
# vim:ft=zsh
|
||||
#
|
||||
# Based on (started as) a copy of Kitty's zsh integration. Kitty is
|
||||
# distributed under GPLv3, so this file is also distributed under GPLv3.
|
||||
# The license header is reproduced below:
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#
|
||||
# Enables integration between zsh and ghostty.
|
||||
#
|
||||
# This is an autoloadable function. It's invoked automatically in shells
|
||||
# directly spawned by Ghostty but not in any other shells. For example, running
|
||||
# `exec zsh`, `sudo -E zsh`, `tmux`, or plain `zsh` will create a shell where
|
||||
# ghostty-integration won't automatically run. Zsh users who want integration with
|
||||
# Ghostty in all shells should add the following lines to their .zshrc:
|
||||
#
|
||||
# if [[ -n $GHOSTTY_RESOURCES_DIR ]]; then
|
||||
# source "$GHOSTTY_RESOURCES_DIR"/shell-integration/zsh/ghostty-integration
|
||||
# fi
|
||||
#
|
||||
# Implementation note: We can assume that alias expansion is disabled in this
|
||||
# file, so no need to quote defensively. We still have to defensively prefix all
|
||||
# builtins with `builtin` to avoid accidentally invoking user-defined functions.
|
||||
# We avoid `function` reserved word as an additional defensive measure.
|
||||
|
||||
# Note that updating options with `builtin emulate -L zsh` affects the global options
|
||||
# if it's called outside of a function. So nearly all code has to be in functions.
|
||||
_entrypoint() {
|
||||
builtin emulate -L zsh -o no_warn_create_global -o no_aliases
|
||||
|
||||
[[ -o interactive ]] || builtin return 0 # non-interactive shell
|
||||
(( ! $+_ghostty_state )) || builtin return 0 # already initialized
|
||||
|
||||
# We require zsh 5.1+ (released Sept 2015) for features like functions_source,
|
||||
# introspection arrays, and array pattern substitution.
|
||||
if ! { builtin autoload -- is-at-least 2>/dev/null && is-at-least 5.1; }; then
|
||||
builtin echo "Zsh ${ZSH_VERSION} is too old for ghostty shell integration (5.1+ required)" >&2
|
||||
builtin return 1
|
||||
fi
|
||||
|
||||
# 0: no OSC 133 [AC] marks have been written yet.
|
||||
# 1: the last written OSC 133 C has not been closed with D yet.
|
||||
# 2: none of the above.
|
||||
builtin typeset -gi _ghostty_state
|
||||
|
||||
# Attempt to create a writable file descriptor to the TTY so that we can print
|
||||
# to the TTY later even when STDOUT is redirected. This code is fairly subtle.
|
||||
#
|
||||
# - It's tempting to do `[[ -t 1 ]] && exec {_ghostty_state}>&1` but we cannot do this
|
||||
# because it'll create a file descriptor >= 10 without O_CLOEXEC. This file
|
||||
# descriptor will leak to child processes.
|
||||
# - If we do `exec {3}>&1`, the file descriptor won't leak to the child processes
|
||||
# but it'll still leak if the current process is replaced with another. In
|
||||
# addition, it'll break user code that relies on fd 3 being available.
|
||||
# - Zsh doesn't expose dup3, which would have allowed us to copy STDOUT with
|
||||
# O_CLOEXEC. The only way to create a file descriptor with O_CLOEXEC is via
|
||||
# sysopen.
|
||||
# - `zmodload zsh/system` and `sysopen -o cloexec -wu _ghostty_fd -- /dev/tty` can
|
||||
# fail with an error message to STDERR (the latter can happen even if /dev/tty
|
||||
# is writable), hence the redirection of STDERR. We do it for the whole block
|
||||
# for performance reasons (redirections are slow).
|
||||
# - We must open the file descriptor right here rather than in _ghostty_deferred_init
|
||||
# because there are broken zsh plugins out there that run `exec {fd}< <(cmd)`
|
||||
# and then close the file descriptor more than once while suppressing errors.
|
||||
# This could end up closing our file descriptor if we opened it in
|
||||
# _ghostty_deferred_init.
|
||||
typeset -gi _ghostty_fd
|
||||
{
|
||||
builtin zmodload zsh/system && (( $+builtins[sysopen] )) && {
|
||||
{ [[ -w $TTY ]] && builtin sysopen -o cloexec -wu _ghostty_fd -- $TTY } ||
|
||||
{ [[ -w /dev/tty ]] && builtin sysopen -o cloexec -wu _ghostty_fd -- /dev/tty }
|
||||
}
|
||||
} 2>/dev/null || (( _ghostty_fd = 1 ))
|
||||
|
||||
# Defer initialization so that other zsh init files can be configure
|
||||
# the integration.
|
||||
builtin typeset -ag precmd_functions
|
||||
precmd_functions+=(_ghostty_deferred_init)
|
||||
}
|
||||
|
||||
_ghostty_deferred_init() {
|
||||
builtin emulate -L zsh -o no_warn_create_global -o no_aliases
|
||||
|
||||
# Enable semantic markup with OSC 133.
|
||||
_ghostty_precmd() {
|
||||
builtin local -i cmd_status=$?
|
||||
builtin emulate -L zsh -o no_warn_create_global -o no_aliases
|
||||
|
||||
# Don't write OSC 133 D when our precmd handler is invoked from zle.
|
||||
# Some plugins do that to update prompt on cd.
|
||||
if ! builtin zle; then
|
||||
# This code works incorrectly in the presence of a precmd or chpwd
|
||||
# hook that prints. For example, sindresorhus/pure prints an empty
|
||||
# line on precmd and marlonrichert/zsh-snap prints $PWD on chpwd.
|
||||
# We'll end up writing our OSC 133 D mark too late.
|
||||
#
|
||||
# Another failure mode is when the output of a command doesn't end
|
||||
# with LF and prompst_sp is set (it is by default). In this case
|
||||
# we'll incorrectly state that '%' from prompt_sp is a part of the
|
||||
# command's output.
|
||||
if (( _ghostty_state == 1 )); then
|
||||
# The last written OSC 133 C has not been closed with D yet.
|
||||
# Close it and supply status.
|
||||
builtin print -nu $_ghostty_fd '\e]133;D;'$cmd_status'\a'
|
||||
(( _ghostty_state = 2 ))
|
||||
elif (( _ghostty_state == 2 )); then
|
||||
# There might be an unclosed OSC 133 C. Close that.
|
||||
builtin print -nu $_ghostty_fd '\e]133;D\a'
|
||||
fi
|
||||
fi
|
||||
|
||||
builtin local mark1=$'%{\e]133;A;cl=line\a%}'
|
||||
if [[ -o prompt_percent ]]; then
|
||||
builtin typeset -g precmd_functions
|
||||
if [[ ${precmd_functions[-1]} == _ghostty_precmd ]]; then
|
||||
# This is the best case for us: we can add our marks to PS1 and
|
||||
# PS2. This way our marks will be printed whenever zsh
|
||||
# redisplays prompt: on reset-prompt, on SIGWINCH, and on
|
||||
# SIGCHLD if notify is set. Themes that update prompt
|
||||
# asynchronously from a `zle -F` handler might still remove our
|
||||
# marks. Oh well.
|
||||
|
||||
# Restore PS1/PS2 to their pre-mark state if nothing else has
|
||||
# modified them since we last added marks. This avoids exposing
|
||||
# PS1 with our marks to other hooks (which can break themes like
|
||||
# Pure that use pattern matching to strip/rebuild the prompt).
|
||||
# If PS1 was modified (by a theme, async update, etc.), we
|
||||
# keep the modified version, prioritizing the theme's changes.
|
||||
builtin local ps1_changed=0
|
||||
if [[ -n ${_ghostty_saved_ps1+x} ]]; then
|
||||
if [[ $PS1 == $_ghostty_marked_ps1 ]]; then
|
||||
PS1=$_ghostty_saved_ps1
|
||||
PS2=$_ghostty_saved_ps2
|
||||
elif [[ $PS1 != $_ghostty_saved_ps1 ]]; then
|
||||
ps1_changed=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Save the clean PS1/PS2 before we add marks.
|
||||
_ghostty_saved_ps1=$PS1
|
||||
_ghostty_saved_ps2=$PS2
|
||||
|
||||
# Add our marks. Since we always start from a clean PS1
|
||||
# (either restored above or freshly set by a theme), we can
|
||||
# unconditionally add mark1 and markB.
|
||||
builtin local mark2=$'%{\e]133;A;k=s\a%}'
|
||||
builtin local markB=$'%{\e]133;B\a%}'
|
||||
PS1=${mark1}${PS1}${markB}
|
||||
|
||||
# Handle multiline prompts by marking newline-separated
|
||||
# continuation lines with k=s (mark2). We skip the newline
|
||||
# immediately after mark1 to avoid introducing a double
|
||||
# newline due to OSC 133;A's fresh-line behavior.
|
||||
#
|
||||
# We skip this when PS1 changed because injecting marks into
|
||||
# newlines can break pattern matching in themes that
|
||||
# strip/rebuild the prompt dynamically (e.g., Pure).
|
||||
if (( ! ps1_changed )) && [[ $PS1 == *$'\n'* ]]; then
|
||||
if [[ $PS1 == ${mark1}$'\n'* ]]; then
|
||||
builtin local rest=${PS1#${mark1}$'\n'}
|
||||
if [[ $rest == *$'\n'* ]]; then
|
||||
PS1=${mark1}$'\n'${rest//$'\n'/$'\n'${mark2}}
|
||||
fi
|
||||
else
|
||||
PS1=${PS1//$'\n'/$'\n'${mark2}}
|
||||
fi
|
||||
fi
|
||||
|
||||
# PS2 mark is needed when clearing the prompt on resize
|
||||
PS2=${mark2}${PS2}${markB}
|
||||
|
||||
# Save the marked PS1 so we can detect modifications
|
||||
# by other hooks in the next cycle.
|
||||
_ghostty_marked_ps1=$PS1
|
||||
(( _ghostty_state = 2 ))
|
||||
else
|
||||
# If our precmd hook is not the last, we cannot rely on prompt
|
||||
# changes to stick, so we don't even try. At least we can move
|
||||
# our hook to the end to have better luck next time. If there is
|
||||
# another piece of code that wants to take this privileged
|
||||
# position, this won't work well. We'll break them as much as
|
||||
# they are breaking us.
|
||||
precmd_functions=(${precmd_functions:#_ghostty_precmd} _ghostty_precmd)
|
||||
# Plugins that invoke precmd hooks from zle do that before zle
|
||||
# is trashed. This means that the cursor is in the middle of
|
||||
# BUFFER and we cannot print our mark there. Prompt might
|
||||
# already have a mark, so the following reset-prompt will write
|
||||
# it. If it doesn't, there is nothing we can do.
|
||||
if ! builtin zle; then
|
||||
builtin print -rnu $_ghostty_fd -- $mark1[3,-3]
|
||||
(( _ghostty_state = 2 ))
|
||||
fi
|
||||
fi
|
||||
elif ! builtin zle; then
|
||||
# Without prompt_percent we cannot patch prompt. Just print the
|
||||
# mark, except when we are invoked from zle. In the latter case we
|
||||
# cannot do anything.
|
||||
builtin print -rnu $_ghostty_fd -- $mark1[3,-3]
|
||||
(( _ghostty_state = 2 ))
|
||||
fi
|
||||
}
|
||||
|
||||
_ghostty_preexec() {
|
||||
builtin emulate -L zsh -o no_warn_create_global -o no_aliases
|
||||
|
||||
# Restore the original PS1/PS2 if nothing else has modified them
|
||||
# since our precmd added marks. This ensures other preexec hooks
|
||||
# see a clean PS1 without our marks. If PS1 was modified (e.g.,
|
||||
# by an async theme update), we leave it alone.
|
||||
if [[ -n ${_ghostty_saved_ps1+x} && $PS1 == $_ghostty_marked_ps1 ]]; then
|
||||
PS1=$_ghostty_saved_ps1
|
||||
PS2=$_ghostty_saved_ps2
|
||||
fi
|
||||
|
||||
# This will work incorrectly in the presence of a preexec hook that
|
||||
# prints. For example, if MichaelAquilina/zsh-you-should-use installs
|
||||
# its preexec hook before us, we'll incorrectly mark its output as
|
||||
# belonging to the command (as if the user typed it into zle) rather
|
||||
# than command output.
|
||||
builtin print -nu $_ghostty_fd '\e]133;C\a'
|
||||
(( _ghostty_state = 1 ))
|
||||
}
|
||||
|
||||
# Enable reporting current working dir to terminal. Ghostty supports
|
||||
# the kitty-shell-cwd format.
|
||||
_ghostty_report_pwd() { builtin print -nu $_ghostty_fd '\e]7;kitty-shell-cwd://'"$HOST""$PWD"'\a'; }
|
||||
chpwd_functions=(${chpwd_functions[@]} "_ghostty_report_pwd")
|
||||
# An executed program could change cwd and report the changed cwd, so also report cwd at each new prompt
|
||||
# as in this case chpwd_functions is insufficient. chpwd_functions is still needed for things like: cd x && something
|
||||
functions[_ghostty_precmd]+="
|
||||
_ghostty_report_pwd"
|
||||
_ghostty_report_pwd
|
||||
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then
|
||||
# Enable terminal title changes, formatted for user-friendly display.
|
||||
functions[_ghostty_precmd]+="
|
||||
builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(%):-%(4~|…/%3~|%~)}\"\$'\\a'"
|
||||
functions[_ghostty_preexec]+="
|
||||
builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${1//[[:cntrl:]]}\"\$'\\a'"
|
||||
fi
|
||||
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then
|
||||
# Enable cursor shape changes depending on the current keymap.
|
||||
# This implementation leaks blinking block cursor into external commands
|
||||
# executed from zle. For example, users of fzf-based widgets may find
|
||||
# themselves with a blinking block cursor within fzf.
|
||||
_ghostty_zle_line_init _ghostty_zle_line_finish _ghostty_zle_keymap_select() {
|
||||
builtin local steady=0
|
||||
[[ "$GHOSTTY_SHELL_FEATURES" == *"cursor:steady"* ]] && steady=1
|
||||
case ${KEYMAP-} in
|
||||
vicmd|visual) builtin print -nu "$_ghostty_fd" "\e[$(( 1 + steady )) q" ;; # block
|
||||
*) builtin print -nu "$_ghostty_fd" "\e[$(( 5 + steady )) q" ;; # bar
|
||||
esac
|
||||
}
|
||||
# Restore the default shape before executing an external command
|
||||
functions[_ghostty_preexec]+="
|
||||
builtin print -rnu $_ghostty_fd \$'\\e[0 q'"
|
||||
fi
|
||||
|
||||
# Emit semantic prompt markers at line-init if PS1 doesn't contain our
|
||||
# marks. This ensures the terminal sees prompt markers even if another
|
||||
# plugin (like zinit or oh-my-posh) regenerated PS1 after our precmd ran.
|
||||
# We use 133;P instead of 133;A to avoid fresh-line behavior which would
|
||||
# disrupt the display since the prompt has already been drawn. We also
|
||||
# emit 133;B to mark the input area, which is needed for click-to-move.
|
||||
(( $+functions[_ghostty_zle_line_init] )) || _ghostty_zle_line_init() { builtin true; }
|
||||
functions[_ghostty_zle_line_init]="
|
||||
if [[ \$PS1 != *$'%{\\e]133;A'* ]]; then
|
||||
builtin print -nu \$_ghostty_fd '\\e]133;P;k=i\\a\\e]133;B\\a'
|
||||
fi
|
||||
"${functions[_ghostty_zle_line_init]}
|
||||
|
||||
# Add Ghostty binary to PATH if the path feature is enabled
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"path"* ]] && [[ -n "$GHOSTTY_BIN_DIR" ]]; then
|
||||
if [[ ":$PATH:" != *":$GHOSTTY_BIN_DIR:"* ]]; then
|
||||
builtin export PATH="$PATH:$GHOSTTY_BIN_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Sudo
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* ]] && [[ -n "$TERMINFO" ]]; then
|
||||
# Wrap `sudo` command to ensure Ghostty terminfo is preserved
|
||||
function sudo() {
|
||||
builtin local sudo_has_sudoedit_flags="no"
|
||||
for arg in "$@"; do
|
||||
# Check if argument is '-e' or '--edit' (sudoedit flags)
|
||||
if [[ "$arg" == "-e" || $arg == "--edit" ]]; then
|
||||
sudo_has_sudoedit_flags="yes"
|
||||
builtin break
|
||||
fi
|
||||
# Check if argument is neither an option nor a key-value pair
|
||||
if [[ "$arg" != -* && "$arg" != *=* ]]; then
|
||||
builtin break
|
||||
fi
|
||||
done
|
||||
if [[ "$sudo_has_sudoedit_flags" == "yes" ]]; then
|
||||
builtin command sudo "$@";
|
||||
else
|
||||
builtin command sudo --preserve-env=TERMINFO "$@";
|
||||
fi
|
||||
}
|
||||
fi
|
||||
|
||||
# SSH Integration
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then
|
||||
function ssh() {
|
||||
emulate -L zsh
|
||||
setopt local_options no_glob_subst
|
||||
|
||||
local ssh_term ssh_opts
|
||||
ssh_term="xterm-256color"
|
||||
ssh_opts=()
|
||||
|
||||
# Configure environment variables for remote session
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]]; then
|
||||
ssh_opts+=(-o "SetEnv COLORTERM=truecolor")
|
||||
ssh_opts+=(-o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION")
|
||||
fi
|
||||
|
||||
# Install terminfo on remote host if needed
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-terminfo* ]]; then
|
||||
local ssh_user ssh_hostname
|
||||
|
||||
while IFS=' ' read -r ssh_key ssh_value; do
|
||||
case "$ssh_key" in
|
||||
user) ssh_user="$ssh_value" ;;
|
||||
hostname) ssh_hostname="$ssh_value" ;;
|
||||
esac
|
||||
[[ -n "$ssh_user" && -n "$ssh_hostname" ]] && break
|
||||
done < <(command ssh -G "$@" 2>/dev/null)
|
||||
|
||||
if [[ -n "$ssh_hostname" ]]; then
|
||||
local ssh_target="${ssh_user}@${ssh_hostname}"
|
||||
|
||||
# Check if terminfo is already cached
|
||||
if "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --host="$ssh_target" >/dev/null 2>&1; then
|
||||
ssh_term="xterm-ghostty"
|
||||
elif (( $+commands[infocmp] )); then
|
||||
local ssh_terminfo ssh_cpath_dir ssh_cpath
|
||||
|
||||
ssh_terminfo=$(infocmp -0 -x xterm-ghostty 2>/dev/null)
|
||||
|
||||
if [[ -n "$ssh_terminfo" ]]; then
|
||||
print "Setting up xterm-ghostty terminfo on $ssh_hostname..." >&2
|
||||
|
||||
ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$"
|
||||
ssh_cpath="$ssh_cpath_dir/socket"
|
||||
|
||||
if builtin print -r "$ssh_terminfo" | command ssh "${ssh_opts[@]}" -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" '
|
||||
infocmp xterm-ghostty >/dev/null 2>&1 && exit 0
|
||||
command -v tic >/dev/null 2>&1 || exit 1
|
||||
mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0
|
||||
exit 1
|
||||
' 2>/dev/null; then
|
||||
ssh_term="xterm-ghostty"
|
||||
ssh_opts+=(-o "ControlPath=$ssh_cpath")
|
||||
|
||||
# Cache successful installation
|
||||
"$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true
|
||||
else
|
||||
print "Warning: Failed to install terminfo." >&2
|
||||
fi
|
||||
else
|
||||
print "Warning: Could not generate terminfo data." >&2
|
||||
fi
|
||||
else
|
||||
print "Warning: ghostty command not available for cache management." >&2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Execute SSH with TERM environment variable
|
||||
TERM="$ssh_term" command ssh "${ssh_opts[@]}" "$@"
|
||||
}
|
||||
fi
|
||||
|
||||
# Some zsh users manually run `source ~/.zshrc` in order to apply rc file
|
||||
# changes to the current shell. This is a terrible practice that breaks many
|
||||
# things, including our shell integration. For example, Oh My Zsh and Prezto
|
||||
# (both very popular among zsh users) will remove zle-line-init and
|
||||
# zle-line-finish hooks if .zshrc is manually sourced. Prezto will also remove
|
||||
# zle-keymap-select.
|
||||
#
|
||||
# Another common (and much more robust) way to apply rc file changes to the
|
||||
# current shell is `exec zsh`. This will remove our integration from the shell
|
||||
# unless it's explicitly invoked from .zshrc. This is not an issue with
|
||||
# `exec zsh` but rather with our implementation of automatic shell integration.
|
||||
|
||||
# In the ideal world we would use add-zle-hook-widget to hook zle-line-init
|
||||
# and similar widget. This breaks user configs though, so we have do this
|
||||
# horrible thing instead.
|
||||
builtin local hook func widget orig_widget flag
|
||||
for hook in line-init line-finish keymap-select; do
|
||||
func=_ghostty_zle_${hook/-/_}
|
||||
(( $+functions[$func] )) || builtin continue
|
||||
widget=zle-$hook
|
||||
if [[ $widgets[$widget] == user:azhw:* &&
|
||||
$+functions[add-zle-hook-widget] -eq 1 ]]; then
|
||||
# If the widget is already hooked by add-zle-hook-widget at the top
|
||||
# level, add our hook at the end. We MUST do it this way. We cannot
|
||||
# just wrap the widget ourselves in this case because it would
|
||||
# trigger bugs in add-zle-hook-widget.
|
||||
add-zle-hook-widget $hook $func
|
||||
else
|
||||
if (( $+widgets[$widget] )); then
|
||||
# There is a widget but it's not from add-zle-hook-widget. We
|
||||
# can rename the original widget, install our own and invoke
|
||||
# the original when we are called.
|
||||
#
|
||||
# Note: The leading dot is to work around bugs in
|
||||
# zsh-syntax-highlighting.
|
||||
orig_widget=._ghostty_orig_$widget
|
||||
builtin zle -A $widget $orig_widget
|
||||
if [[ $widgets[$widget] == user:* ]]; then
|
||||
# No -w here to preserve $WIDGET within the original widget.
|
||||
flag=
|
||||
else
|
||||
flag=w
|
||||
fi
|
||||
functions[$func]+="
|
||||
builtin zle $orig_widget -N$flag -- \"\$@\""
|
||||
fi
|
||||
builtin zle -N $widget $func
|
||||
fi
|
||||
done
|
||||
|
||||
if (( $+functions[_ghostty_preexec] )); then
|
||||
builtin typeset -ag preexec_functions
|
||||
preexec_functions+=(_ghostty_preexec)
|
||||
fi
|
||||
|
||||
builtin typeset -ag precmd_functions
|
||||
if (( $+functions[_ghostty_precmd] )); then
|
||||
precmd_functions=(${precmd_functions:#_ghostty_deferred_init} _ghostty_precmd)
|
||||
_ghostty_precmd
|
||||
else
|
||||
precmd_functions=(${precmd_functions:#_ghostty_deferred_init})
|
||||
fi
|
||||
|
||||
# Unfunction _ghostty_deferred_init to save memory. Don't unfunction
|
||||
# ghostty-integration though because decent public functions aren't supposed to
|
||||
# to unfunction themselves when invoked. Unfunctioning is done by calling code.
|
||||
builtin unfunction _ghostty_deferred_init
|
||||
}
|
||||
|
||||
_entrypoint
|
||||
Reference in new issue
Block a user