feat: port GTK version fallbacks, helpgen, bench modes, CLI leftovers

- apprt-gtk: Ghostty's runtime+build-time version gating (at_least)
  with cargo features adw_1_3/adw_1_4/adw_1_5/gtk_4_16 (default on) and
  the fallbacks for older libraries: MessageDialog (adw < 1.5),
  clipboard dialog without SwitchRow, pre-Banner debug warning and
  child-exited widgets, the old about dialog, GTK 4.16 CSS error source
  bytes. The minimum stays GTK 4.14 / libadwaita 1.5 (Ghostty's floor).
  The root build links with -z lazy so version-gated symbols don't keep
  older libraries from loading.
- ghostty-helpgen (port of helpgen.zig) generates CLI action help and
  keybinding action docs from doc comments at build time, replacing the
  checked-in help/*.txt; +help and +list-actions --docs stay
  byte-identical to Ghostty 1.3.1.
- ghostty-bench: libc wcwidth mode and separate simd/table and
  uucode/table code paths like Zig.
- ghostty-cli: Windows branches of edit-config, list-colors,
  list-keybinds and boo; ssh-cache DiskCache flock locking (no-op on
  Windows, as in Zig) with an exclusivity test.

Test Plan:
- cargo test --workspace
- diff <(cargo run -q -- +list-actions --docs) \
  <(ghostty +list-actions --docs)
This commit is contained in:
ddidderr committed 2026-10-12 06:38:33 +02:00
1 parent 020beb2d75
commit 4787565397
51 files changed
+2426 -1621

No files matched your search

+76 -15
View File
@@ -2,12 +2,13 @@
//! (ZIG/src/cli/ssh-cache/DiskCache.zig). The cache only stores metadata
//! (hostname, terminfo value, etc.) and does not store any sensitive data.
#[cfg(unix)]
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::{
collections::BTreeMap,
fs::{File, OpenOptions},
io::{Read, Write},
net::Ipv6Addr,
os::unix::fs::{OpenOptionsExt, PermissionsExt},
path::PathBuf,
};
@@ -87,13 +88,11 @@ impl DiskCache {
}
// Open or create cache file with secure permissions
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.mode(0o600)
.open(&self.path)?;
let mut open = OpenOptions::new();
open.read(true).write(true).create(true).truncate(false);
#[cfg(unix)]
open.mode(0o600);
let file = open.open(&self.path)?;
fixup_permissions(&file)?;
// Lock
@@ -196,12 +195,11 @@ impl DiskCache {
let tmp_path = cache_dir.join(tmp_name);
let result = (|| -> std::io::Result<()> {
let mut f = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(&tmp_path)?;
let mut open = OpenOptions::new();
open.write(true).create(true).truncate(true);
#[cfg(unix)]
open.mode(0o600);
let mut f = open.open(&tmp_path)?;
f.write_all(data.as_bytes())?;
f.sync_all()?;
std::fs::rename(&tmp_path, &self.path)
@@ -227,6 +225,18 @@ impl Drop for Lock<'_> {
/// Zig: `_ = file.tryLock(.exclusive) catch return error.CacheIsLocked;`
/// The boolean result is ignored upstream, so a lock held by someone else
/// does not fail; only a real error does.
///
/// Zig skips locking entirely on Windows
/// (`if (comptime builtin.os.tag != .windows)`), so we do too. On unix
/// `File::try_lock` is `flock(LOCK_EX | LOCK_NB)`, the same call as Zig's
/// `File.tryLock(.exclusive)`.
#[cfg(windows)]
#[allow(clippy::unnecessary_wraps)]
fn lock(_file: &File) -> Result<Lock<'_>, Error> {
Ok(Lock(None))
}
#[cfg(not(windows))]
fn lock(file: &File) -> Result<Lock<'_>, Error> {
match file.try_lock() {
Ok(()) => Ok(Lock(Some(file))),
@@ -235,7 +245,20 @@ fn lock(file: &File) -> Result<Lock<'_>, Error> {
}
}
#[cfg_attr(windows, allow(clippy::unnecessary_wraps))]
fn fixup_permissions(file: &File) -> Result<(), Error> {
// Windows does not support chmod
#[cfg(windows)]
{
let _ = file;
Ok(())
}
#[cfg(unix)]
fixup_permissions_unix(file)
}
#[cfg(unix)]
fn fixup_permissions_unix(file: &File) -> Result<(), Error> {
// Ensure file has correct permissions (readable/writable by
// owner only)
let meta = file.metadata()?;
@@ -323,6 +346,34 @@ fn is_valid_user(user: &str) -> bool {
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#[cfg(not(windows))]
#[test]
fn lock_is_exclusive_until_dropped() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("lock");
let a = File::create(&path).unwrap();
let b = File::options().read(true).write(true).open(&path).unwrap();
let held = super::lock(&a).unwrap();
assert!(held.0.is_some());
// A second handle can't take the lock; Zig ignores that result.
assert!(matches!(
b.try_lock(),
Err(std::fs::TryLockError::WouldBlock)
));
assert!(super::lock(&b).unwrap().0.is_none());
drop(held);
// Unlocked on drop.
assert!(b.try_lock().is_ok());
}
#[cfg(windows)]
#[test]
fn lock_is_noop_on_windows() {
let dir = tempfile::tempdir().unwrap();
let a = File::create(dir.path().join("lock")).unwrap();
assert!(super::lock(&a).unwrap().0.is_none());
}
use super::*;
#[test]
@@ -389,8 +440,9 @@ mod tests {
assert_eq!(vec![std::ffi::OsString::from("cache")], names);
}
#[cfg(unix)]
#[test]
fn disk_cache_file_permissions_and_invalid_host() {
fn disk_cache_file_permissions() {
let tmp = tempfile::tempdir().unwrap();
let cache = DiskCache {
path: tmp.path().join("sub/cache"),
@@ -398,6 +450,15 @@ mod tests {
cache.add("example.com").unwrap();
let mode = std::fs::metadata(&cache.path).unwrap().permissions().mode();
assert_eq!(0o600, mode & 0o777);
}
#[test]
fn disk_cache_invalid_host() {
let tmp = tempfile::tempdir().unwrap();
let cache = DiskCache {
path: tmp.path().join("sub/cache"),
};
cache.add("example.com").unwrap();
assert!(matches!(
cache.add("bad host"),
Err(Error::HostnameIsInvalid)