feat: honor async-backend with epoll/io_uring, fix last RV13 items
Resolves the 4 P3 items in docs/rewrite/notes/review-rv13-codex.md (final-Z16.md): - async-backend now takes effect like Ghostty: ghostty_os::xev ports libxev's backend detection (io_uring, then epoll); global init detects and logs it, the GTK startup applies the config with Zig's messages, and +version reports the real backend. The termio IO loop runs on a new reactor with epoll, io_uring (io-uring crate) and poll(2) (kept for macOS, where libxev uses kqueue). The 21 exec integration tests run under every backend; io_uring was exercised here. - auto-update-channel defaults to the build's release channel. - Windows error names: sharing violation -> AccessDenied, plus ERROR_FILE_EXISTS -> PathAlreadyExists and virus errors -> AntivirusInterference, mirroring std.os.windows.OpenFile's NTSTATUS mapping in Zig 0.15.2 (verified against its source by the orchestrator). - Exact ports of std.fs.path.basenameWindows/basenamePosix for theme name validation (drive, UNC and device roots). Test Plan: - cargo test --workspace --no-fail-fast (3555 passed, exit 0) - just check-macos (both), just check-windows, just check-wasm - +show-config --default --docs vs /usr/bin/ghostty (only the build-specific gtk-opengl-debug line differs)
This commit is contained in:
1 parent
3ce3225642
commit
1bd918159a
18 files changed
+2012
-742
No files matched your search
@@ -6,6 +6,7 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
const_format = "0.2.36"
|
||||
ghostty-build-info.workspace = true
|
||||
ghostty-input.workspace = true
|
||||
ghostty-os.workspace = true
|
||||
ghostty-terminal.workspace = true
|
||||
|
||||
@@ -28,6 +28,14 @@ use crate::{
|
||||
const NS_PER_S: u64 = 1_000_000_000;
|
||||
const NS_PER_MS: u64 = 1_000_000;
|
||||
|
||||
/// `build_config.release_channel` as a config value.
|
||||
fn build_release_channel() -> ReleaseChannel {
|
||||
match ghostty_build_info::RELEASE_CHANNEL {
|
||||
"tip" => ReleaseChannel::Tip,
|
||||
_ => ReleaseChannel::Stable,
|
||||
}
|
||||
}
|
||||
|
||||
config_enum! {
|
||||
/// The release channel of the build (Zig: `build_config.ReleaseChannel`).
|
||||
pub enum ReleaseChannel {
|
||||
@@ -4656,7 +4664,7 @@ impl Config {
|
||||
// We can't set this as a struct default because our config is
|
||||
// loaded in environments where a build config isn't available.
|
||||
if self.auto_update_channel.is_none() {
|
||||
self.auto_update_channel = Some(ReleaseChannel::Stable);
|
||||
self.auto_update_channel = Some(build_release_channel());
|
||||
}
|
||||
|
||||
self.faint_opacity = cli_args::zig_clamp(self.faint_opacity, 0.0, 1.0);
|
||||
@@ -5027,6 +5035,35 @@ impl CliTarget for Config {
|
||||
clippy::assert_is_empty
|
||||
)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn auto_update_channel_defaults_to_the_build_channel() {
|
||||
let mut cfg = Config::default();
|
||||
assert_eq!(cfg.auto_update_channel, None);
|
||||
cfg.finalize().unwrap();
|
||||
let expected = match ghostty_build_info::RELEASE_CHANNEL {
|
||||
"tip" => ReleaseChannel::Tip,
|
||||
"stable" => ReleaseChannel::Stable,
|
||||
other => panic!("unexpected channel {other}"),
|
||||
};
|
||||
assert_eq!(cfg.auto_update_channel, Some(expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_update_channel_explicit_value_is_kept() {
|
||||
for (text, channel) in [
|
||||
("stable", ReleaseChannel::Stable),
|
||||
("tip", ReleaseChannel::Tip),
|
||||
] {
|
||||
let mut cfg = Config::default();
|
||||
let data = format!("auto-update-channel = {text}\n");
|
||||
let mut reader = std::io::Cursor::new(data.into_bytes());
|
||||
cfg.load_reader(&mut reader, &EncodedOs::from_encoded("/c/config"))
|
||||
.unwrap();
|
||||
cfg.finalize().unwrap();
|
||||
assert_eq!(cfg.auto_update_channel, Some(channel));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn shell_from_env_round_trips_bytes() {
|
||||
|
||||
@@ -151,24 +151,43 @@ fn check_regular(file: &File, theme: &str, diags: &mut DiagnosticList) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// `std.fs.path.basename` (last `ComponentIterator` component; `.` and `..`
|
||||
/// are ordinary names). The caller only passes non-absolute paths, so the root
|
||||
/// is empty on POSIX and an optional `X:` drive designator on Windows.
|
||||
/// `std.fs.path.basename` for the host: `basenamePosix` or `basenameWindows`.
|
||||
fn zig_basename(path: &str) -> &str {
|
||||
let is_sep = |b: u8| b == b'/' || (cfg!(windows) && b == b'\\');
|
||||
let bytes = path.as_bytes();
|
||||
let root_end = if cfg!(windows)
|
||||
&& bytes.len() >= 2
|
||||
&& bytes[0].is_ascii_alphabetic()
|
||||
&& bytes[1] == b':'
|
||||
{
|
||||
2
|
||||
if cfg!(windows) {
|
||||
basename_windows(path)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
basename_posix(path)
|
||||
}
|
||||
}
|
||||
|
||||
/// `std.fs.path.basenamePosix`: the last `ComponentIterator` component.
|
||||
/// `.` and `..` are ordinary names.
|
||||
fn basename_posix(path: &str) -> &str {
|
||||
let root_len = usize::from(path.as_bytes().first() == Some(&b'/'));
|
||||
basename_inner(path, root_len, |b| b == b'/')
|
||||
}
|
||||
|
||||
/// `std.fs.path.basenameWindows`.
|
||||
fn basename_windows(path: &str) -> &str {
|
||||
basename_inner(path, parse_path_windows_root_len(path), |b| {
|
||||
b == b'/' || b == b'\\'
|
||||
})
|
||||
}
|
||||
|
||||
/// `ComponentIterator.init` + `last()`: skip the root and any separators after
|
||||
/// it, then take the last component (empty if there is none).
|
||||
fn basename_inner(path: &str, root_len: usize, is_sep: impl Fn(u8) -> bool) -> &str {
|
||||
let bytes = path.as_bytes();
|
||||
let mut root_end_index = root_len;
|
||||
for &c in &bytes[root_len..] {
|
||||
if !is_sep(c) {
|
||||
break;
|
||||
}
|
||||
root_end_index += 1;
|
||||
}
|
||||
let mut end = bytes.len();
|
||||
loop {
|
||||
if end == root_end {
|
||||
if end == root_end_index {
|
||||
return "";
|
||||
}
|
||||
if !is_sep(bytes[end - 1]) {
|
||||
@@ -177,12 +196,73 @@ fn zig_basename(path: &str) -> &str {
|
||||
end -= 1;
|
||||
}
|
||||
let mut start = end;
|
||||
while start != root_end && !is_sep(bytes[start - 1]) {
|
||||
while start != root_end_index && !is_sep(bytes[start - 1]) {
|
||||
start -= 1;
|
||||
}
|
||||
&path[start..end]
|
||||
}
|
||||
|
||||
/// `std.fs.path.parsePathWindows(u8, path).root.len` (with
|
||||
/// `getWin32PathType`; the path is WTF-8 so the drive letter is one whole code
|
||||
/// point of at most three bytes, like `RtlDetermineDosPathNameType_U`).
|
||||
fn parse_path_windows_root_len(path: &str) -> usize {
|
||||
let b = path.as_bytes();
|
||||
let sep = |c: u8| c == b'/' || c == b'\\';
|
||||
let Some(&first) = b.first() else {
|
||||
return 0; // relative
|
||||
};
|
||||
if sep(first) {
|
||||
// \x
|
||||
if b.len() < 2 || !sep(b[1]) {
|
||||
return 1; // rooted
|
||||
}
|
||||
// \\. or \\?
|
||||
if b.len() > 2 && (b[2] == b'.' || b[2] == b'?') {
|
||||
// exactly \\. or \\? with nothing trailing
|
||||
if b.len() == 3 {
|
||||
return b.len(); // root_local_device
|
||||
}
|
||||
// \\.\x or \\?\x
|
||||
if sep(b[3]) {
|
||||
return 4; // local_device
|
||||
}
|
||||
}
|
||||
// \\x: UNC absolute (`parseUNC`)
|
||||
let Some(server_end) = b[2..].iter().position(|&c| sep(c)).map(|i| i + 2) else {
|
||||
return b.len();
|
||||
};
|
||||
// Any number of separators may sit between the server and the share.
|
||||
let share_start =
|
||||
server_end + 1 + b[server_end + 1..].iter().take_while(|&&c| sep(c)).count();
|
||||
if share_start == b.len() {
|
||||
// No share: the share is the empty slice right after the server
|
||||
// separator.
|
||||
return server_end + 1;
|
||||
}
|
||||
let share_end = b[share_start..]
|
||||
.iter()
|
||||
.position(|&c| sep(c))
|
||||
.map_or(b.len(), |i| i + share_start);
|
||||
// `sep_after_share` adds one more.
|
||||
return share_end + usize::from(share_end != b.len());
|
||||
}
|
||||
// Drive letter: a code point of 1..=3 bytes followed by ':'.
|
||||
let colon_i = match first {
|
||||
0x00..=0x7f => 1,
|
||||
0xc2..=0xdf => 2,
|
||||
0xe0..=0xef => 3,
|
||||
_ => return 0, // invalid or 4-byte sequence: relative
|
||||
};
|
||||
if b.len() < colon_i + 1 || b[colon_i] != b':' {
|
||||
return 0; // relative
|
||||
}
|
||||
if b.len() > colon_i + 1 && sep(b[colon_i + 1]) {
|
||||
colon_i + 2 // drive_absolute
|
||||
} else {
|
||||
colon_i + 1 // drive_relative
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the given named theme. If there are any errors then messages
|
||||
/// will be appended to the given error list and None is returned. If
|
||||
/// a Some return value is returned, there are never any errors added.
|
||||
@@ -363,6 +443,68 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basename_windows_matches_zig_tests() {
|
||||
// The `testBasenameWindows` cases of `std/fs/path.zig`.
|
||||
for (input, expected) in [
|
||||
("\\dir\\basename.ext", "basename.ext"),
|
||||
("\\basename.ext", "basename.ext"),
|
||||
("basename.ext", "basename.ext"),
|
||||
("basename.ext\\", "basename.ext"),
|
||||
("basename.ext\\\\", "basename.ext"),
|
||||
("foo", "foo"),
|
||||
("C:", ""),
|
||||
("C:.", "."),
|
||||
("C:\\", ""),
|
||||
("C:\\dir\\base.ext", "base.ext"),
|
||||
("C:\\basename.ext", "basename.ext"),
|
||||
("C:basename.ext", "basename.ext"),
|
||||
("C:basename.ext\\", "basename.ext"),
|
||||
("C:basename.ext\\\\", "basename.ext"),
|
||||
("\\\\.", ""),
|
||||
("\\\\.\\", ""),
|
||||
("\\\\.\\basename.ext", "basename.ext"),
|
||||
("\\\\?", ""),
|
||||
("\\\\?\\", ""),
|
||||
("\\\\?\\basename.ext", "basename.ext"),
|
||||
("C:foo", "foo"),
|
||||
("file:stream", "file:stream"),
|
||||
// Non-letter drive designators are drive roots too (the
|
||||
// RV13 finding): `1:theme` is `theme`, not the whole name.
|
||||
("1:theme", "theme"),
|
||||
("1:", ""),
|
||||
("\u{3bb}:theme", "theme"),
|
||||
("\u{20ac}:\\theme", "theme"),
|
||||
// UNC roots.
|
||||
("//a", ""),
|
||||
("//a/b", ""),
|
||||
("//a/b/c", "c"),
|
||||
("\\\\server\\\\share\\file", "file"),
|
||||
("//", ""),
|
||||
] {
|
||||
assert_eq!(basename_windows(input), expected, "{input:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basename_posix_matches_zig_tests() {
|
||||
for (input, expected) in [
|
||||
("", ""),
|
||||
("/", ""),
|
||||
("/dir/basename.ext", "basename.ext"),
|
||||
("/basename.ext", "basename.ext"),
|
||||
("basename.ext/", "basename.ext"),
|
||||
("basename.ext//", "basename.ext"),
|
||||
("/aaa/", "aaa"),
|
||||
("//a", "a"),
|
||||
("\\dir\\basename.ext", "\\dir\\basename.ext"),
|
||||
("basename.ext\\\\", "basename.ext\\\\"),
|
||||
("1:theme", "1:theme"),
|
||||
] {
|
||||
assert_eq!(basename_posix(input), expected, "{input:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zig_basename_matches_std() {
|
||||
assert_eq!(zig_basename(""), "");
|
||||
|
||||
Reference in new issue
Block a user