fix: Windows core build, embedded init, byte-exact paths, version info

Closes the remaining core/lib/termio/GTK rows (final-Z2.md):
- ghostty-core compiles for Windows (Unix-only exec fields and file
  modes gated); checked with cargo check --target x86_64-pc-windows-gnu.
- Embedded init: ensure_locale, SIGPIPE ignored in ghostty_init for
  C/Swift hosts, Apple crash tags; the argv passed to ghostty_init feeds
  ghostty_config_load_cli_args; ghostty_config_load_file accepts
  non-UTF-8 paths.
- OSC 7 pwd, embedded surface cwd/command/env/initial input,
  ghostty_surface_text and GTK file drops keep raw bytes.
- XTVERSION, TERM_PROGRAM_VERSION, the About dialog and crash metadata
  use ghostty_build_info instead of hardcoded versions.

Test Plan:
- cargo test --workspace (3327 passed)
- cargo clippy --workspace --all-targets -- -D warnings
This commit is contained in:
ddidderr committed 2026-10-12 08:14:02 +02:00
1 parent 8519f1ba1b
commit ebefad5031
30 files changed
+863 -80

No files matched your search

Generated
+390 -1
View File
@@ -325,6 +325,9 @@ name = "bitflags"
version = "2.13.2" version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
dependencies = [
"serde_core",
]
[[package]] [[package]]
name = "block" name = "block"
@@ -395,6 +398,18 @@ dependencies = [
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]] [[package]]
name = "cairo-rs" name = "cairo-rs"
version = "0.22.9" version = "0.22.9"
@@ -547,6 +562,12 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "core_detect"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.17" version = "0.2.17"
@@ -556,6 +577,17 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crash-context"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3748e166981fa3ba9dc54ff950ad0ed5ca4aab4b73eae21d9f539659fe806e28"
dependencies = [
"cfg-if",
"libc",
"mach2",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.5.2" version = "1.5.2"
@@ -718,6 +750,15 @@ dependencies = [
"mint", "mint",
] ]
[[package]]
name = "debugid"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
dependencies = [
"uuid",
]
[[package]] [[package]]
name = "defmt" name = "defmt"
version = "1.1.1" version = "1.1.1"
@@ -835,6 +876,34 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
[[package]]
name = "embed-resource"
version = "3.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6168487b5e7eb2673e6efe1200b1848016e436a6ca0b8031bf04bd9d493a249f"
dependencies = [
"cc",
"memchr",
"rustc_version",
"toml",
"vswhom",
"winreg",
]
[[package]]
name = "encoding_rs"
version = "0.8.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679"
dependencies = [
"cfg-if",
"core_detect",
"multiversion_no_op",
"rustversion",
"scopeguard",
"simdutf8",
]
[[package]] [[package]]
name = "endi" name = "endi"
version = "1.1.1" version = "1.1.1"
@@ -901,6 +970,15 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "error-graph"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b920e777967421aa5f9bf34f842c0ab6ba19b3bdb4a082946093860f5858879"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "euclid" name = "euclid"
version = "0.22.14" version = "0.22.14"
@@ -930,6 +1008,12 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "failspot"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c942e64b20ecd39933d5ff938ca4fdb6ef0d298cc3855b231179a5ef0b24948d"
[[package]] [[package]]
name = "fancy-regex" name = "fancy-regex"
version = "0.11.0" version = "0.11.0"
@@ -1291,6 +1375,7 @@ dependencies = [
"gdk4-wayland", "gdk4-wayland",
"gdk4-x11", "gdk4-x11",
"ghostty-apprt", "ghostty-apprt",
"ghostty-build-info",
"ghostty-config", "ghostty-config",
"ghostty-datastruct", "ghostty-datastruct",
"ghostty-input", "ghostty-input",
@@ -1336,12 +1421,17 @@ dependencies = [
"ghostty-termio", "ghostty-termio",
] ]
[[package]]
name = "ghostty-build-info"
version = "0.1.0"
[[package]] [[package]]
name = "ghostty-cli" name = "ghostty-cli"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"flate2", "flate2",
"ghostty-apprt", "ghostty-apprt",
"ghostty-build-info",
"ghostty-config", "ghostty-config",
"ghostty-crash", "ghostty-crash",
"ghostty-font", "ghostty-font",
@@ -1378,6 +1468,7 @@ dependencies = [
"base64 0.23.1", "base64 0.23.1",
"crossbeam-channel", "crossbeam-channel",
"ghostty-apprt", "ghostty-apprt",
"ghostty-build-info",
"ghostty-config", "ghostty-config",
"ghostty-crash", "ghostty-crash",
"ghostty-datastruct", "ghostty-datastruct",
@@ -1405,6 +1496,8 @@ dependencies = [
"ghostty-os", "ghostty-os",
"libc", "libc",
"log", "log",
"minidump",
"minidump-writer",
"serde_json", "serde_json",
"tempfile", "tempfile",
"thiserror 2.0.21", "thiserror 2.0.21",
@@ -1481,15 +1574,18 @@ dependencies = [
"dispatch2", "dispatch2",
"ghostty-apprt", "ghostty-apprt",
"ghostty-bench", "ghostty-bench",
"ghostty-build-info",
"ghostty-cli", "ghostty-cli",
"ghostty-config", "ghostty-config",
"ghostty-core", "ghostty-core",
"ghostty-crash",
"ghostty-input", "ghostty-input",
"ghostty-inspector", "ghostty-inspector",
"ghostty-os", "ghostty-os",
"ghostty-renderer", "ghostty-renderer",
"ghostty-terminal", "ghostty-terminal",
"ghostty-vt", "ghostty-vt",
"libc",
"log", "log",
"objc2", "objc2",
"objc2-core-foundation", "objc2-core-foundation",
@@ -1566,6 +1662,7 @@ dependencies = [
name = "ghostty-rs" name = "ghostty-rs"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"embed-resource",
"ghostty-apprt", "ghostty-apprt",
"ghostty-apprt-gtk", "ghostty-apprt-gtk",
"ghostty-cli", "ghostty-cli",
@@ -1605,6 +1702,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"ghostty-apprt", "ghostty-apprt",
"ghostty-build-info",
"ghostty-config", "ghostty-config",
"ghostty-crash", "ghostty-crash",
"ghostty-datastruct", "ghostty-datastruct",
@@ -1787,6 +1885,17 @@ dependencies = [
"system-deps 9.0.0", "system-deps 9.0.0",
] ]
[[package]]
name = "goblin"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daa0a64d21a7eb230583b4c5f4e23b7e4e57974f96620f42a7e75e08ae66d745"
dependencies = [
"log",
"plain",
"scroll 0.12.0",
]
[[package]] [[package]]
name = "graphene-rs" name = "graphene-rs"
version = "0.22.8" version = "0.22.8"
@@ -2303,6 +2412,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "mach2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b"
[[package]] [[package]]
name = "malloc_buf" name = "malloc_buf"
version = "0.0.6" version = "0.0.6"
@@ -2318,6 +2433,15 @@ version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "memmap2"
version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "memmem" name = "memmem"
version = "0.1.1" version = "0.1.1"
@@ -2333,6 +2457,83 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "minidump"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a03516f742a87e757019a3019707b38900663397d63cdb0442721c5dbd29ed6b"
dependencies = [
"debugid",
"encoding_rs",
"memmap2",
"minidump-common 0.27.0",
"num-traits",
"procfs-core",
"prost",
"range-map",
"scroll 0.13.0",
"thiserror 2.0.21",
"time",
"tracing",
"uuid",
]
[[package]]
name = "minidump-common"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e16d10087ae9e375bad7a40e8ef5504bc08e808ccc6019067ff9de42a84570f"
dependencies = [
"bitflags 2.13.2",
"debugid",
"num-derive 0.4.2",
"num-traits",
"range-map",
"scroll 0.12.0",
"smart-default",
]
[[package]]
name = "minidump-common"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e6411f30cf59f8a4815820a9b5d0c38962a719f5c43f80f5afbb71e791af16"
dependencies = [
"bitflags 2.13.2",
"debugid",
"num-derive 0.5.1",
"num-traits",
"range-map",
"scroll 0.13.0",
"smart-default",
]
[[package]]
name = "minidump-writer"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f742d7445a9d4c63826d302dadbbcb607745560108e88b21c4014a9724d09dbe"
dependencies = [
"bitflags 2.13.2",
"byteorder",
"cfg-if",
"crash-context",
"error-graph",
"failspot",
"goblin",
"libc",
"log",
"mach2",
"memmap2",
"minidump-common 0.26.1",
"process-backend",
"procfs-core",
"scroll 0.12.0",
"serde",
"serde_json",
"thiserror 2.0.21",
]
[[package]] [[package]]
name = "minimal-lexical" name = "minimal-lexical"
version = "0.2.1" version = "0.2.1"
@@ -2377,6 +2578,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "multiversion_no_op"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d"
[[package]] [[package]]
name = "naga" name = "naga"
version = "30.0.1" version = "30.0.1"
@@ -2467,6 +2674,17 @@ dependencies = [
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "num-derive"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e98dc3b890f6c23a0f9d3d491a2823d0dea0fa656302a13dd225fa924112a8"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.8",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -2966,6 +3184,12 @@ version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
[[package]]
name = "plain"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]] [[package]]
name = "png" name = "png"
version = "0.18.1" version = "0.18.1"
@@ -3051,6 +3275,63 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "process-backend"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07c02e259a3522eccc4957ee6e922db4945648fb7730a41192d919518d06a530"
dependencies = [
"libc",
"log",
"process-reader",
"serde",
"thiserror 2.0.21",
]
[[package]]
name = "process-reader"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3024e1803f4b8c40cca8c1955243410354cb4d67f053af9210147416a7265535"
dependencies = [
"libc",
"serde",
]
[[package]]
name = "procfs-core"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405"
dependencies = [
"bitflags 2.13.2",
"hex",
"serde",
]
[[package]]
name = "prost"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-derive"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
dependencies = [
"anyhow",
"itertools 0.14.0",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]] [[package]]
name = "psm" name = "psm"
version = "0.1.24" version = "0.1.24"
@@ -3105,6 +3386,15 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
[[package]]
name = "range-map"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12a5a2d6c7039059af621472a4389be1215a816df61aa4d531cfe85264aee95f"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "ratatui" name = "ratatui"
version = "0.30.2" version = "0.30.2"
@@ -3302,6 +3592,46 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "scroll"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6"
dependencies = [
"scroll_derive 0.12.1",
]
[[package]]
name = "scroll"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add"
dependencies = [
"scroll_derive 0.13.2",
]
[[package]]
name = "scroll_derive"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "scroll_derive"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1a36a382ed65dbcc0ab47fd5e9a94112417ccd34560a392ef3b7b0f0ec39148"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.8",
]
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.28" version = "1.0.28"
@@ -3443,6 +3773,12 @@ version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "1.0.4" version = "1.0.4"
@@ -3470,6 +3806,17 @@ version = "1.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
[[package]]
name = "smart-default"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]] [[package]]
name = "stacker" name = "stacker"
version = "0.1.25" version = "0.1.25"
@@ -3681,7 +4028,7 @@ dependencies = [
"log", "log",
"memmem", "memmem",
"nix 0.29.0", "nix 0.29.0",
"num-derive", "num-derive 0.4.2",
"num-traits", "num-traits",
"ordered-float", "ordered-float",
"pest", "pest",
@@ -3757,6 +4104,7 @@ dependencies = [
"powerfmt", "powerfmt",
"serde_core", "serde_core",
"time-core", "time-core",
"time-macros",
] ]
[[package]] [[package]]
@@ -3765,6 +4113,16 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
[[package]]
name = "time-macros"
version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tiny-skia" name = "tiny-skia"
version = "0.12.0" version = "0.12.0"
@@ -3848,6 +4206,7 @@ version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [ dependencies = [
"log",
"pin-project-lite", "pin-project-lite",
"tracing-attributes", "tracing-attributes",
"tracing-core", "tracing-core",
@@ -3968,6 +4327,26 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vswhom"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
dependencies = [
"libc",
"vswhom-sys",
]
[[package]]
name = "vswhom-sys"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150"
dependencies = [
"cc",
"libc",
]
[[package]] [[package]]
name = "vtparse" name = "vtparse"
version = "0.6.2" version = "0.6.2"
@@ -4343,6 +4722,16 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "winreg"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
dependencies = [
"cfg-if",
"windows-sys",
]
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
version = "0.57.1" version = "0.57.1"
+4
View File
@@ -62,6 +62,7 @@ ghostty-core = { path = "crates/ghostty-core" }
ghostty-apprt-gtk = { path = "crates/ghostty-apprt-gtk" } ghostty-apprt-gtk = { path = "crates/ghostty-apprt-gtk" }
ghostty-apprt = { path = "crates/ghostty-apprt" } ghostty-apprt = { path = "crates/ghostty-apprt" }
ghostty-cli = { path = "crates/ghostty-cli" } ghostty-cli = { path = "crates/ghostty-cli" }
ghostty-build-info = { path = "crates/ghostty-build-info" }
ghostty-crash = { path = "crates/ghostty-crash" } ghostty-crash = { path = "crates/ghostty-crash" }
ghostty-bench = { path = "crates/ghostty-bench" } ghostty-bench = { path = "crates/ghostty-bench" }
ghostty-inspector = { path = "crates/ghostty-inspector" } ghostty-inspector = { path = "crates/ghostty-inspector" }
@@ -119,3 +120,6 @@ overflow-checks = false
lto = true lto = true
incremental = false incremental = false
codegen-units = 1 codegen-units = 1
[build-dependencies]
embed-resource = "3.0.12"
+1
View File
@@ -23,6 +23,7 @@ ghostty-config.workspace = true
ghostty-input.workspace = true ghostty-input.workspace = true
ghostty-datastruct.workspace = true ghostty-datastruct.workspace = true
ghostty-inspector.workspace = true ghostty-inspector.workspace = true
ghostty-build-info.workspace = true
ghostty-os.workspace = true ghostty-os.workspace = true
ghostty-vt.workspace = true ghostty-vt.workspace = true
log = "0.4" log = "0.4"
+11
View File
@@ -69,6 +69,17 @@ fn main() {
println!("cargo:rerun-if-changed=resources"); println!("cargo:rerun-if-changed=resources");
println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=build.rs");
// Source tarballs ship the prebuilt GResource bundle so packagers need
// neither blueprint-compiler nor glib-compile-resources (Zig:
// `GhosttyDist.Resource` for `ghostty_resources.{c,h}`). As in Zig, it is
// never used in a git checkout.
println!("cargo:rerun-if-changed=dist/ghostty.gresource");
let prebuilt = manifest.join("dist/ghostty.gresource");
if prebuilt.is_file() && !manifest.join("../../.git").exists() {
std::fs::copy(&prebuilt, out.join("ghostty.gresource")).unwrap();
return;
}
let mut xml = String::new(); let mut xml = String::new();
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<gresources>\n"); xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<gresources>\n");
+38 -9
View File
@@ -1512,6 +1512,12 @@ impl Surface {
clipboard::paste(self, text); clipboard::paste(self, text);
} }
/// [`Surface::paste`] for raw bytes (e.g. dropped file names that are
/// not valid UTF-8 must reach the pty unchanged).
pub fn paste_bytes(&self, bytes: &[u8]) {
clipboard::paste_bytes(self, bytes);
}
/// Focus this surface. This properly focuses the input part of /// Focus this surface. This properly focuses the input part of
/// our surface. (Shadows `WidgetExt::grab_focus` on purpose, like Zig.) /// our surface. (Shadows `WidgetExt::grab_focus` on purpose, like Zig.)
pub fn grab_focus(&self) { pub fn grab_focus(&self) {
@@ -2622,6 +2628,26 @@ mod clipboard {
} }
} }
/// Paste explicit bytes. Valid UTF-8 takes the normal text path; other
/// bytes use the byte-preserving paste (the confirmation dialog, which
/// is text based, carries them with the reversible byte escape).
pub(super) fn paste_bytes(surface: &Surface, bytes: &[u8]) {
if let Ok(text) = std::str::from_utf8(bytes) {
paste(surface, text);
return;
}
let Some(core) = surface.core() else { return };
match core.complete_paste_bytes(bytes, false) {
Ok(()) => {}
Err(ClipboardError::UnsafePaste | ClipboardError::UnauthorizedPaste) => {
let text = ghostty_os::osbytes::bytes_to_config_string(bytes);
show_clipboard_confirmation(surface, ClipboardRequest::Paste, &text);
}
Err(err) => log::warn!("failed to complete clipboard request err={err}"),
}
}
/// Get the specific type of clipboard for a widget. /// Get the specific type of clipboard for a widget.
fn get(widget: &gtk::Widget, clipboard: Clipboard) -> gdk::Clipboard { fn get(widget: &gtk::Widget, clipboard: Clipboard) -> gdk::Clipboard {
match clipboard { match clipboard {
@@ -2661,7 +2687,13 @@ mod clipboard {
// Get our text // Get our text
let text = buf.text(&buf.start_iter(), &buf.end_iter(), false); let text = buf.text(&buf.start_iter(), &buf.end_iter(), false);
if let Err(err) = core.complete_clipboard_request(req, &text, true) { let result = if matches!(req, ClipboardRequest::Paste) {
// Undo the byte escape used for non-UTF-8 pastes.
core.complete_paste_bytes(&ghostty_os::osbytes::config_str_to_bytes(&text), true)
} else {
core.complete_clipboard_request(req, &text, true)
};
if let Err(err) = result {
log::warn!("failed to complete clipboard request: {err}"); log::warn!("failed to complete clipboard request: {err}");
} }
}); });
@@ -3375,17 +3407,14 @@ mod imp {
let mut w = ghostty_os::ShellEscapeWriter::new(&mut escaped); let mut w = ghostty_os::ShellEscapeWriter::new(&mut escaped);
for file in list.files() { for file in list.files() {
let Some(path) = file.path() else { continue }; let Some(path) = file.path() else { continue };
let path = path.to_string_lossy().into_owned(); // Raw path bytes: non-UTF-8 names must survive.
if let Err(err) = w let path = ghostty_os::osbytes::os_bytes(path.as_os_str());
.write_all(path.as_bytes()) if let Err(err) = w.write_all(&path).and_then(|()| w.write_all(b"\n")) {
.and_then(|()| w.write_all(b"\n"))
{
log::error!("unable to write path to buffer: {err}"); log::error!("unable to write path to buffer: {err}");
} }
} }
} }
let string = String::from_utf8_lossy(&escaped).into_owned(); this.paste_bytes(&escaped);
this.paste(&string);
return true; return true;
} }
@@ -3397,7 +3426,7 @@ mod imp {
{ {
let mut w = ghostty_os::ShellEscapeWriter::new(&mut escaped); let mut w = ghostty_os::ShellEscapeWriter::new(&mut escaped);
if let Err(err) = w if let Err(err) = w
.write_all(path.to_string_lossy().as_bytes()) .write_all(&ghostty_os::osbytes::os_bytes(path.as_os_str()))
.and_then(|()| w.write_all(b"\n")) .and_then(|()| w.write_all(b"\n"))
{ {
log::error!("unable to write path to buffer: {err}"); log::error!("unable to write path to buffer: {err}");
@@ -113,11 +113,11 @@ mod imp {
return; return;
}; };
if data.exit_code == 0 { if data.exit_code == 0 {
banner.set_title("Command succeeded"); banner.set_title(&ghostty_os::i18n::tr("Command succeeded"));
this.add_css_class("normal"); this.add_css_class("normal");
this.remove_css_class("abnormal"); this.remove_css_class("abnormal");
} else { } else {
banner.set_title("Command failed"); banner.set_title(&ghostty_os::i18n::tr("Command failed"));
this.remove_css_class("normal"); this.remove_css_class("normal");
this.add_css_class("abnormal"); this.add_css_class("abnormal");
} }
+2 -2
View File
@@ -1662,7 +1662,7 @@ impl Window {
.application_name(name) .application_name(name)
.developer_name(tr("Ghostty Developers")) .developer_name(tr("Ghostty Developers"))
.application_icon(icon) .application_icon(icon)
.version(env!("CARGO_PKG_VERSION")) .version(ghostty_build_info::VERSION_STRING)
.issue_url("https://github.com/ghostty-org/ghostty/issues") .issue_url("https://github.com/ghostty-org/ghostty/issues")
.website(website) .website(website)
.build(); .build();
@@ -1675,7 +1675,7 @@ impl Window {
("program-name", &name), ("program-name", &name),
("logo-icon-name", &icon), ("logo-icon-name", &icon),
("title", &tr("About Ghostty")), ("title", &tr("About Ghostty")),
("version", &env!("CARGO_PKG_VERSION")), ("version", &ghostty_build_info::VERSION_STRING),
("website", &website), ("website", &website),
], ],
); );
+5
View File
@@ -649,6 +649,11 @@ impl std::fmt::Display for SetTitle {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pwd { pub struct Pwd {
/// The working directory as reported by OSC 7. Zig keeps raw bytes; here
/// non-UTF-8 bytes are carried with the reversible escape of
/// `ghostty_os::osbytes` (valid UTF-8 is unchanged). Decode with
/// `osbytes::config_str_to_bytes` / `config_str_to_os` before using it
/// as a path or handing it to C.
pub pwd: String, pub pwd: String,
} }
+9
View File
@@ -201,6 +201,15 @@ pub trait CoreSurface {
data: &str, data: &str,
confirmed: bool, confirmed: bool,
) -> Result<(), ClipboardError>; ) -> Result<(), ClipboardError>;
/// [`CoreSurface::complete_clipboard_request`] for a paste of raw bytes
/// (Zig data is `[]const u8`). The default converts lossily.
fn complete_paste_bytes(&self, data: &[u8], confirmed: bool) -> Result<(), ClipboardError> {
self.complete_clipboard_request(
ClipboardRequest::Paste,
&String::from_utf8_lossy(data),
confirmed,
)
}
/// After the user confirms (`surface.config.clipboard_read = .allow`). /// After the user confirms (`surface.config.clipboard_read = .allow`).
fn set_clipboard_read_allowed(&self, allow: bool); fn set_clipboard_read_allowed(&self, allow: bool);
/// See [`CoreSurface::set_clipboard_read_allowed`] (`clipboard_write`). /// See [`CoreSurface::set_clipboard_read_allowed`] (`clipboard_write`).
+1
View File
@@ -14,6 +14,7 @@ ghostty-datastruct.workspace = true
ghostty-font.workspace = true ghostty-font.workspace = true
ghostty-inspector.workspace = true ghostty-inspector.workspace = true
ghostty-input.workspace = true ghostty-input.workspace = true
ghostty-build-info.workspace = true
ghostty-os.workspace = true ghostty-os.workspace = true
ghostty-renderer.workspace = true ghostty-renderer.workspace = true
ghostty-terminal.workspace = true ghostty-terminal.workspace = true
+48 -5
View File
@@ -116,7 +116,7 @@ pub struct GlobalState {
static STATE: OnceLock<GlobalState> = OnceLock::new(); static STATE: OnceLock<GlobalState> = OnceLock::new();
/// Options for [`init`]. /// Options for [`init`].
#[derive(Clone, Copy, Default)] #[derive(Clone, Default)]
pub struct InitOptions { pub struct InitOptions {
/// A CLI action is being executed: disable stderr logging by default /// A CLI action is being executed: disable stderr logging by default
/// since the action writes to stderr/stdout itself. /// since the action writes to stderr/stdout itself.
@@ -126,6 +126,32 @@ pub struct InitOptions {
/// `apprt.runtime.resourcesDir`, e.g. GTK's Flatpak host path lookup). /// `apprt.runtime.resourcesDir`, e.g. GTK's Flatpak host path lookup).
/// `None` uses the generic `ghostty_os::resourcesdir::resources_dir`. /// `None` uses the generic `ghostty_os::resourcesdir::resources_dir`.
pub resources_dir: Option<fn() -> std::io::Result<ResourcesDir>>, pub resources_dir: Option<fn() -> std::io::Result<ResourcesDir>>,
/// Crash reporting metadata (version, build mode, runtime, font backend
/// and renderer tags; Zig: `build_config`). `None` uses
/// [`default_crash_options`].
pub crash: Option<ghostty_crash::Options>,
}
/// Crash metadata for this build when the caller supplies none: the
/// version from the shared build info and the compile-time platform
/// defaults (Metal + CoreText on Apple targets, fontconfig + OpenGL
/// elsewhere).
#[must_use]
pub fn default_crash_options() -> ghostty_crash::Options {
let mut o = ghostty_crash::Options {
release: ghostty_build_info::VERSION_STRING.to_owned(),
build_mode: ghostty_build_info::BUILD_MODE
.trim_start_matches('.')
.to_owned(),
..ghostty_crash::Options::default()
};
if cfg!(target_vendor = "apple") {
"none".clone_into(&mut o.app_runtime);
"coretext".clone_into(&mut o.font_backend);
"metal".clone_into(&mut o.renderer);
}
o
} }
/// The global state. If [`init`] was never called, a default state (no /// The global state. If [`init`] was never called, a default state (no
@@ -182,13 +208,17 @@ fn init_with(opts: InitOptions, default_logging: Logging) -> &'static GlobalStat
let rlimits = ResourceLimits::init(); let rlimits = ResourceLimits::init();
// Initialize our crash reporting. // Initialize our crash reporting.
if let Err(err) = ghostty_crash::init(&ghostty_crash::Options { let crash_opts = opts.crash.clone().unwrap_or_else(default_crash_options);
release: ghostty_termio::stream_handler::VERSION_STRING.to_owned(), if let Err(err) = ghostty_crash::init(&crash_opts) {
..ghostty_crash::Options::default()
}) {
log::warn!("sentry init failed, no crash capture available err={err}"); log::warn!("sentry init failed, no crash capture available err={err}");
} }
// We need to make sure the process locale is set properly. Locale
// affects a lot of behaviors in a shell.
if let Some(locale) = ghostty_os::ensure_locale() {
log::debug!("locale set locale={locale}");
}
// Find our resources directory once for the app so every launch // Find our resources directory once for the app so every launch
// hereafter can use this cached value. // hereafter can use this cached value.
let discover = opts let discover = opts
@@ -352,6 +382,19 @@ mod tests {
assert_eq!(log_scope("other::x"), "other"); assert_eq!(log_scope("other::x"), "other");
} }
#[test]
fn crash_defaults_use_shared_version() {
let o = default_crash_options();
assert_eq!(o.release, ghostty_build_info::VERSION_STRING);
if cfg!(target_vendor = "apple") {
assert_eq!(o.renderer, "metal");
assert_eq!(o.font_backend, "coretext");
assert_eq!(o.app_runtime, "none");
} else {
assert_eq!(o.renderer, "opengl");
}
}
#[test] #[test]
fn logging_parse() { fn logging_parse() {
assert!(Logging::parse("stderr").stderr); assert!(Logging::parse("stderr").stderr);
+13 -10
View File
@@ -424,6 +424,7 @@ pub fn write_screen_temp_file(
loc: WriteScreenLoc, loc: WriteScreenLoc,
emit: WriteScreenFormat, emit: WriteScreenFormat,
) -> Result<String, CoreError> { ) -> Result<String, CoreError> {
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt as _; use std::os::unix::fs::OpenOptionsExt as _;
// Create a temporary directory to store our scrollback. // Create a temporary directory to store our scrollback.
@@ -432,12 +433,11 @@ pub fn write_screen_temp_file(
let file_path = tmp_dir.path().join(&filename); let file_path = tmp_dir.path().join(&filename);
let result = (|| -> Result<String, CoreError> { let result = (|| -> Result<String, CoreError> {
let mut file = std::fs::OpenOptions::new() let mut opts = std::fs::OpenOptions::new();
.write(true) opts.write(true).create(true).truncate(true);
.create(true) #[cfg(unix)]
.truncate(true) opts.mode(0o600);
.mode(0o600) let mut file = opts.open(&file_path)?;
.open(&file_path)?;
file.write_all(contents.as_bytes())?; file.write_all(contents.as_bytes())?;
file.flush()?; file.flush()?;
Ok(std::fs::canonicalize(&file_path)? Ok(std::fs::canonicalize(&file_path)?
@@ -1272,10 +1272,13 @@ mod tests {
assert!(path.ends_with("screen.txt")); assert!(path.ends_with("screen.txt"));
let read = std::fs::read_to_string(&path).unwrap(); let read = std::fs::read_to_string(&path).unwrap();
assert_eq!(read, s); assert_eq!(read, s);
let mode = std::os::unix::fs::PermissionsExt::mode( #[cfg(unix)]
&std::fs::metadata(&path).unwrap().permissions(), {
); let mode = std::os::unix::fs::PermissionsExt::mode(
assert_eq!(mode & 0o777, 0o600); &std::fs::metadata(&path).unwrap().permissions(),
);
assert_eq!(mode & 0o777, 0o600);
}
let _ = std::fs::remove_dir_all(std::path::Path::new(&path).parent().unwrap()); let _ = std::fs::remove_dir_all(std::path::Path::new(&path).parent().unwrap());
} }
+13 -1
View File
@@ -215,6 +215,12 @@ impl Surface {
} }
} }
/// [`Surface::complete_clipboard_request`] for a paste of raw bytes
/// (Zig passes `[]const u8`; e.g. dropped non-UTF-8 file names).
pub fn complete_paste_bytes(&self, data: &[u8], confirmed: bool) -> Result<(), ClipboardError> {
self.complete_clipboard_paste(data, confirmed)
}
/// Paste text (also the target of `text_callback`). /// Paste text (also the target of `text_callback`).
pub(crate) fn complete_clipboard_paste( pub(crate) fn complete_clipboard_paste(
&self, &self,
@@ -390,7 +396,13 @@ impl Surface {
/// `textCallback`: text input that is pasted verbatim. /// `textCallback`: text input that is pasted verbatim.
pub fn text_callback(&self, text: &str) -> Result<(), ClipboardError> { pub fn text_callback(&self, text: &str) -> Result<(), ClipboardError> {
self.text_callback_bytes(text.as_bytes())
}
/// [`Surface::text_callback`] for raw bytes (Zig takes `[]const u8`;
/// the embedded `ghostty_surface_text` forwards the host bytes as is).
pub fn text_callback_bytes(&self, text: &[u8]) -> Result<(), ClipboardError> {
let _crash = self.crash_guard(); let _crash = self.crash_guard();
self.complete_clipboard_paste(text.as_bytes(), true) self.complete_clipboard_paste(text, true)
} }
} }
+24 -6
View File
@@ -471,10 +471,15 @@ impl Surface {
.and_then(|wd| wd.value()) .and_then(|wd| wd.value())
.map(str::to_owned); .map(str::to_owned);
ecfg.resources_dir = global::state().resources_dir.host().map(str::to_owned); ecfg.resources_dir = global::state().resources_dir.host().map(str::to_owned);
ecfg.rt_pre_exec_info = ghostty_termio::command::RtPreExecInfo::init(&config); // The Windows exec backend has no pre-exec/post-fork hooks,
ecfg.rt_post_fork_info = ghostty_termio::command::RtPostForkInfo::init(&config); // cgroup scopes or rlimits (Zig: Linux/POSIX only).
ecfg.create_scope = rt_surface.create_scope_fn(); #[cfg(unix)]
ecfg.restore_rlimit = global::state().rlimits.nofile; {
ecfg.rt_pre_exec_info = ghostty_termio::command::RtPreExecInfo::init(&config);
ecfg.rt_post_fork_info = ghostty_termio::command::RtPostForkInfo::init(&config);
ecfg.create_scope = rt_surface.create_scope_fn();
ecfg.restore_rlimit = global::state().rlimits.nofile;
}
let exec = Exec::new(ecfg)?; let exec = Exec::new(ecfg)?;
let args = exec.args().to_vec(); let args = exec.args().to_vec();
@@ -935,7 +940,9 @@ impl Surface {
} }
Message::PwdChange(w) => { Message::PwdChange(w) => {
let pwd = String::from_utf8_lossy(w.slice()).into_owned(); // Keep the OSC 7 bytes (see `Pwd`): non-UTF-8 bytes are
// carried with the reversible byte escape.
let pwd = ghostty_os::osbytes::bytes_to_config_string(w.slice());
self.perform(RtAction::Pwd(Pwd { pwd })); self.perform(RtAction::Pwd(Pwd { pwd }));
} }
@@ -1364,7 +1371,14 @@ impl Surface {
pub fn pwd(&self) -> Option<String> { pub fn pwd(&self) -> Option<String> {
let t = self.lock_terminal(); let t = self.lock_terminal();
let pwd = t.get_pwd()?; let pwd = t.get_pwd()?;
Some(String::from_utf8_lossy(pwd).into_owned()) Some(ghostty_os::osbytes::bytes_to_config_string(pwd))
}
/// The raw bytes of [`Surface::pwd`] (Zig returns the OSC 7 bytes as is).
#[must_use]
pub fn pwd_bytes(&self) -> Option<Vec<u8>> {
let t = self.lock_terminal();
t.get_pwd().map(<[u8]>::to_vec)
} }
/// Returns the x/y coordinate of where the IME (Input Method Editor) /// Returns the x/y coordinate of where the IME (Input Method Editor)
@@ -2055,6 +2069,10 @@ impl CoreSurface for Surface {
Surface::complete_clipboard_request(self, req, data, confirmed) Surface::complete_clipboard_request(self, req, data, confirmed)
} }
fn complete_paste_bytes(&self, data: &[u8], confirmed: bool) -> Result<(), ClipboardError> {
Surface::complete_paste_bytes(self, data, confirmed)
}
fn set_clipboard_read_allowed(&self, allow: bool) { fn set_clipboard_read_allowed(&self, allow: bool) {
use ghostty_config::types_b::ClipboardAccess; use ghostty_config::types_b::ClipboardAccess;
self.set_config_clipboard( self.set_config_clipboard(
+1 -1
View File
@@ -1664,7 +1664,7 @@ impl Surface {
sel: link.selection, sel: link.selection,
trim: false, trim: false,
}); });
let pwd = t.get_pwd().map(|p| String::from_utf8_lossy(p).into_owned()); let pwd = t.get_pwd().map(ghostty_os::osbytes::bytes_to_config_string);
resolve_path_for_opening(pwd.as_deref(), &str).unwrap_or(str) resolve_path_for_opening(pwd.as_deref(), &str).unwrap_or(str)
} }
+18
View File
@@ -114,6 +114,24 @@ fn pwd_osc7() {
assert_eq!(h.surface.pwd().as_deref(), Some("/tmp/a b")); assert_eq!(h.surface.pwd().as_deref(), Some("/tmp/a b"));
} }
#[test]
fn pwd_osc7_keeps_non_utf8_bytes() {
let h = harness!(Harness::recorder(
r"\033]7;file://localhost/tmp/a%%FFb\033\\",
&[]
));
let pwd = h.wait_action("pwd", |a| match a {
Action::Pwd(p) if !p.pwd.is_empty() => Some(p.pwd.clone()),
_ => None,
});
assert_eq!(
ghostty_os::osbytes::config_str_to_bytes(&pwd),
b"/tmp/a\xffb"
);
assert_eq!(h.surface.pwd_bytes().as_deref(), Some(&b"/tmp/a\xffb"[..]));
assert_eq!(h.surface.pwd().as_deref(), Some(pwd.as_str()));
}
#[test] #[test]
fn bell_rings_once_per_burst() { fn bell_rings_once_per_burst() {
let h = harness!(Harness::recorder(r"\007", &[])); let h = harness!(Harness::recorder(r"\007", &[]));
+5
View File
@@ -23,6 +23,7 @@ ghostty-bench.workspace = true
ghostty-cli.workspace = true ghostty-cli.workspace = true
ghostty-config.workspace = true ghostty-config.workspace = true
ghostty-core.workspace = true ghostty-core.workspace = true
ghostty-crash.workspace = true
ghostty-input.workspace = true ghostty-input.workspace = true
ghostty-inspector.workspace = true ghostty-inspector.workspace = true
ghostty-os.workspace = true ghostty-os.workspace = true
@@ -31,6 +32,9 @@ ghostty-terminal.workspace = true
ghostty-vt.workspace = true ghostty-vt.workspace = true
log = "0.4.34" log = "0.4.34"
[target.'cfg(unix)'.dependencies]
libc = "0.2.190"
[target.'cfg(target_vendor = "apple")'.dependencies] [target.'cfg(target_vendor = "apple")'.dependencies]
block2 = "0.6.2" block2 = "0.6.2"
dispatch2 = "0.3.1" dispatch2 = "0.3.1"
@@ -40,6 +44,7 @@ objc2-foundation = "0.3.2"
objc2-metal = { version = "0.3.2", features = ["block2"] } objc2-metal = { version = "0.3.2", features = ["block2"] }
[dev-dependencies] [dev-dependencies]
ghostty-build-info.workspace = true
tempfile = "3" tempfile = "3"
# The workspace forbids `unsafe`; this crate is the C ABI boundary, so it # The workspace forbids `unsafe`; this crate is the C ABI boundary, so it
+11 -2
View File
@@ -36,7 +36,14 @@ pub struct Keep {
impl Keep { impl Keep {
/// A NUL terminated copy of `s` (interior NULs are dropped). /// A NUL terminated copy of `s` (interior NULs are dropped).
fn cstr(&mut self, s: &str) -> *const c_char { fn cstr(&mut self, s: &str) -> *const c_char {
let c = CString::new(s.replace('\0', "")).unwrap_or_default(); self.cstr_bytes(s.as_bytes())
}
/// [`Keep::cstr`] for raw bytes.
fn cstr_bytes(&mut self, s: &[u8]) -> *const c_char {
let mut s = s.to_vec();
s.retain(|&b| b != 0);
let c = CString::new(s).unwrap_or_default();
let ptr = c.as_ptr(); let ptr = c.as_ptr();
// The heap buffer of a CString doesn't move when the Vec grows. // The heap buffer of a CString doesn't move when the Vec grows.
self.strings.push(c); self.strings.push(c);
@@ -123,7 +130,9 @@ pub fn action_to_c(action: &Action, keep: &mut Keep) -> ghostty_action_s {
Action::SetTitle(t) => v.set_title.title = keep.cstr(&t.title), Action::SetTitle(t) => v.set_title.title = keep.cstr(&t.title),
Action::SetTabTitle(t) => v.set_tab_title.title = keep.cstr(&t.title), Action::SetTabTitle(t) => v.set_tab_title.title = keep.cstr(&t.title),
Action::PromptTitle(p) => v.prompt_title = *p as c_int, Action::PromptTitle(p) => v.prompt_title = *p as c_int,
Action::Pwd(p) => v.pwd.pwd = keep.cstr(&p.pwd), Action::Pwd(p) => {
v.pwd.pwd = keep.cstr_bytes(&ghostty_os::osbytes::config_str_to_bytes(&p.pwd));
}
Action::MouseShape(s) => v.mouse_shape = *s as c_int, Action::MouseShape(s) => v.mouse_shape = *s as c_int,
Action::MouseVisibility(m) => v.mouse_visibility = *m as c_int, Action::MouseVisibility(m) => v.mouse_visibility = *m as c_int,
Action::MouseOverLink(l) => { Action::MouseOverLink(l) => {
+4 -3
View File
@@ -249,9 +249,10 @@ pub unsafe extern "C" fn ghostty_config_load_file(ptr: ghostty_config_t, path: *
return; return;
} }
// SAFETY: `path` is NUL terminated. // SAFETY: `path` is NUL terminated.
let path = unsafe { CStr::from_ptr(path) } // Non-UTF-8 file names are valid on Unix; keep the bytes (reversible
.to_string_lossy() // escape understood by the config file loader).
.into_owned(); let path =
ghostty_os::osbytes::bytes_to_config_string(unsafe { CStr::from_ptr(path) }.to_bytes());
if let Err(err) = c.config.load_file(&path) { if let Err(err) = c.config.load_file(&path) {
log::error!("error loading config from file path={path} err={err}"); log::error!("error loading config from file path={path} err={err}");
} }
+81 -15
View File
@@ -42,6 +42,7 @@ use ghostty_input::{
binding::Action as BindingAction, binding::Action as BindingAction,
keycodes, keycodes,
}; };
use ghostty_os::osbytes;
use ghostty_terminal::{ use ghostty_terminal::{
point::{Coordinate, Point, Tag as PointTag}, point::{Coordinate, Point, Tag as PointTag},
selection::Selection, selection::Selection,
@@ -884,17 +885,22 @@ fn string_escape(input: &str) -> String {
unsafe fn apply_surface_options(config: &mut Config, opts: &ghostty_surface_config_s) { unsafe fn apply_surface_options(config: &mut Config, opts: &ghostty_surface_config_s) {
// If we have a working directory from the options then we set it. // If we have a working directory from the options then we set it.
// SAFETY: per the function contract. // SAFETY: per the function contract.
if let Some(wd) = if let Some(wd) = unsafe { cstr(opts.working_directory) }.map(CStr::to_bytes)
unsafe { cstr(opts.working_directory) }.map(|c| c.to_string_lossy().into_owned())
&& !wd.is_empty() && !wd.is_empty()
{ {
match std::fs::metadata(&wd) { // Raw bytes are kept (reversible escape in the String based config).
Err(err) => log::warn!("error opening requested working directory dir={wd} err={err}"), let wd_str = osbytes::bytes_to_config_string(wd);
let wd_os = osbytes::os_from_bytes(wd.to_vec());
let wd_disp = String::from_utf8_lossy(wd);
match std::fs::metadata(&wd_os) {
Err(err) => {
log::warn!("error opening requested working directory dir={wd_disp} err={err}");
}
Ok(m) if !m.is_dir() => { Ok(m) if !m.is_dir() => {
log::warn!("requested working directory is not a directory dir={wd}"); log::warn!("requested working directory is not a directory dir={wd_disp}");
} }
Ok(_) => { Ok(_) => {
let mut wd_val = WorkingDirectory::Path(wd); let mut wd_val = WorkingDirectory::Path(wd_str);
wd_val.finalize(); wd_val.finalize();
config.working_directory = Some(wd_val); config.working_directory = Some(wd_val);
} }
@@ -903,10 +909,10 @@ unsafe fn apply_surface_options(config: &mut Config, opts: &ghostty_surface_conf
// If we have a command from the options then we set it. // If we have a command from the options then we set it.
// SAFETY: per the function contract. // SAFETY: per the function contract.
if let Some(cmd) = unsafe { cstr(opts.command) }.map(|c| c.to_string_lossy().into_owned()) if let Some(cmd) = unsafe { cstr(opts.command) }.map(CStr::to_bytes)
&& !cmd.is_empty() && !cmd.is_empty()
{ {
config.command = Some(Command::Shell(cmd)); config.command = Some(Command::Shell(osbytes::bytes_to_config_string(cmd)));
config.wait_after_command = true; config.wait_after_command = true;
} }
@@ -919,7 +925,10 @@ unsafe fn apply_surface_options(config: &mut Config, opts: &ghostty_surface_conf
let (Some(k), Some(v)) = (unsafe { cstr(var.key) }, unsafe { cstr(var.value) }) else { let (Some(k), Some(v)) = (unsafe { cstr(var.key) }, unsafe { cstr(var.value) }) else {
continue; continue;
}; };
config.env.put(&k.to_string_lossy(), &v.to_string_lossy()); config.env.put(
&osbytes::bytes_to_config_string(k.to_bytes()),
&osbytes::bytes_to_config_string(v.to_bytes()),
);
} }
} }
@@ -929,10 +938,9 @@ unsafe fn apply_surface_options(config: &mut Config, opts: &ghostty_surface_conf
// We need to escape the string because the "raw" field expects a // We need to escape the string because the "raw" field expects a
// Zig string. // Zig string.
config.input.list.clear(); config.input.list.clear();
config config.input.list.push(ReadableIO::Raw(string_escape(
.input &osbytes::bytes_to_config_string(input.to_bytes()),
.list )));
.push(ReadableIO::Raw(string_escape(&input.to_string_lossy())));
} }
// Wait after command // Wait after command
@@ -1107,7 +1115,7 @@ pub unsafe extern "C" fn ghostty_surface_inherited_config(
} }
if should_inherit_working_directory(context, &config) if should_inherit_working_directory(context, &config)
&& let Some(cwd) = core.pwd() && let Some(cwd) = core.pwd_bytes()
&& let Ok(c) = CString::new(cwd) && let Ok(c) = CString::new(cwd)
{ {
let mut keep = s.inherited_wd.borrow_mut(); let mut keep = s.inherited_wd.borrow_mut();
@@ -1593,7 +1601,7 @@ pub unsafe extern "C" fn ghostty_surface_text(
// SAFETY: `text` points to `len` readable bytes. // SAFETY: `text` points to `len` readable bytes.
unsafe { std::slice::from_raw_parts(text.cast::<u8>(), len) } unsafe { std::slice::from_raw_parts(text.cast::<u8>(), len) }
}; };
if let Err(err) = core.text_callback(&String::from_utf8_lossy(bytes)) { if let Err(err) = core.text_callback_bytes(bytes) {
log::error!("error in key callback err={err}"); log::error!("error in key callback err={err}");
} }
} }
@@ -2155,3 +2163,61 @@ mod darwin {
fill_text(core, t, sel, result) fill_text(core, t, sel, result)
} }
} }
#[cfg(test)]
mod tests {
use std::os::unix::ffi::OsStrExt;
use super::*;
use crate::types::ghostty_env_var_s;
/// Surface options are raw C bytes in Zig; non-UTF-8 values must reach
/// the config unchanged (reversible byte escape).
#[test]
fn surface_options_keep_non_utf8_bytes() {
let td = tempfile::tempdir().unwrap();
let mut dir = td.path().as_os_str().as_bytes().to_vec();
dir.extend_from_slice(b"/d\xff");
std::fs::create_dir(std::ffi::OsStr::from_bytes(&dir)).unwrap();
let wd = CString::new(dir.clone()).unwrap();
let cmd = CString::new(&b"echo \xfe"[..]).unwrap();
let key = CString::new(&b"K\xfd"[..]).unwrap();
let val = CString::new(&b"v\xfc"[..]).unwrap();
let input = CString::new(&b"in\xff\n"[..]).unwrap();
let mut env = [ghostty_env_var_s {
key: key.as_ptr(),
value: val.as_ptr(),
}];
// SAFETY: all-zero is a valid value (null pointers, 0 enums).
let mut opts: ghostty_surface_config_s = unsafe { std::mem::zeroed() };
opts.working_directory = wd.as_ptr();
opts.command = cmd.as_ptr();
opts.env_vars = env.as_mut_ptr();
opts.env_var_count = 1;
opts.initial_input = input.as_ptr();
let mut config = Config::default();
// SAFETY: the pointers above are valid NUL terminated strings.
unsafe { apply_surface_options(&mut config, &opts) };
let got_wd = config
.working_directory
.as_ref()
.and_then(|w| w.value())
.unwrap();
assert_eq!(osbytes::config_str_to_bytes(got_wd), dir);
let Some(Command::Shell(c)) = &config.command else {
panic!("no command");
};
assert_eq!(osbytes::config_str_to_bytes(c), b"echo \xfe");
let (k, v) = config.env.iter().next().unwrap();
assert_eq!(osbytes::config_str_to_bytes(k), b"K\xfd");
assert_eq!(osbytes::config_str_to_bytes(v), b"v\xfc");
assert!(matches!(
config.input.list.first(),
Some(ReadableIO::Raw(_))
));
}
}
+47 -5
View File
@@ -4,12 +4,13 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
ffi::{CStr, CString, c_char, c_int}, ffi::{CStr, CString, OsString, c_char, c_int},
sync::{Mutex, OnceLock}, sync::{Mutex, OnceLock},
}; };
use ghostty_cli::build_info::{self, BuildInfo}; use ghostty_cli::build_info::{self, BuildInfo};
use ghostty_core::global::{self, InitOptions}; use ghostty_core::global::{self, InitOptions};
use ghostty_os::osbytes;
use crate::types::{build_mode, ghostty_info_s, ghostty_string_s}; use crate::types::{build_mode, ghostty_info_s, ghostty_string_s};
@@ -77,7 +78,8 @@ pub(crate) fn alloc_c_text(bytes: &[u8]) -> *const c_char {
/// `argv` must be null or point to `argc` valid NUL terminated strings. /// `argv` must be null or point to `argc` valid NUL terminated strings.
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_init(argc: usize, argv: *mut *mut c_char) -> c_int { pub unsafe extern "C" fn ghostty_init(argc: usize, argv: *mut *mut c_char) -> c_int {
let mut args = Vec::with_capacity(argc); // Raw argument bytes (Zig: `std.os.argv = argv[0..argc]`).
let mut raw: Vec<OsString> = Vec::with_capacity(argc);
if !argv.is_null() { if !argv.is_null() {
for i in 0..argc { for i in 0..argc {
// SAFETY: the caller guarantees `argv[0..argc]` are valid. // SAFETY: the caller guarantees `argv[0..argc]` are valid.
@@ -87,10 +89,18 @@ pub unsafe extern "C" fn ghostty_init(argc: usize, argv: *mut *mut c_char) -> c_
} }
// SAFETY: each argv entry is a NUL terminated string. // SAFETY: each argv entry is a NUL terminated string.
let c = unsafe { CStr::from_ptr(p) }; let c = unsafe { CStr::from_ptr(p) };
args.push(c.to_string_lossy().into_owned()); raw.push(osbytes::os_from_bytes(c.to_bytes().to_vec()));
} }
} }
// Later config CLI loading (`ghostty_config_load_cli_args`) must see
// these arguments rather than the process arguments.
ghostty_os::args::set_argv(raw.clone());
let args: Vec<String> = raw
.iter()
.map(|a| osbytes::os_to_config_string(a))
.collect();
// We first try to parse any action that we may be executing. // We first try to parse any action that we may be executing.
let action = match ghostty_cli::ghostty::detect(&args) { let action = match ghostty_cli::ghostty::detect(&args) {
Ok(a) => a, Ok(a) => a,
@@ -101,16 +111,48 @@ pub unsafe extern "C" fn ghostty_init(argc: usize, argv: *mut *mut c_char) -> c_
}; };
let _ = ARGS.set(args); let _ = ARGS.set(args);
// `+version` and friends report the library build.
build_info::set(lib_build_info());
// Setup our signal handlers before logging (Zig: `initSignals`). A C or
// Swift host does not get the Rust runtime's SIGPIPE ignore.
init_signals();
global::init_lib(InitOptions { global::init_lib(InitOptions {
action: action.is_some(), action: action.is_some(),
resources_dir: None, resources_dir: None,
crash: Some(lib_crash_options()),
}); });
// `+version` and friends report the library build.
build_info::set(lib_build_info());
0 0
} }
/// Ignore SIGPIPE: it is common with how termio shuts down read threads
/// (Zig: `GlobalState.initSignals`; not on Windows).
fn init_signals() {
#[cfg(unix)]
// SAFETY: installing SIG_IGN for SIGPIPE has no memory safety impact.
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_IGN);
}
}
/// Crash metadata for the library artifact: no app runtime, CoreText +
/// Metal on Apple targets (so the GTK / fontconfig / OpenGL defaults are
/// not reported for the macOS app).
fn lib_crash_options() -> ghostty_crash::Options {
let info = build_info::get();
let tag = |s: &str| s.trim_start_matches('.').to_owned();
let mut o = global::default_crash_options();
o.release.clone_from(&info.version_string);
o.app_runtime = tag(&lib_build_info().app_runtime);
if cfg!(target_vendor = "apple") {
"coretext".clone_into(&mut o.font_backend);
"metal".clone_into(&mut o.renderer);
}
o
}
/// The build info of the library (Zig: `build_config` for the lib artifact: /// The build info of the library (Zig: `build_config` for the lib artifact:
/// no app runtime, CoreText + Metal on Apple targets). /// no app runtime, CoreText + Metal on Apple targets).
fn lib_build_info() -> BuildInfo { fn lib_build_info() -> BuildInfo {
+8 -2
View File
@@ -43,7 +43,10 @@ fn host_program_passes() {
"host failed\nstdout:\n{stdout}\nstderr:\n{stderr}" "host failed\nstdout:\n{stdout}\nstderr:\n{stderr}"
); );
assert!(stdout.contains("OK"), "stdout:\n{stdout}"); assert!(stdout.contains("OK"), "stdout:\n{stdout}");
assert!(stdout.contains("version=1.3.1"), "stdout:\n{stdout}"); assert!(
stdout.contains(&format!("version={}", ghostty_build_info::VERSION_STRING)),
"stdout:\n{stdout}"
);
} }
#[test] #[test]
@@ -55,7 +58,10 @@ fn cli_try_action_runs_version_and_exits() {
"stderr: {}", "stderr: {}",
String::from_utf8_lossy(&out.stderr) String::from_utf8_lossy(&out.stderr)
); );
assert!(stdout.starts_with("Ghostty 1.3.1"), "stdout:\n{stdout}"); assert!(
stdout.starts_with(&format!("Ghostty {}", ghostty_build_info::VERSION_STRING)),
"stdout:\n{stdout}"
);
// The action ran instead of the rest of the program. // The action ran instead of the rest of the program.
assert!(!stdout.contains("OK")); assert!(!stdout.contains("OK"));
} }
+61
View File
@@ -0,0 +1,61 @@
//! `ghostty_init(argc, argv)` semantics from `main_c.zig`: the supplied
//! argument vector (not the process arguments) feeds the later config CLI
//! loading, and SIGPIPE is ignored for C / Swift hosts.
#![cfg(unix)]
#![allow(
unsafe_code,
clippy::unwrap_used,
clippy::expect_used,
clippy::undocumented_unsafe_blocks
)]
use std::ffi::{CString, c_char};
use ghostty::{config_capi::*, main_c::ghostty_init, types::ghostty_config_t};
fn get_bool(cfg: ghostty_config_t, key: &str) -> bool {
let mut out = false;
let ok =
unsafe { ghostty_config_get(cfg, (&raw mut out).cast(), key.as_ptr().cast(), key.len()) };
assert!(ok);
out
}
#[test]
fn init_argv_feeds_config_cli_and_ignores_sigpipe() {
// Start from the default disposition so that the check is meaningful.
unsafe { libc::signal(libc::SIGPIPE, libc::SIG_DFL) };
// Not the process argv: the test harness argv has no `--maximize`.
let owned: Vec<CString> = [&b"ghostty"[..], b"--maximize=true"]
.iter()
.map(|a| CString::new(*a).unwrap())
.collect();
let mut argv: Vec<*mut c_char> = owned.iter().map(|c| c.as_ptr().cast_mut()).collect();
assert_eq!(unsafe { ghostty_init(argv.len(), argv.as_mut_ptr()) }, 0);
let mut sa: libc::sigaction = unsafe { std::mem::zeroed() };
unsafe { libc::sigaction(libc::SIGPIPE, std::ptr::null(), &mut sa) };
assert_eq!(sa.sa_sigaction, libc::SIG_IGN);
let cfg = ghostty_config_new();
unsafe { ghostty_config_load_cli_args(cfg) };
assert!(get_bool(cfg, "maximize"));
unsafe { ghostty_config_free(cfg) };
}
#[test]
fn config_load_file_accepts_non_utf8_path() {
use std::os::unix::ffi::OsStrExt;
let dir = tempfile::tempdir().unwrap();
let mut name = dir.path().as_os_str().as_bytes().to_vec();
name.extend_from_slice(b"/cfg\xff");
let path = std::ffi::OsStr::from_bytes(&name);
std::fs::write(path, "maximize = true\n").unwrap();
let cfg = ghostty_config_new();
let c = CString::new(name).unwrap();
unsafe { ghostty_config_load_file(cfg, c.as_ptr()) };
assert!(get_bool(cfg, "maximize"));
unsafe { ghostty_config_free(cfg) };
}
+1
View File
@@ -10,6 +10,7 @@ ghostty-apprt.workspace = true
ghostty-config.workspace = true ghostty-config.workspace = true
ghostty-crash.workspace = true ghostty-crash.workspace = true
ghostty-datastruct.workspace = true ghostty-datastruct.workspace = true
ghostty-build-info.workspace = true
ghostty-os.workspace = true ghostty-os.workspace = true
ghostty-renderer.workspace = true ghostty-renderer.workspace = true
ghostty-terminal.workspace = true ghostty-terminal.workspace = true
+37 -5
View File
@@ -18,7 +18,7 @@ use std::{
panic::{AssertUnwindSafe, catch_unwind}, panic::{AssertUnwindSafe, catch_unwind},
}; };
use ghostty_os::file::Rlimit; use ghostty_os::{file::Rlimit, osbytes::config_str_to_bytes};
use rustix::{ use rustix::{
io::Errno, io::Errno,
process::{self, WaitOptions}, process::{self, WaitOptions},
@@ -264,13 +264,13 @@ impl Command {
} }
fn prepare(&self) -> Result<Prepared, NulError> { fn prepare(&self) -> Result<Prepared, NulError> {
let path = CString::new(self.path.as_str())?; let path = CString::new(config_str_to_bytes(&self.path))?;
let argv = if self.args.is_empty() { let argv = if self.args.is_empty() {
vec![path.clone()] vec![path.clone()]
} else { } else {
self.args self.args
.iter() .iter()
.map(|a| CString::new(a.as_str())) .map(|a| CString::new(config_str_to_bytes(a)))
.collect::<Result<_, _>>()? .collect::<Result<_, _>>()?
}; };
let envp = match &self.env { let envp = match &self.env {
@@ -297,7 +297,7 @@ impl Command {
#[cfg(target_vendor = "apple")] #[cfg(target_vendor = "apple")]
let candidates = exec_candidates(&self.path, std::env::var("PATH").ok().as_deref()) let candidates = exec_candidates(&self.path, std::env::var("PATH").ok().as_deref())
.into_iter() .into_iter()
.map(CString::new) .map(|c| CString::new(config_str_to_bytes(&c)))
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
Ok(Prepared { Ok(Prepared {
path, path,
@@ -315,7 +315,10 @@ impl Command {
pub fn start(&mut self) -> Result<(), StartError> { pub fn start(&mut self) -> Result<(), StartError> {
// IMPORTANT: do all allocation prior to the fork(). // IMPORTANT: do all allocation prior to the fork().
let prep = self.prepare()?; let prep = self.prepare()?;
let cwd = self.cwd.as_deref().and_then(|c| CString::new(c).ok()); let cwd = self
.cwd
.as_deref()
.and_then(|c| CString::new(config_str_to_bytes(c)).ok());
let mut argv_ptrs: Vec<*const c_char> = prep.argv.iter().map(|c| c.as_ptr()).collect(); let mut argv_ptrs: Vec<*const c_char> = prep.argv.iter().map(|c| c.as_ptr()).collect();
argv_ptrs.push(std::ptr::null()); argv_ptrs.push(std::ptr::null());
@@ -876,6 +879,35 @@ mod tests {
assert_eq!("/tmp\n", read_all(stdout)); assert_eq!("/tmp\n", read_all(stdout));
} }
#[test]
fn non_utf8_cwd_and_args_are_preserved() {
use ghostty_os::osbytes::{bytes_to_config_string, os_from_bytes};
let td = TempDir::init().unwrap();
let stdout = test_file(&td, "stdout.txt");
// A directory whose name is not valid UTF-8, handed over with the
// reversible byte escape used by the String based layers.
let mut raw = ghostty_os::osbytes::os_bytes(td.path().as_os_str());
raw.extend_from_slice(b"/d\xff");
std::fs::create_dir(os_from_bytes(raw.clone())).unwrap();
let mut cmd = Command::new("/bin/sh", &["/bin/sh", "-c", "pwd -P; printf %s \"$0\""]);
cmd.args.push(bytes_to_config_string(b"a\xffb"));
cmd.stdout = Some(stdout.try_clone().unwrap().into());
cmd.cwd = Some(bytes_to_config_string(&raw));
cmd.start().unwrap();
assert_eq!(cmd.wait(true).unwrap(), Exit::Exited(0));
let mut bytes = Vec::new();
std::io::Read::read_to_end(
&mut std::fs::File::open(td.path().join("stdout.txt")).unwrap(),
&mut bytes,
)
.unwrap();
assert!(bytes.windows(2).any(|w| w == b"d\xff"), "{bytes:?}");
assert!(bytes.ends_with(b"a\xffb"), "{bytes:?}");
}
#[test] #[test]
fn invalid_cwd_is_ignored() { fn invalid_cwd_is_ignored() {
let td = TempDir::init().unwrap(); let td = TempDir::init().unwrap();
+7 -4
View File
@@ -181,7 +181,7 @@ impl Exec {
// set that on the terminal now. This allows rapidly initializing // set that on the terminal now. This allows rapidly initializing
// new surfaces to use the proper pwd. // new surfaces to use the proper pwd.
if let Some(cwd) = &self.subprocess.cwd if let Some(cwd) = &self.subprocess.cwd
&& let Err(err) = term.set_pwd(cwd.as_bytes()) && let Err(err) = term.set_pwd(&ghostty_os::osbytes::config_str_to_bytes(cwd))
{ {
log::warn!("error setting initial pwd err={err:?}"); log::warn!("error setting initial pwd err={err:?}");
} }
@@ -907,7 +907,10 @@ impl Subprocess {
// Add the environment variables that override any others. // Add the environment variables that override any others.
for (k, v) in cfg.env_override.iter() { for (k, v) in cfg.env_override.iter() {
env.insert(k, v); env.insert(
ghostty_os::osbytes::config_str_to_os(k),
ghostty_os::osbytes::config_str_to_os(v),
);
} }
// Build our args list // Build our args list
@@ -925,7 +928,7 @@ impl Subprocess {
// https://github.com/kovidgoyal/kitty/issues/1595 // https://github.com/kovidgoyal/kitty/issues/1595
// https://github.com/ghostty-org/ghostty/discussions/7769 // https://github.com/ghostty-org/ghostty/discussions/7769
if let Some(pwd) = &cwd { if let Some(pwd) = &cwd {
env.insert("PWD", pwd.clone()); env.insert("PWD", ghostty_os::osbytes::config_str_to_os(pwd));
} }
Ok(Subprocess { Ok(Subprocess {
@@ -1037,7 +1040,7 @@ impl Subprocess {
} }
} }
Some(proposed) => { Some(proposed) => {
if std::path::Path::new(proposed).exists() { if std::path::Path::new(&ghostty_os::osbytes::config_str_to_os(proposed)).exists() {
Some(proposed.clone()) Some(proposed.clone())
} else { } else {
log::warn!("cannot access cwd, ignoring: {proposed}"); log::warn!("cannot access cwd, ignoring: {proposed}");
+6 -2
View File
@@ -86,7 +86,7 @@ use crate::{
}; };
/// `build_config.version_string`. /// `build_config.version_string`.
pub const VERSION_STRING: &str = "1.3.1"; pub const VERSION_STRING: &str = ghostty_build_info::VERSION_STRING;
/// The long-lived state of the handler (everything but the terminal). /// The long-lived state of the handler (everything but the terminal).
pub struct HandlerState { pub struct HandlerState {
@@ -1693,7 +1693,11 @@ mod tests {
fn xtversion() { fn xtversion() {
let mut h = Harness::new(); let mut h = Harness::new();
h.feed(b"\x1b[>q"); h.feed(b"\x1b[>q");
assert_eq!(b"\x1bP>|ghostty 1.3.1\x1b\\".as_slice(), h.written()); let want = format!(
"\x1bP>|ghostty {}\x1b\\",
ghostty_build_info::VERSION_STRING
);
assert_eq!(want.as_bytes(), h.written());
} }
#[test] #[test]
+1 -1
View File
@@ -173,7 +173,7 @@ impl ThreadEnterState {
let mut out = Vec::with_capacity(self.input.len()); let mut out = Vec::with_capacity(self.input.len());
for item in &self.input { for item in &self.input {
match item { match item {
ReadableIO::Raw(v) => out.push(v.clone().into_bytes()), ReadableIO::Raw(v) => out.push(ghostty_os::osbytes::config_str_to_bytes(v)),
ReadableIO::Path(path) => { ReadableIO::Path(path) => {
let f = std::fs::File::open(path).map_err(|err| { let f = std::fs::File::open(path).map_err(|err| {
log::warn!("failed to open input file={path} err={err}"); log::warn!("failed to open input file={path} err={err}");
+8
View File
@@ -0,0 +1,8 @@
# Z2 final notes (core / lib / termio / apprt-gtk)
- Windows: `cargo check --target x86_64-pc-windows-gnu -p ghostty-core --tests` passes. Surface init gates `rt_pre_exec_info`, `rt_post_fork_info`, `create_scope` and `restore_rlimit` on `cfg(unix)`. Screen-file writing only sets mode 0600 on unix. The test that read the file mode is unix-gated.
- Embedded init: `global::init` calls `ensure_locale`. `InitOptions.crash` carries crash metadata, with `default_crash_options()` (shared version; Metal/CoreText/`none` tags on Apple). `ghostty_init` ignores SIGPIPE and passes crash options for the lib build. It also stores the raw argv with `ghostty_os::args::set_argv`, so `ghostty_config_load_cli_args` uses it (I added `set_argv` and a vec-returning `iterator()` in ghostty-os/args.rs). `ghostty_config_load_file` keeps non-UTF-8 path bytes (`file_load::open` decodes them).
- Bytes: `Pwd.pwd` and `Surface::pwd()` carry OSC 7 bytes with the reversible `osbytes` escape. `pwd_bytes()` returns the raw bytes. Sinks decode: C `Action::Pwd`, inherited working dir, termio cwd/args/env/PWD/`Raw` input. Embedded surface working-directory/command/env/initial_input and `ghostty_surface_text` (`text_callback_bytes`) pass raw bytes. GTK file drop uses `paste_bytes` and a new `CoreSurface::complete_paste_bytes`.
- Version: XTVERSION, `TERM_PROGRAM_VERSION`, both GTK About branches and crash metadata use `ghostty_build_info::VERSION_STRING`.
- Tests added: e2e OSC 7 non-UTF-8, `command.rs` non-UTF-8 cwd/args, `embedded` surface-options bytes, `tests/init_argv.rs` (argv, SIGPIPE, non-UTF-8 load_file), crash defaults.
- Gate: workspace build, tests and clippy (`--no-deps -D warnings`) for my crates pass, and `cargo +nightly fmt` is applied. Not done: macOS cross clippy (the wrapper setup was declined at the tool prompt). No GTK drop UI test (needs a display). The paste confirmation dialog is text-based, so it carries non-UTF-8 bytes via the escape.
+6 -4
View File
@@ -20,16 +20,18 @@ fn main() -> ExitCode {
} }
fn real_main() -> ExitCode { fn real_main() -> ExitCode {
// Byte-safe argv (`std::env::args()` panics on non-UTF-8 arguments). // Raw argv bytes (Zig `[]const u8`); `std::env::args()` would panic on
let args: Vec<String> = ghostty_os::args::collect_lossy(); // non-UTF-8 arguments and a lossy conversion would change them.
let args: Vec<std::ffi::OsString> = ghostty_os::args::collect_os();
// We first start by initializing our global state. This sets up // We first start by initializing our global state. This sets up
// process-level state we need to run the terminal. We detect whether a // process-level state we need to run the terminal. We detect whether a
// CLI action was requested so logging can be silenced for it. // CLI action was requested so logging can be silenced for it.
let action_requested = !matches!(ghostty_cli::ghostty::detect(&args), Ok(None)); let action_requested = !matches!(ghostty_cli::ghostty::detect_os(&args), Ok(None));
global::init(InitOptions { global::init(InitOptions {
action: action_requested, action: action_requested,
resources_dir: Some(ghostty_apprt_gtk::flatpak::resources_dir), resources_dir: Some(ghostty_apprt_gtk::flatpak::resources_dir),
..InitOptions::default()
}); });
// Zig `global.init` runs `ensureLocale` before anything locale dependent. // Zig `global.init` runs `ensureLocale` before anything locale dependent.
ghostty_apprt_gtk::class::application::init_locale(); ghostty_apprt_gtk::class::application::init_locale();
@@ -38,7 +40,7 @@ fn real_main() -> ExitCode {
// Execute our action if we have one. // Execute our action if we have one.
if action_requested { if action_requested {
ghostty_cli::new_window::set_ipc_handler(new_window_ipc); ghostty_cli::new_window::set_ipc_handler(new_window_ipc);
if let Some(code) = ghostty_cli::run_from_args(&args) { if let Some(code) = ghostty_cli::ghostty::run_from_os_args(&args) {
return ExitCode::from(code); return ExitCode::from(code);
} }
} }