refactor: type escaped OS strings and fix the final audit findings
Escaped config strings (ghostty_os::osbytes) kept reaching OS boundaries undecoded, and decoding was occasionally applied to plain text. The root cause was that both were plain `String`s. Now: - `EncodedOs` is a newtype (no Display/AsRef<Path>) that decodes only via to_os/to_path/to_bytes/to_wide; plain text enters via from_plain_text. OS helpers (env, home, xdg, passwd, resources dir, path expansion), config::Path, WorkingDirectory, theme/file_load/edit paths, terminal pwd, OpenUrl/Pwd actions and termio cwd use it, so the compiler forces decoding at every boundary. os::open takes &OsStr. Config output is unchanged. Fixes all rows of docs/rewrite/notes/review-rv5-codex.md (final-Z7.md): screen-file copy; regex/OSC 8 URLs no longer decoded (regression); lossless Windows WTF-8/WTF-16 for args, env and CreateProcessW; Windows resources dir/TERMINFO; color OSC request lists and X11 color lookup no longer allocate globally (C zero-allocation test extended); macOS Mach handler resumes all threads before KERN_FAILURE (hang regression) and SIGABRT minidumps go through the exception thread; plus open-config, background images, crash dir, ssh cache, exe dir/PATH, fontconfig filenames, file links, kitty TMPDIR, .hushlogin and benchmark paths. The typing also surfaced and fixed gtk-custom-css, bell audio, bash HISTFILE, OSC 52 writes and the C config getters. Test Plan: - cargo test --workspace (3376 passed) - just check-macos (both), just check-windows, just check-wasm - diff <(ghostty-rs +show-config --default --docs) \ <(ghostty +show-config --default --docs)
This commit is contained in:
1 parent
7a3f0b0f2c
commit
f7fec6f44d
92 files changed
+2967
-992
No files matched your search
@@ -177,7 +177,7 @@ pub struct Command {
|
||||
/// 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>,
|
||||
pub cwd: Option<ghostty_os::osbytes::EncodedOs>,
|
||||
|
||||
/// 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.
|
||||
@@ -317,8 +317,8 @@ impl Command {
|
||||
let prep = self.prepare()?;
|
||||
let cwd = self
|
||||
.cwd
|
||||
.as_deref()
|
||||
.and_then(|c| CString::new(config_str_to_bytes(c)).ok());
|
||||
.as_ref()
|
||||
.and_then(|c| CString::new(c.to_bytes()).ok());
|
||||
|
||||
let mut argv_ptrs: Vec<*const c_char> = prep.argv.iter().map(|c| c.as_ptr()).collect();
|
||||
argv_ptrs.push(std::ptr::null());
|
||||
@@ -872,7 +872,7 @@ mod tests {
|
||||
|
||||
let mut cmd = sh("pwd");
|
||||
cmd.stdout = Some(stdout.try_clone().unwrap().into());
|
||||
cmd.cwd = Some("/tmp".to_owned());
|
||||
cmd.cwd = Some(ghostty_os::osbytes::EncodedOs::from_encoded("/tmp"));
|
||||
cmd.start().unwrap();
|
||||
assert_eq!(cmd.wait(true).unwrap(), Exit::Exited(0));
|
||||
|
||||
@@ -894,7 +894,7 @@ mod tests {
|
||||
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.cwd = Some(ghostty_os::osbytes::EncodedOs::from_bytes(&raw));
|
||||
cmd.start().unwrap();
|
||||
assert_eq!(cmd.wait(true).unwrap(), Exit::Exited(0));
|
||||
|
||||
@@ -915,7 +915,9 @@ mod tests {
|
||||
|
||||
let mut cmd = sh("echo ok");
|
||||
cmd.stdout = Some(stdout.try_clone().unwrap().into());
|
||||
cmd.cwd = Some("/definitely/not/a/dir".to_owned());
|
||||
cmd.cwd = Some(ghostty_os::osbytes::EncodedOs::from_encoded(
|
||||
"/definitely/not/a/dir",
|
||||
));
|
||||
cmd.start().unwrap();
|
||||
assert_eq!(cmd.wait(true).unwrap(), Exit::Exited(0));
|
||||
assert_eq!("ok\n", read_all(stdout));
|
||||
@@ -933,7 +935,7 @@ mod tests {
|
||||
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.cwd = Some(ghostty_os::osbytes::EncodedOs::from_encoded("/bin"));
|
||||
cmd.start().unwrap();
|
||||
assert!(cmd.pid.is_some());
|
||||
assert_eq!(cmd.wait(true).unwrap(), Exit::Exited(1));
|
||||
|
||||
@@ -16,11 +16,13 @@ use std::{
|
||||
ptr,
|
||||
};
|
||||
|
||||
use ghostty_os::windows::{
|
||||
CREATE_UNICODE_ENVIRONMENT,
|
||||
EXTENDED_STARTUPINFO_PRESENT,
|
||||
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
|
||||
to_wide_nul,
|
||||
use ghostty_os::{
|
||||
osbytes::EncodedOs,
|
||||
windows::{
|
||||
CREATE_UNICODE_ENVIRONMENT,
|
||||
EXTENDED_STARTUPINFO_PRESENT,
|
||||
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
|
||||
},
|
||||
};
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{HANDLE, TRUE, WAIT_FAILED},
|
||||
@@ -43,7 +45,7 @@ use windows_sys::Win32::{
|
||||
},
|
||||
};
|
||||
|
||||
use crate::windows_args::{self, create_windows_env_block, windows_create_command_line};
|
||||
use crate::windows_args::{self, create_windows_env_block, windows_create_command_line_wide};
|
||||
|
||||
/// Errors from [`Command::start`].
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@@ -77,7 +79,7 @@ pub struct Command {
|
||||
|
||||
/// Working directory for the child. If not set, the working directory of
|
||||
/// the calling process is preserved.
|
||||
pub cwd: Option<String>,
|
||||
pub cwd: Option<ghostty_os::osbytes::EncodedOs>,
|
||||
|
||||
/// The file handles to set for stdin/out/err. If not set the NUL device
|
||||
/// is used. Ignored if `pseudo_console` is set.
|
||||
@@ -127,12 +129,21 @@ impl Command {
|
||||
/// available.
|
||||
#[allow(unsafe_code)]
|
||||
pub fn start(&mut self) -> Result<(), StartError> {
|
||||
let application_w = to_wide_nul(&self.path);
|
||||
let cwd_w = self.cwd.as_deref().map(to_wide_nul);
|
||||
// `path` and `args` are the config command's argv strings (encoded
|
||||
// OS strings); decode them to WTF-16 for the Windows API.
|
||||
let application_w = EncodedOs::from_encoded(&self.path).to_wide_nul();
|
||||
let cwd_w = self.cwd.as_ref().map(EncodedOs::to_wide_nul);
|
||||
let mut command_line_w = if self.args.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(to_wide_nul(&windows_create_command_line(&self.args)))
|
||||
let wide: Vec<Vec<u16>> = self
|
||||
.args
|
||||
.iter()
|
||||
.map(|a| EncodedOs::from_encoded(a).to_wide())
|
||||
.collect();
|
||||
let mut line = windows_create_command_line_wide(&wide);
|
||||
line.push(0);
|
||||
Some(line)
|
||||
};
|
||||
let mut env_w = match &self.env {
|
||||
Some(map) => Some(create_windows_env_block(map.iter())?),
|
||||
|
||||
@@ -32,7 +32,7 @@ use ghostty_config::{
|
||||
types_b::{ShellIntegration, ShellIntegrationFeatures},
|
||||
};
|
||||
use ghostty_datastruct::blocking_queue::Timeout;
|
||||
use ghostty_os::file::Rlimit;
|
||||
use ghostty_os::{file::Rlimit, osbytes::EncodedOs};
|
||||
use ghostty_renderer::size::{GridSize, ScreenSize};
|
||||
use ghostty_terminal::terminal::Terminal;
|
||||
use rustix::{
|
||||
@@ -107,8 +107,8 @@ pub struct Config {
|
||||
pub shell_integration: ShellIntegration,
|
||||
pub shell_integration_features: ShellIntegrationFeatures,
|
||||
pub cursor_blink: Option<bool>,
|
||||
pub working_directory: Option<String>,
|
||||
pub resources_dir: Option<String>,
|
||||
pub working_directory: Option<EncodedOs>,
|
||||
pub resources_dir: Option<EncodedOs>,
|
||||
pub term: String,
|
||||
|
||||
pub rt_pre_exec_info: RtPreExecInfo,
|
||||
@@ -158,8 +158,8 @@ impl Exec {
|
||||
}
|
||||
|
||||
/// The working directory the subprocess will be started in, if any.
|
||||
pub fn cwd(&self) -> Option<&str> {
|
||||
self.subprocess.cwd.as_deref()
|
||||
pub fn cwd(&self) -> Option<&EncodedOs> {
|
||||
self.subprocess.cwd.as_ref()
|
||||
}
|
||||
|
||||
/// The argv that will be executed.
|
||||
@@ -181,7 +181,7 @@ impl Exec {
|
||||
// set that on the terminal now. This allows rapidly initializing
|
||||
// new surfaces to use the proper pwd.
|
||||
if let Some(cwd) = &self.subprocess.cwd
|
||||
&& let Err(err) = term.set_pwd(&ghostty_os::osbytes::config_str_to_bytes(cwd))
|
||||
&& let Err(err) = term.set_pwd(&cwd.to_bytes())
|
||||
{
|
||||
log::warn!("error setting initial pwd err={err:?}");
|
||||
}
|
||||
@@ -766,7 +766,7 @@ struct FlatpakProcess {
|
||||
}
|
||||
|
||||
struct Subprocess {
|
||||
cwd: Option<String>,
|
||||
cwd: Option<EncodedOs>,
|
||||
env: Option<EnvMap>,
|
||||
args: Vec<String>,
|
||||
grid_size: GridSize,
|
||||
@@ -790,11 +790,8 @@ impl Subprocess {
|
||||
|
||||
// If we have a resources dir then set our env var
|
||||
if let Some(dir) = &cfg.resources_dir {
|
||||
log::info!("found Ghostty resources dir: {dir}");
|
||||
env.insert(
|
||||
"GHOSTTY_RESOURCES_DIR",
|
||||
ghostty_os::osbytes::config_str_to_os(dir),
|
||||
);
|
||||
log::info!("found Ghostty resources dir: {}", dir.as_encoded());
|
||||
env.insert("GHOSTTY_RESOURCES_DIR", dir.to_os());
|
||||
}
|
||||
|
||||
// Set our TERM var. This is a bit complicated because we want to use
|
||||
@@ -923,7 +920,7 @@ impl Subprocess {
|
||||
// https://github.com/kovidgoyal/kitty/issues/1595
|
||||
// https://github.com/ghostty-org/ghostty/discussions/7769
|
||||
if let Some(pwd) = &cwd {
|
||||
env.insert("PWD", ghostty_os::osbytes::config_str_to_os(pwd));
|
||||
env.insert("PWD", pwd.to_os());
|
||||
}
|
||||
|
||||
Ok(Subprocess {
|
||||
@@ -1002,7 +999,7 @@ impl Subprocess {
|
||||
// If we can't access the cwd, then don't set any cwd and inherit.
|
||||
// This is important because our cwd can be set by the shell (OSC 7)
|
||||
// and we don't want to break new windows.
|
||||
let cwd: Option<String> = match &self.cwd {
|
||||
let cwd: Option<EncodedOs> = match &self.cwd {
|
||||
None => None,
|
||||
Some(proposed) if flatpak => {
|
||||
// Flatpak sandboxing prevents access to certain reserved paths
|
||||
@@ -1035,10 +1032,10 @@ impl Subprocess {
|
||||
}
|
||||
}
|
||||
Some(proposed) => {
|
||||
if std::path::Path::new(&ghostty_os::osbytes::config_str_to_os(proposed)).exists() {
|
||||
if proposed.to_path().exists() {
|
||||
Some(proposed.clone())
|
||||
} else {
|
||||
log::warn!("cannot access cwd, ignoring: {proposed}");
|
||||
log::warn!("cannot access cwd, ignoring: {}", proposed.as_encoded());
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1188,10 +1185,17 @@ fn add_ghostty_path(env: &mut EnvMap) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Some(exe_dir) = exe.parent().map(|p| p.to_string_lossy().into_owned()) else {
|
||||
add_ghostty_path_for(env, &exe);
|
||||
}
|
||||
|
||||
/// [`add_ghostty_path`] for a given executable (testable with non-UTF-8
|
||||
/// directories).
|
||||
fn add_ghostty_path_for(env: &mut EnvMap, exe: &std::path::Path) {
|
||||
// Keep the exact bytes of the directory (it may be non-UTF-8).
|
||||
let Some(exe_dir) = exe.parent().map(|p| p.as_os_str().to_owned()) else {
|
||||
return;
|
||||
};
|
||||
log::debug!("appending ghostty bin to path dir={exe_dir}");
|
||||
log::debug!("appending ghostty bin to path dir={}", exe_dir.display());
|
||||
|
||||
// We always set this so that if the shell overwrites the path
|
||||
// scripts still have a way to find the Ghostty binary when
|
||||
@@ -1211,10 +1215,7 @@ fn add_ghostty_path(env: &mut EnvMap) {
|
||||
{
|
||||
return;
|
||||
}
|
||||
env.insert(
|
||||
"PATH",
|
||||
ghostty_os::env::append_env_os(&path, std::ffi::OsStr::new(&exe_dir)),
|
||||
);
|
||||
env.insert("PATH", ghostty_os::env::append_env_os(&path, &exe_dir));
|
||||
} else {
|
||||
env.insert("PATH", exe_dir);
|
||||
}
|
||||
@@ -1222,8 +1223,8 @@ fn add_ghostty_path(env: &mut EnvMap) {
|
||||
|
||||
/// The `TERMINFO` directory for a resources dir (reversible config-string
|
||||
/// encoding in, raw OS string out).
|
||||
fn terminfo_dir(base: &str) -> std::ffi::OsString {
|
||||
let base = ghostty_os::osbytes::config_str_to_os(base);
|
||||
fn terminfo_dir(base: &EncodedOs) -> std::ffi::OsString {
|
||||
let base = base.to_os();
|
||||
let own = std::path::Path::new(&base).join("terminfo");
|
||||
if own.join("x/xterm-ghostty").exists() {
|
||||
own.into_os_string()
|
||||
@@ -1238,9 +1239,9 @@ fn terminfo_dir(base: &str) -> std::ffi::OsString {
|
||||
|
||||
/// macOS: export additional data directories from our application bundle
|
||||
/// (`XDG_DATA_DIRS` and `MANPATH`).
|
||||
pub fn darwin_bundle_env(env: &mut EnvMap, resources_dir: &str) {
|
||||
pub fn darwin_bundle_env(env: &mut EnvMap, resources_dir: &EncodedOs) {
|
||||
let xdg_data_dir_key = "XDG_DATA_DIRS";
|
||||
let mut data_dir = ghostty_os::osbytes::config_str_to_os(resources_dir);
|
||||
let mut data_dir = resources_dir.to_os();
|
||||
data_dir.push("/..");
|
||||
let current = env.get(xdg_data_dir_key).map_or_else(
|
||||
|| std::ffi::OsString::from("/usr/local/share:/usr/share"),
|
||||
@@ -1252,7 +1253,7 @@ pub fn darwin_bundle_env(env: &mut EnvMap, resources_dir: &str) {
|
||||
);
|
||||
|
||||
let manpath_key = "MANPATH";
|
||||
let mut man_dir = ghostty_os::osbytes::config_str_to_os(resources_dir);
|
||||
let mut man_dir = resources_dir.to_os();
|
||||
man_dir.push("/../man");
|
||||
// Always append with colon in front, as it mean that if
|
||||
// `MANPATH` is empty, then it should be treated as an extra
|
||||
@@ -1439,11 +1440,13 @@ fn darwin_login_command(
|
||||
|
||||
let hush = match &passwd.home {
|
||||
Some(home) => {
|
||||
if std::fs::read_dir(home).is_err() {
|
||||
// The passwd home is an encoded OS string: decode it first.
|
||||
let home = home.to_path();
|
||||
if std::fs::read_dir(&home).is_err() {
|
||||
log::warn!("failed to open home dir, not checking for hushlogin");
|
||||
false
|
||||
} else {
|
||||
std::path::Path::new(home).join(".hushlogin").exists()
|
||||
home.join(".hushlogin").exists()
|
||||
}
|
||||
}
|
||||
None => false,
|
||||
@@ -1507,7 +1510,7 @@ fn darwin_login_command(
|
||||
args.push("-q".into());
|
||||
}
|
||||
args.push("-flp".into());
|
||||
args.push(username);
|
||||
args.push(username.into_encoded());
|
||||
|
||||
match command {
|
||||
// Direct args can be passed directly to login, since
|
||||
@@ -1552,11 +1555,64 @@ impl Drop for ThreadData {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ghostty_path_keeps_non_utf8_exe_dir_bytes() {
|
||||
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
||||
let exe = std::path::PathBuf::from(std::ffi::OsString::from_vec(
|
||||
b"/opt/gh\xffostty/bin/ghostty".to_vec(),
|
||||
));
|
||||
|
||||
// No PATH: PATH becomes the directory itself.
|
||||
let mut env = EnvMap::new();
|
||||
add_ghostty_path_for(&mut env, &exe);
|
||||
assert_eq!(
|
||||
env.get("GHOSTTY_BIN_DIR").unwrap().as_bytes(),
|
||||
b"/opt/gh\xffostty/bin"
|
||||
);
|
||||
assert_eq!(env.get("PATH").unwrap().as_bytes(), b"/opt/gh\xffostty/bin");
|
||||
|
||||
// Existing PATH: appended byte for byte (no U+FFFD), and not twice.
|
||||
let mut env = EnvMap::new();
|
||||
env.insert("PATH", "/usr/bin");
|
||||
add_ghostty_path_for(&mut env, &exe);
|
||||
assert_eq!(
|
||||
env.get("PATH").unwrap().as_bytes(),
|
||||
b"/usr/bin:/opt/gh\xffostty/bin"
|
||||
);
|
||||
add_ghostty_path_for(&mut env, &exe);
|
||||
assert_eq!(
|
||||
env.get("PATH").unwrap().as_bytes(),
|
||||
b"/usr/bin:/opt/gh\xffostty/bin"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_command_darwin_hushlogin_with_non_utf8_home() {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
let tmp = ghostty_os::TempDir::init().unwrap();
|
||||
let home = tmp.path().join(std::ffi::OsStr::from_bytes(b"h\xffome"));
|
||||
std::fs::create_dir_all(&home).unwrap();
|
||||
std::fs::write(home.join(".hushlogin"), "").unwrap();
|
||||
// The passwd entry carries the *encoded* home; it must be decoded
|
||||
// before the lookup.
|
||||
let enc = EncodedOs::from_path(&home);
|
||||
assert_ne!(enc.as_encoded().as_bytes(), home.as_os_str().as_bytes());
|
||||
let passwd = move || {
|
||||
Ok(ghostty_os::passwd::Entry {
|
||||
name: Some(EncodedOs::from_encoded("u")),
|
||||
home: Some(enc.clone()),
|
||||
..Default::default()
|
||||
})
|
||||
};
|
||||
let result = exec_command_with(&ConfigCommand::Direct(vec!["x".into()]), true, &passwd);
|
||||
assert_eq!(result, ["/usr/bin/login", "-q", "-flp", "u", "x"]);
|
||||
}
|
||||
|
||||
fn pw(name: Option<&str>) -> impl Fn() -> std::io::Result<ghostty_os::passwd::Entry> {
|
||||
let name = name.map(str::to_owned);
|
||||
move || {
|
||||
Ok(ghostty_os::passwd::Entry {
|
||||
name: name.clone(),
|
||||
name: name.clone().map(EncodedOs::from_encoded),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@@ -1602,10 +1658,10 @@ mod tests {
|
||||
let dir = std::env::temp_dir().join(format!("ghostty-hush-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join(".hushlogin"), "").unwrap();
|
||||
let home = dir.to_str().unwrap().to_owned();
|
||||
let home = EncodedOs::from_path(&dir);
|
||||
let passwd = move || {
|
||||
Ok(ghostty_os::passwd::Entry {
|
||||
name: Some("u".into()),
|
||||
name: Some(EncodedOs::from_encoded("u")),
|
||||
home: Some(home.clone()),
|
||||
..Default::default()
|
||||
})
|
||||
|
||||
@@ -35,6 +35,7 @@ use ghostty_config::{
|
||||
types_b::{ShellIntegration, ShellIntegrationFeatures},
|
||||
};
|
||||
use ghostty_datastruct::blocking_queue::Timeout;
|
||||
use ghostty_os::osbytes::EncodedOs;
|
||||
use ghostty_renderer::size::{GridSize, ScreenSize};
|
||||
use ghostty_terminal::terminal::Terminal;
|
||||
use windows_sys::Win32::{
|
||||
@@ -99,8 +100,8 @@ pub struct Config {
|
||||
pub shell_integration: ShellIntegration,
|
||||
pub shell_integration_features: ShellIntegrationFeatures,
|
||||
pub cursor_blink: Option<bool>,
|
||||
pub working_directory: Option<String>,
|
||||
pub resources_dir: Option<String>,
|
||||
pub working_directory: Option<EncodedOs>,
|
||||
pub resources_dir: Option<EncodedOs>,
|
||||
pub term: String,
|
||||
}
|
||||
|
||||
@@ -136,8 +137,8 @@ impl Exec {
|
||||
}
|
||||
|
||||
/// The working directory the subprocess will be started in, if any.
|
||||
pub fn cwd(&self) -> Option<&str> {
|
||||
self.subprocess.cwd.as_deref()
|
||||
pub fn cwd(&self) -> Option<&EncodedOs> {
|
||||
self.subprocess.cwd.as_ref()
|
||||
}
|
||||
|
||||
/// The argv that will be executed.
|
||||
@@ -157,7 +158,7 @@ impl Exec {
|
||||
// set that on the terminal now. This allows rapidly initializing
|
||||
// new surfaces to use the proper pwd.
|
||||
if let Some(cwd) = &self.subprocess.cwd
|
||||
&& let Err(err) = term.set_pwd(cwd.as_bytes())
|
||||
&& let Err(err) = term.set_pwd(&cwd.to_bytes())
|
||||
{
|
||||
log::warn!("error setting initial pwd err={err:?}");
|
||||
}
|
||||
@@ -583,7 +584,7 @@ fn write_thread_main(handle: &OwnedHandle, rx: &Receiver<Vec<u8>>) {
|
||||
// Subprocess
|
||||
|
||||
struct Subprocess {
|
||||
cwd: Option<String>,
|
||||
cwd: Option<EncodedOs>,
|
||||
env: Option<EnvMap>,
|
||||
args: Vec<String>,
|
||||
grid_size: GridSize,
|
||||
@@ -602,8 +603,10 @@ impl Subprocess {
|
||||
|
||||
// If we have a resources dir then set our env var
|
||||
if let Some(dir) = &cfg.resources_dir {
|
||||
log::info!("found Ghostty resources dir: {dir}");
|
||||
env.insert("GHOSTTY_RESOURCES_DIR", dir.clone());
|
||||
log::info!("found Ghostty resources dir: {}", dir.as_encoded());
|
||||
// The resources dir is an encoded OS string: decode it for the
|
||||
// child's environment.
|
||||
env.insert("GHOSTTY_RESOURCES_DIR", dir.to_os());
|
||||
}
|
||||
|
||||
// Set our TERM var. This is a bit complicated because we want to use
|
||||
@@ -615,10 +618,7 @@ impl Subprocess {
|
||||
|
||||
// Assume that the resources directory is adjacent to the terminfo
|
||||
// database
|
||||
let parent = std::path::Path::new(base)
|
||||
.parent()
|
||||
.map_or_else(String::new, |p| p.to_string_lossy().into_owned());
|
||||
env.insert("TERMINFO", format!("{parent}\\terminfo"));
|
||||
env.insert("TERMINFO", terminfo_dir(base));
|
||||
} else {
|
||||
env.insert("TERM", "xterm-256color");
|
||||
env.insert("COLORTERM", "truecolor");
|
||||
@@ -693,7 +693,10 @@ impl Subprocess {
|
||||
|
||||
// Build our args list. If we fail to find the shell, we'd still like
|
||||
// to try to run something so Ghostty can launch.
|
||||
let args = match exec_command(&shell_command, std::env::var("WINDIR").ok().as_deref()) {
|
||||
// %WINDIR% may hold lone surrogates: carry it in the encoded form the
|
||||
// argv strings use (decoded to WTF-16 when the process is created).
|
||||
let windir = std::env::var_os("WINDIR").map(|v| EncodedOs::from_os(&v).into_encoded());
|
||||
let args = match exec_command(&shell_command, windir.as_deref()) {
|
||||
Ok(args) => args,
|
||||
Err(Error::SystemError) => {
|
||||
log::warn!("failed to get WINDIR, cannot run shell command");
|
||||
@@ -708,7 +711,7 @@ impl Subprocess {
|
||||
|
||||
// Propagate the current working directory (CWD) to the shell.
|
||||
if let Some(pwd) = &cwd {
|
||||
env.insert("PWD", pwd.clone());
|
||||
env.insert("PWD", pwd.to_os());
|
||||
}
|
||||
|
||||
Ok(Subprocess {
|
||||
@@ -777,9 +780,9 @@ impl Subprocess {
|
||||
// If we can't access the cwd, then don't set any cwd and inherit.
|
||||
// This is important because our cwd can be set by the shell (OSC 7)
|
||||
// and we don't want to break new windows.
|
||||
let cwd: Option<String> = match &self.cwd {
|
||||
let cwd: Option<EncodedOs> = match &self.cwd {
|
||||
None => None,
|
||||
Some(proposed) => match std::fs::metadata(proposed) {
|
||||
Some(proposed) => match std::fs::metadata(proposed.to_path()) {
|
||||
Ok(_) => Some(proposed.clone()),
|
||||
Err(err) => {
|
||||
log::warn!("cannot access cwd, ignoring: {err}");
|
||||
@@ -852,10 +855,17 @@ fn add_ghostty_path(env: &mut EnvMap) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Some(exe_dir) = exe.parent().map(|p| p.to_string_lossy().into_owned()) else {
|
||||
add_ghostty_path_for(env, &exe);
|
||||
}
|
||||
|
||||
/// [`add_ghostty_path`] for a given executable (testable with non-UTF-8
|
||||
/// directories).
|
||||
fn add_ghostty_path_for(env: &mut EnvMap, exe: &std::path::Path) {
|
||||
// Keep the exact OS string (it may hold lone UTF-16 surrogates).
|
||||
let Some(exe_dir) = exe.parent().map(|p| p.as_os_str().to_owned()) else {
|
||||
return;
|
||||
};
|
||||
log::debug!("appending ghostty bin to path dir={exe_dir}");
|
||||
log::debug!("appending ghostty bin to path dir={}", exe_dir.display());
|
||||
|
||||
// We always set this so that if the shell overwrites the path
|
||||
// scripts still have a way to find the Ghostty binary when
|
||||
@@ -868,19 +878,27 @@ fn add_ghostty_path(env: &mut EnvMap) {
|
||||
if let Some(path) = env.get("PATH").map(std::ffi::OsStr::to_owned) {
|
||||
// Verify that our path doesn't already contain this entry
|
||||
if std::env::split_paths(&path)
|
||||
.any(|entry| !entry.as_os_str().is_empty() && entry.as_os_str() == exe_dir.as_str())
|
||||
.any(|entry| !entry.as_os_str().is_empty() && entry.as_os_str() == exe_dir.as_os_str())
|
||||
{
|
||||
return;
|
||||
}
|
||||
env.insert(
|
||||
"PATH",
|
||||
ghostty_os::env::append_env_os(&path, std::ffi::OsStr::new(&exe_dir)),
|
||||
);
|
||||
env.insert("PATH", ghostty_os::env::append_env_os(&path, &exe_dir));
|
||||
} else {
|
||||
env.insert("PATH", exe_dir);
|
||||
}
|
||||
}
|
||||
|
||||
/// The `TERMINFO` value for a resources dir: `<parent of base>\\terminfo` as
|
||||
/// an OS string (the resources dir is an encoded OS string).
|
||||
fn terminfo_dir(base: &EncodedOs) -> std::ffi::OsString {
|
||||
let base = base.to_path();
|
||||
let mut out = base
|
||||
.parent()
|
||||
.map_or_else(std::ffi::OsString::new, |p| p.as_os_str().to_owned());
|
||||
out.push("\\terminfo");
|
||||
out
|
||||
}
|
||||
|
||||
/// Terminate the underlying subprocess and wait for it to exit
|
||||
/// (`killCommand`: `TerminateProcess(pid, 0)` then wait).
|
||||
fn kill_command(command: &Command) -> Result<(), Error> {
|
||||
@@ -908,6 +926,23 @@ pub fn exec_command(command: &ConfigCommand, windir: Option<&str>) -> Result<Vec
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn terminfo_dir_is_sibling_of_resources_and_lossless() {
|
||||
use std::os::windows::ffi::{OsStrExt, OsStringExt};
|
||||
|
||||
// C:\Users\<lone surrogate>\share\ghostty -> ...\share\terminfo
|
||||
let mut wide: Vec<u16> = "C:\\Users\\".encode_utf16().collect();
|
||||
wide.push(0xD800);
|
||||
wide.extend("\\share\\ghostty".encode_utf16());
|
||||
let base = EncodedOs::from_wide(&wide);
|
||||
let got = super::terminfo_dir(&base);
|
||||
let mut want: Vec<u16> = "C:\\Users\\".encode_utf16().collect();
|
||||
want.push(0xD800);
|
||||
want.extend("\\share\\terminfo".encode_utf16());
|
||||
assert_eq!(got.encode_wide().collect::<Vec<_>>(), want);
|
||||
assert_eq!(std::ffi::OsString::from_wide(&want), got);
|
||||
}
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use ghostty_os::{env::prepend_env_os, homedir, shell::ShellCommandBuilder};
|
||||
use ghostty_os::{env::prepend_env_os, homedir, osbytes::EncodedOs, shell::ShellCommandBuilder};
|
||||
|
||||
/// The environment passed to the child (Zig `std.process.EnvMap`).
|
||||
pub type EnvMap = ghostty_os::EnvMap;
|
||||
@@ -65,8 +65,8 @@ pub struct ShellIntegration {
|
||||
/// `<resource_dir>/<rel>` as an OS string. `resource_dir` carries the
|
||||
/// reversible config-string encoding (it may hold non-UTF-8 bytes), so it is
|
||||
/// decoded before reaching the filesystem or the child environment.
|
||||
fn resource_path(resource_dir: &str, rel: &str) -> std::ffi::OsString {
|
||||
let mut p = ghostty_os::osbytes::config_str_to_os(resource_dir);
|
||||
fn resource_path(resource_dir: &EncodedOs, rel: &str) -> std::ffi::OsString {
|
||||
let mut p = resource_dir.to_os();
|
||||
p.push("/");
|
||||
p.push(rel);
|
||||
p
|
||||
@@ -77,7 +77,7 @@ fn resource_path(resource_dir: &str, rel: &str) -> std::ffi::OsString {
|
||||
/// struct describing the integration. If integration fails
|
||||
/// (shell type couldn't be detected, etc.), this will return None.
|
||||
pub fn setup(
|
||||
resource_dir: &str,
|
||||
resource_dir: &EncodedOs,
|
||||
command: &Command,
|
||||
env: &mut EnvMap,
|
||||
force_shell: Option<Shell>,
|
||||
@@ -185,7 +185,7 @@ pub fn setup_features(env: &mut EnvMap, features: ShellIntegrationFeatures, curs
|
||||
/// or None if integration failed.
|
||||
fn setup_bash(
|
||||
command: &Command,
|
||||
resource_dir: &str,
|
||||
resource_dir: &EncodedOs,
|
||||
env: &mut EnvMap,
|
||||
) -> Result<Option<Command>, Error> {
|
||||
let mut cmd = ShellCommandBuilder::new();
|
||||
@@ -271,7 +271,10 @@ fn setup_bash(
|
||||
if !env.contains_key("HISTFILE")
|
||||
&& let Some(home) = homedir::home()?
|
||||
{
|
||||
env.insert("HISTFILE", format!("{home}/.bash_history"));
|
||||
// `home` is an encoded OS string: build the real OS value.
|
||||
let mut histfile = home.to_os();
|
||||
histfile.push("/.bash_history");
|
||||
env.insert("HISTFILE", histfile);
|
||||
env.insert("GHOSTTY_BASH_UNEXPORT_HISTFILE", "1");
|
||||
}
|
||||
|
||||
@@ -286,7 +289,7 @@ fn setup_bash(
|
||||
/// 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 {
|
||||
fn setup_xdg_data_dirs(resource_dir: &EncodedOs, env: &mut EnvMap) -> bool {
|
||||
// Get our path to the shell integration directory.
|
||||
let integ_path = resource_path(resource_dir, "shell-integration");
|
||||
if let Err(err) = std::fs::read_dir(&integ_path) {
|
||||
@@ -319,7 +322,7 @@ fn setup_xdg_data_dirs(resource_dir: &str, env: &mut EnvMap) -> bool {
|
||||
///
|
||||
/// 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> {
|
||||
fn setup_nushell(command: &Command, resource_dir: &EncodedOs, 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.
|
||||
@@ -377,7 +380,7 @@ fn setup_nushell(command: &Command, resource_dir: &str, env: &mut EnvMap) -> Opt
|
||||
/// 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> {
|
||||
fn setup_zsh(command: &Command, resource_dir: &EncodedOs, env: &mut EnvMap) -> Option<Command> {
|
||||
// Preserve an existing ZDOTDIR value. We're about to overwrite it.
|
||||
if let Some(old) = env.get("ZDOTDIR").map(std::ffi::OsStr::to_owned) {
|
||||
env.insert("GHOSTTY_ZSH_ZDOTDIR", old);
|
||||
@@ -419,6 +422,10 @@ mod tests {
|
||||
}
|
||||
|
||||
impl TmpResourcesDir {
|
||||
fn enc(&self) -> EncodedOs {
|
||||
EncodedOs::from_plain_text(&self.path)
|
||||
}
|
||||
|
||||
fn new(shell: Shell) -> Self {
|
||||
let tmp = TempDir::init().unwrap();
|
||||
let path = std::fs::canonicalize(tmp.path())
|
||||
@@ -455,7 +462,7 @@ mod tests {
|
||||
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();
|
||||
let result = setup(&res.enc(), &sh("sh"), &mut env, Some(shell)).unwrap();
|
||||
assert_eq!(shell, result.unwrap().shell);
|
||||
}
|
||||
}
|
||||
@@ -463,7 +470,13 @@ mod tests {
|
||||
#[test]
|
||||
fn shell_integration_failure() {
|
||||
let mut env = EnvMap::new();
|
||||
let result = setup("/nonexistent", &sh("sh"), &mut env, None).unwrap();
|
||||
let result = setup(
|
||||
&EncodedOs::from_encoded("/nonexistent"),
|
||||
&sh("sh"),
|
||||
&mut env,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(result.is_none());
|
||||
assert_eq!(0, env.len());
|
||||
}
|
||||
@@ -585,7 +598,7 @@ mod tests {
|
||||
let res = TmpResourcesDir::new(Shell::Bash);
|
||||
let mut env = EnvMap::new();
|
||||
|
||||
let command = setup_bash(&sh("bash"), &res.path, &mut env).unwrap();
|
||||
let command = setup_bash(&sh("bash"), &res.enc(), &mut env).unwrap();
|
||||
assert_eq!("bash --posix", shell_str(command.as_ref()));
|
||||
assert_eq!(
|
||||
"1",
|
||||
@@ -609,7 +622,7 @@ mod tests {
|
||||
] {
|
||||
let mut env = EnvMap::new();
|
||||
assert!(
|
||||
setup_bash(&sh(cmdline), &res.path, &mut env)
|
||||
setup_bash(&sh(cmdline), &res.enc(), &mut env)
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
@@ -622,7 +635,7 @@ mod tests {
|
||||
let res = TmpResourcesDir::new(Shell::Bash);
|
||||
|
||||
let mut env = EnvMap::new();
|
||||
let command = setup_bash(&sh("bash --norc"), &res.path, &mut env).unwrap();
|
||||
let command = setup_bash(&sh("bash --norc"), &res.enc(), &mut env).unwrap();
|
||||
assert_eq!("bash --posix", shell_str(command.as_ref()));
|
||||
assert_eq!(
|
||||
"1 --norc",
|
||||
@@ -630,7 +643,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let mut env = EnvMap::new();
|
||||
let command = setup_bash(&sh("bash --noprofile"), &res.path, &mut env).unwrap();
|
||||
let command = setup_bash(&sh("bash --noprofile"), &res.enc(), &mut env).unwrap();
|
||||
assert_eq!("bash --posix", shell_str(command.as_ref()));
|
||||
assert_eq!(
|
||||
"1 --noprofile",
|
||||
@@ -643,14 +656,14 @@ mod tests {
|
||||
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();
|
||||
let command = setup_bash(&sh("bash --rcfile profile.sh"), &res.enc(), &mut env).unwrap();
|
||||
assert_eq!("bash --posix", shell_str(command.as_ref()));
|
||||
assert_eq!(
|
||||
"profile.sh",
|
||||
env.get("GHOSTTY_BASH_RCFILE").unwrap().to_str().unwrap()
|
||||
);
|
||||
|
||||
let command = setup_bash(&sh("bash --init-file profile.sh"), &res.path, &mut env).unwrap();
|
||||
let command = setup_bash(&sh("bash --init-file profile.sh"), &res.enc(), &mut env).unwrap();
|
||||
assert_eq!("bash --posix", shell_str(command.as_ref()));
|
||||
assert_eq!(
|
||||
"profile.sh",
|
||||
@@ -665,7 +678,7 @@ mod tests {
|
||||
// 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();
|
||||
setup_bash(&sh("bash"), &res.enc(), &mut env).unwrap();
|
||||
assert!(
|
||||
env.get("HISTFILE")
|
||||
.unwrap()
|
||||
@@ -685,7 +698,7 @@ mod tests {
|
||||
// HISTFILE set
|
||||
let mut env = EnvMap::new();
|
||||
env.insert("HISTFILE", "my_history");
|
||||
setup_bash(&sh("bash"), &res.path, &mut env).unwrap();
|
||||
setup_bash(&sh("bash"), &res.enc(), &mut env).unwrap();
|
||||
assert_eq!("my_history", env.get("HISTFILE").unwrap().to_str().unwrap());
|
||||
assert!(!env.contains_key("GHOSTTY_BASH_UNEXPORT_HISTFILE"));
|
||||
}
|
||||
@@ -696,7 +709,7 @@ mod tests {
|
||||
let mut env = EnvMap::new();
|
||||
env.insert("ENV", "env.sh");
|
||||
|
||||
setup_bash(&sh("bash"), &res.path, &mut env).unwrap();
|
||||
setup_bash(&sh("bash"), &res.enc(), &mut env).unwrap();
|
||||
assert_eq!(
|
||||
"env.sh",
|
||||
env.get("GHOSTTY_BASH_ENV").unwrap().to_str().unwrap()
|
||||
@@ -712,13 +725,13 @@ mod tests {
|
||||
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();
|
||||
let command = setup_bash(&sh("bash - --arg file1 file2"), &res.enc(), &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();
|
||||
let command = setup_bash(&sh("bash -- --arg file1 file2"), &res.enc(), &mut env).unwrap();
|
||||
assert_eq!(
|
||||
"bash --posix -- --arg file1 file2",
|
||||
shell_str(command.as_ref())
|
||||
@@ -731,7 +744,7 @@ mod tests {
|
||||
let dir = std::fs::canonicalize(tmp.path()).unwrap();
|
||||
let mut env = EnvMap::new();
|
||||
assert!(
|
||||
setup_bash(&sh("bash"), dir.to_str().unwrap(), &mut env)
|
||||
setup_bash(&sh("bash"), &EncodedOs::from_path(&dir), &mut env)
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
@@ -743,7 +756,7 @@ mod tests {
|
||||
let res = TmpResourcesDir::new(Shell::Fish);
|
||||
let mut env = EnvMap::new();
|
||||
|
||||
assert!(setup_xdg_data_dirs(&res.path, &mut env));
|
||||
assert!(setup_xdg_data_dirs(&res.enc(), &mut env));
|
||||
assert_eq!(
|
||||
format!("{}/shell-integration", res.path),
|
||||
env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR")
|
||||
@@ -763,7 +776,7 @@ mod tests {
|
||||
let mut env = EnvMap::new();
|
||||
env.insert("XDG_DATA_DIRS", "/opt/share");
|
||||
|
||||
assert!(setup_xdg_data_dirs(&res.path, &mut env));
|
||||
assert!(setup_xdg_data_dirs(&res.enc(), &mut env));
|
||||
assert_eq!(
|
||||
format!("{}/shell-integration", res.path),
|
||||
env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR")
|
||||
@@ -782,7 +795,7 @@ mod tests {
|
||||
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!(!setup_xdg_data_dirs(&EncodedOs::from_path(&dir), &mut env));
|
||||
assert_eq!(0, env.len());
|
||||
}
|
||||
|
||||
@@ -791,7 +804,7 @@ mod tests {
|
||||
let res = TmpResourcesDir::new(Shell::Nushell);
|
||||
let mut env = EnvMap::new();
|
||||
|
||||
let command = setup_nushell(&sh("nu"), &res.path, &mut env);
|
||||
let command = setup_nushell(&sh("nu"), &res.enc(), &mut env);
|
||||
assert_eq!("nu --execute 'use ghostty *'", shell_str(command.as_ref()));
|
||||
assert_eq!(
|
||||
format!("{}/shell-integration", res.path),
|
||||
@@ -819,7 +832,7 @@ mod tests {
|
||||
"nu -ic script.sh",
|
||||
] {
|
||||
let mut env = EnvMap::new();
|
||||
assert!(setup_nushell(&sh(cmdline), &res.path, &mut env).is_none());
|
||||
assert!(setup_nushell(&sh(cmdline), &res.enc(), &mut env).is_none());
|
||||
assert!(env.contains_key("XDG_DATA_DIRS"));
|
||||
assert!(env.contains_key("GHOSTTY_SHELL_INTEGRATION_XDG_DIR"));
|
||||
}
|
||||
@@ -830,7 +843,7 @@ mod tests {
|
||||
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!(setup_nushell(&sh("nu"), &EncodedOs::from_path(&dir), &mut env).is_none());
|
||||
assert_eq!(0, env.len());
|
||||
}
|
||||
|
||||
@@ -839,7 +852,7 @@ mod tests {
|
||||
let res = TmpResourcesDir::new(Shell::Zsh);
|
||||
let mut env = EnvMap::new();
|
||||
|
||||
let command = setup_zsh(&sh("zsh"), &res.path, &mut env);
|
||||
let command = setup_zsh(&sh("zsh"), &res.enc(), &mut env);
|
||||
assert_eq!("zsh", shell_str(command.as_ref()));
|
||||
assert_eq!(
|
||||
res.shell_path,
|
||||
@@ -854,7 +867,7 @@ mod tests {
|
||||
let mut env = EnvMap::new();
|
||||
env.insert("ZDOTDIR", "$HOME/.config/zsh");
|
||||
|
||||
let command = setup_zsh(&sh("zsh"), &res.path, &mut env);
|
||||
let command = setup_zsh(&sh("zsh"), &res.enc(), &mut env);
|
||||
assert_eq!("zsh", shell_str(command.as_ref()));
|
||||
assert_eq!(
|
||||
res.shell_path,
|
||||
@@ -871,7 +884,7 @@ mod tests {
|
||||
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!(setup_zsh(&sh("zsh"), &EncodedOs::from_path(&dir), &mut env).is_none());
|
||||
assert_eq!(0, env.len());
|
||||
}
|
||||
|
||||
@@ -879,9 +892,11 @@ mod tests {
|
||||
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 root_enc = EncodedOs::from_path(&root);
|
||||
let mut env = EnvMap::new();
|
||||
let r = setup(root, &sh("bash"), &mut env, None).unwrap().unwrap();
|
||||
let r = setup(&root_enc, &sh("bash"), &mut env, None)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(r.shell, Shell::Bash);
|
||||
assert!(
|
||||
env.get("ENV")
|
||||
@@ -891,8 +906,7 @@ mod tests {
|
||||
.ends_with("shell-integration/bash/ghostty.bash")
|
||||
);
|
||||
assert!(
|
||||
Path::new(root)
|
||||
.join("shell-integration/nushell/vendor/autoload/ghostty.nu")
|
||||
root.join("shell-integration/nushell/vendor/autoload/ghostty.nu")
|
||||
.is_file()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1022,7 +1022,7 @@ impl StreamHandler<'_> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn color_operation(&mut self, requests: &osc_color::List, terminator: Terminator) {
|
||||
fn color_operation(&mut self, requests: &osc_color::List<'_>, terminator: Terminator) {
|
||||
// return early if there is nothing to do
|
||||
if requests.is_empty() {
|
||||
return;
|
||||
@@ -1030,8 +1030,8 @@ impl StreamHandler<'_> {
|
||||
|
||||
let mut response: Vec<u8> = Vec::new();
|
||||
|
||||
for req in requests {
|
||||
match *req {
|
||||
for req in requests.iter() {
|
||||
match req {
|
||||
Request::Set(set) => {
|
||||
match set.target {
|
||||
Target::Palette(i) => {
|
||||
@@ -1216,13 +1216,13 @@ impl StreamHandler<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn kitty_color_report(&mut self, request: &kitty::color::OSC) {
|
||||
fn kitty_color_report(&mut self, request: &kitty::color::OSC<'_>) {
|
||||
use kitty::color::{Kind, Request as KReq, Special};
|
||||
|
||||
let mut out: Vec<u8> = Vec::new();
|
||||
|
||||
for item in &request.list {
|
||||
match item {
|
||||
for item in request.list.iter() {
|
||||
match &item {
|
||||
KReq::Query(key) => {
|
||||
// If the writer buffer is empty, we need to write our prefix
|
||||
if out.is_empty() {
|
||||
|
||||
@@ -90,6 +90,48 @@ pub fn windows_create_command_line<S: AsRef<str>>(argv: &[S]) -> String {
|
||||
buf
|
||||
}
|
||||
|
||||
/// [`windows_create_command_line`] over UTF-16 arguments (which may hold
|
||||
/// lone surrogates, so they cannot be a `str`). The quoting rules are the
|
||||
/// same: only ASCII characters are significant, so the logic is identical on
|
||||
/// code units.
|
||||
pub fn windows_create_command_line_wide(argv: &[Vec<u16>]) -> Vec<u16> {
|
||||
let (sp, tab, nl, quote, bs) = (
|
||||
u16::from(b' '),
|
||||
u16::from(b'\t'),
|
||||
u16::from(b'\n'),
|
||||
u16::from(b'"'),
|
||||
u16::from(b'\\'),
|
||||
);
|
||||
let mut buf: Vec<u16> = Vec::new();
|
||||
for (arg_i, arg) in argv.iter().enumerate() {
|
||||
if arg_i != 0 {
|
||||
buf.push(sp);
|
||||
}
|
||||
if !arg.iter().any(|c| [sp, tab, nl, quote].contains(c)) {
|
||||
buf.extend_from_slice(arg);
|
||||
continue;
|
||||
}
|
||||
buf.push(quote);
|
||||
let mut backslash_count = 0usize;
|
||||
for &c in arg {
|
||||
if c == bs {
|
||||
backslash_count += 1;
|
||||
} else if c == quote {
|
||||
buf.extend(std::iter::repeat_n(bs, backslash_count * 2 + 1));
|
||||
buf.push(quote);
|
||||
backslash_count = 0;
|
||||
} else {
|
||||
buf.extend(std::iter::repeat_n(bs, backslash_count));
|
||||
buf.push(c);
|
||||
backslash_count = 0;
|
||||
}
|
||||
}
|
||||
buf.extend(std::iter::repeat_n(bs, backslash_count * 2));
|
||||
buf.push(quote);
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
/// The argv for a `shell` command on Windows (`execCommand`): the command is
|
||||
/// run as `%WINDIR%\System32\cmd.exe /C <command>`.
|
||||
pub fn windows_shell_args(windir: &str, command: &str) -> Vec<String> {
|
||||
@@ -115,6 +157,25 @@ mod tests {
|
||||
windows_create_command_line(args)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_command_line_matches_str_version_and_keeps_lone_surrogates() {
|
||||
let w = |s: &str| s.encode_utf16().collect::<Vec<u16>>();
|
||||
for args in [
|
||||
vec!["a", "b c", "d\"e", "f\\", "g h\\"],
|
||||
vec!["cmd.exe", "/C", "echo \"hi\" there"],
|
||||
] {
|
||||
let wide: Vec<Vec<u16>> = args.iter().map(|a| w(a)).collect();
|
||||
assert_eq!(
|
||||
windows_create_command_line_wide(&wide),
|
||||
w(&windows_create_command_line(&args))
|
||||
);
|
||||
}
|
||||
// A lone surrogate inside a quoted argument is carried unchanged.
|
||||
let lone = vec![u16::from(b'a'), u16::from(b' '), 0xD800];
|
||||
let out = windows_create_command_line_wide(&[lone]);
|
||||
assert_eq!(out, [34, 97, 32, 0xD800, 34]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_args_use_windir_cmd() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in new issue
Block a user