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
0f898d3e41
commit
8f3e206d9f
92 files changed
+2967
-992
No files matched your search
@@ -18,3 +18,4 @@ workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
onig = "6.5.3"
|
||||
temp-env = "0.3.6"
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use ghostty_os::osbytes::EncodedOs;
|
||||
|
||||
use crate::{
|
||||
cli_args::{self, ArgError, ArgIter, CliTarget},
|
||||
command::Command,
|
||||
@@ -3765,7 +3767,7 @@ pub enum ReplayStep {
|
||||
Arg(String),
|
||||
|
||||
/// A base path to expand relative paths against.
|
||||
Expand(String),
|
||||
Expand(EncodedOs),
|
||||
|
||||
/// A conditional argument. This arg is parsed only if all
|
||||
/// conditions match (an "AND"). An "OR" can be achieved by
|
||||
@@ -3876,12 +3878,15 @@ impl Config {
|
||||
/// Load configuration from the target config file at `path`.
|
||||
///
|
||||
/// `path` must be resolved and absolute.
|
||||
pub fn load_file(&mut self, path: &str) -> Result<(), Error> {
|
||||
assert!(ghostty_os::osbytes::config_str_to_path(path).is_absolute());
|
||||
pub fn load_file(&mut self, path: &EncodedOs) -> Result<(), Error> {
|
||||
assert!(path.to_path().is_absolute());
|
||||
let file = match crate::file_load::open(path) {
|
||||
Ok(f) => f,
|
||||
Err(crate::file_load::OpenFileError::NotAFile) => {
|
||||
log::warn!("config-file {path}: not reading because it is not a file");
|
||||
log::warn!(
|
||||
"config-file {}: not reading because it is not a file",
|
||||
path.as_encoded()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
Err(crate::file_load::OpenFileError::FileNotFound) => {
|
||||
@@ -3894,8 +3899,8 @@ impl Config {
|
||||
}
|
||||
|
||||
/// Load config from the given File.
|
||||
fn load_fs_file(&mut self, file: std::fs::File, path: &str) -> Result<(), Error> {
|
||||
log::info!("reading configuration file path={path}");
|
||||
fn load_fs_file(&mut self, file: std::fs::File, path: &EncodedOs) -> Result<(), Error> {
|
||||
log::info!("reading configuration file path={}", path.as_encoded());
|
||||
let mut reader = std::io::BufReader::with_capacity(2048, file);
|
||||
self.load_reader(&mut reader, path)
|
||||
}
|
||||
@@ -3904,7 +3909,7 @@ impl Config {
|
||||
fn load_reader<R: std::io::BufRead>(
|
||||
&mut self,
|
||||
reader: &mut R,
|
||||
path: &str,
|
||||
path: &EncodedOs,
|
||||
) -> Result<(), Error> {
|
||||
// If the file starts with a UTF-8 byte order mark, skip it.
|
||||
// https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8
|
||||
@@ -3914,11 +3919,12 @@ impl Config {
|
||||
reader.consume(3);
|
||||
}
|
||||
}
|
||||
let mut iter = cli_args::LineIterator::with_path(reader, path);
|
||||
let mut iter = cli_args::LineIterator::with_path(reader, path.as_encoded());
|
||||
self.load_iter(&mut iter)?;
|
||||
let dir = ghostty_os::osbytes::config_str_to_path(path)
|
||||
let dir = path
|
||||
.to_path()
|
||||
.parent()
|
||||
.map(ghostty_os::osbytes::path_to_config_string)
|
||||
.map(EncodedOs::from_path)
|
||||
.unwrap_or_default();
|
||||
self.expand_paths(&dir)?;
|
||||
Ok(())
|
||||
@@ -3927,23 +3933,26 @@ impl Config {
|
||||
/// Load optional configuration file from `path`. All errors are ignored.
|
||||
///
|
||||
/// Returns the action that was taken.
|
||||
pub fn load_optional_file(&mut self, path: &str) -> OptionalFileAction {
|
||||
pub fn load_optional_file(&mut self, path: &EncodedOs) -> OptionalFileAction {
|
||||
match self.load_file(path) {
|
||||
Ok(()) => OptionalFileAction::Loaded,
|
||||
Err(Error::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
OptionalFileAction::NotFound
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("error reading optional config file, not loading err={err} path={path}");
|
||||
log::warn!(
|
||||
"error reading optional config file, not loading err={err} path={}",
|
||||
path.as_encoded()
|
||||
);
|
||||
OptionalFileAction::Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The default template with the (decoded, byte-exact) path substituted.
|
||||
fn render_config_template(path: &str) -> Vec<u8> {
|
||||
fn render_config_template(path: &EncodedOs) -> Vec<u8> {
|
||||
let tmpl = include_str!("config-template");
|
||||
let path_bytes = ghostty_os::osbytes::config_str_to_bytes(path);
|
||||
let path_bytes = path.to_bytes();
|
||||
let mut out = Vec::with_capacity(tmpl.len() + path_bytes.len());
|
||||
for (i, part) in tmpl.split("{[path]s}").enumerate() {
|
||||
if i > 0 {
|
||||
@@ -3954,10 +3963,9 @@ impl Config {
|
||||
out
|
||||
}
|
||||
|
||||
fn write_config_template(path: &str) -> std::io::Result<()> {
|
||||
log::info!("creating template config file: path={path}");
|
||||
// `path` carries the reversible byte encoding; decode it for the OS.
|
||||
let os_path = ghostty_os::osbytes::config_str_to_path(path);
|
||||
fn write_config_template(path: &EncodedOs) -> std::io::Result<()> {
|
||||
log::info!("creating template config file: path={}", path.as_encoded());
|
||||
let os_path = path.to_path();
|
||||
if let Some(dir) = os_path.parent() {
|
||||
std::fs::create_dir_all(dir)?;
|
||||
}
|
||||
@@ -3982,7 +3990,11 @@ impl Config {
|
||||
if xdg_action != OptionalFileAction::NotFound
|
||||
&& legacy_xdg_action != OptionalFileAction::NotFound
|
||||
{
|
||||
log::warn!("both config files `{legacy_xdg_path}` and `{xdg_path}` exist.");
|
||||
log::warn!(
|
||||
"both config files `{}` and `{}` exist.",
|
||||
legacy_xdg_path.as_encoded(),
|
||||
xdg_path.as_encoded()
|
||||
);
|
||||
log::warn!("loading them both in that order");
|
||||
true
|
||||
} else {
|
||||
@@ -4014,7 +4026,9 @@ impl Config {
|
||||
&& legacy_app_support_action != OptionalFileAction::NotFound
|
||||
{
|
||||
log::warn!(
|
||||
"both config files `{legacy_app_support_path}` and `{app_support_path}` exist."
|
||||
"both config files `{}` and `{}` exist.",
|
||||
legacy_app_support_path.as_encoded(),
|
||||
app_support_path.as_encoded()
|
||||
);
|
||||
log::warn!("loading them both in that order");
|
||||
true
|
||||
@@ -4142,7 +4156,7 @@ impl Config {
|
||||
// Any paths referenced from the CLI are relative to the current working
|
||||
// directory.
|
||||
let cwd = std::fs::canonicalize(".")?;
|
||||
self.expand_paths(&ghostty_os::osbytes::path_to_config_string(&cwd))?;
|
||||
self.expand_paths(&EncodedOs::from_path(&cwd))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4153,7 +4167,7 @@ impl Config {
|
||||
}
|
||||
|
||||
// Keeps track of loaded files to prevent cycles.
|
||||
let mut loaded: HashSet<String> = HashSet::new();
|
||||
let mut loaded: HashSet<EncodedOs> = HashSet::new();
|
||||
|
||||
// We need to insert all of our loaded config-file values
|
||||
// PRIOR to the "-e" in our replay steps, since everything
|
||||
@@ -4186,12 +4200,12 @@ impl Config {
|
||||
|
||||
// All paths should already be absolute at this point because
|
||||
// they're fixed up after each load.
|
||||
assert!(ghostty_os::osbytes::config_str_to_path(&path).is_absolute());
|
||||
assert!(path.to_path().is_absolute());
|
||||
|
||||
// We must only load a unique file once
|
||||
if !loaded.insert(path.clone()) {
|
||||
let diag = Diagnostic {
|
||||
message: format!("config-file {path}: cycle detected"),
|
||||
message: format!("config-file {}: cycle detected", path.as_encoded()),
|
||||
..Diagnostic::default()
|
||||
};
|
||||
self.diagnostics.append(diag.clone());
|
||||
@@ -4199,13 +4213,14 @@ impl Config {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file = match std::fs::File::open(ghostty_os::osbytes::config_str_to_path(&path)) {
|
||||
let file = match std::fs::File::open(path.to_path()) {
|
||||
Ok(f) => f,
|
||||
Err(err) => {
|
||||
if err.kind() != std::io::ErrorKind::NotFound || !optional {
|
||||
let diag = Diagnostic {
|
||||
message: format!(
|
||||
"error opening config-file {path}: {}",
|
||||
"error opening config-file {}: {}",
|
||||
path.as_encoded(),
|
||||
zig_io_error_name(&err)
|
||||
),
|
||||
..Diagnostic::default()
|
||||
@@ -4221,7 +4236,8 @@ impl Config {
|
||||
if !meta.is_file() {
|
||||
let diag = Diagnostic {
|
||||
message: format!(
|
||||
"config-file {path}: not reading because file type is {}",
|
||||
"config-file {}: not reading because file type is {}",
|
||||
path.as_encoded(),
|
||||
file_kind_name(&meta)
|
||||
),
|
||||
..Diagnostic::default()
|
||||
@@ -4347,9 +4363,9 @@ impl Config {
|
||||
|
||||
/// Expand the relative paths in config-files to be absolute paths
|
||||
/// relative to the base directory.
|
||||
fn expand_paths(&mut self, base: &str) -> Result<(), Error> {
|
||||
fn expand_paths(&mut self, base: &EncodedOs) -> Result<(), Error> {
|
||||
// Keep track of this step for replays
|
||||
self.replay_steps.push(ReplayStep::Expand(base.to_string()));
|
||||
self.replay_steps.push(ReplayStep::Expand(base.clone()));
|
||||
|
||||
// Expand all of our paths
|
||||
if let Some(path) = &mut self.background_image {
|
||||
@@ -4376,7 +4392,7 @@ impl Config {
|
||||
// Find our theme file and open it. See the open function for details.
|
||||
let resources_dir = ghostty_os::resources_dir().ok();
|
||||
let Some(themefile) = crate::theme::open(
|
||||
name,
|
||||
&EncodedOs::from_encoded(name),
|
||||
resources_dir.as_ref().and_then(|r| r.app()),
|
||||
&mut self.diagnostics,
|
||||
) else {
|
||||
@@ -4400,7 +4416,7 @@ impl Config {
|
||||
|
||||
// Load our theme
|
||||
let mut reader = std::io::BufReader::with_capacity(2048, file);
|
||||
let mut iter = cli_args::LineIterator::with_path(&mut reader, path);
|
||||
let mut iter = cli_args::LineIterator::with_path(&mut reader, path.as_encoded());
|
||||
new_config.load_iter(&mut iter)?;
|
||||
|
||||
// Setup our replay to be conditional.
|
||||
@@ -4641,14 +4657,17 @@ impl Config {
|
||||
let pw = ghostty_os::passwd::get()?;
|
||||
if self.command.is_none() {
|
||||
if let Some(sh) = pw.shell {
|
||||
log::info!("default shell src=passwd value={sh}");
|
||||
self.command = Some(Command::Shell(sh));
|
||||
log::info!("default shell src=passwd value={}", sh.as_encoded());
|
||||
self.command = Some(Command::Shell(sh.into_encoded()));
|
||||
}
|
||||
}
|
||||
|
||||
if *wd == WorkingDirectory::Home {
|
||||
if let Some(home) = pw.home {
|
||||
log::info!("default working directory src=passwd value={home}");
|
||||
log::info!(
|
||||
"default working directory src=passwd value={}",
|
||||
home.as_encoded()
|
||||
);
|
||||
*wd = WorkingDirectory::Path(home);
|
||||
} else {
|
||||
*wd = WorkingDirectory::Inherit;
|
||||
@@ -4951,7 +4970,7 @@ mod tests {
|
||||
let td = ghostty_os::TempDir::init().unwrap();
|
||||
let dir = td.path().join(std::ffi::OsStr::from_bytes(b"cfg\xff"));
|
||||
let file = dir.join("config.ghostty");
|
||||
let enc = ghostty_os::osbytes::path_to_config_string(&file);
|
||||
let enc = EncodedOs::from_path(&file);
|
||||
Config::write_config_template(&enc).unwrap();
|
||||
let data = std::fs::read(&file).unwrap();
|
||||
let raw = file.as_os_str().as_bytes();
|
||||
@@ -4985,8 +5004,11 @@ mod tests {
|
||||
let data = "\u{feff}abnormal-command-exit-runtime = 2500\n";
|
||||
let mut reader = Cursor::new(data.as_bytes().to_vec());
|
||||
let mut cfg = Config::default();
|
||||
cfg.load_reader(&mut reader, "/home/ghostty/.config/ghostty/config.ghostty")
|
||||
.unwrap();
|
||||
cfg.load_reader(
|
||||
&mut reader,
|
||||
&EncodedOs::from_encoded("/home/ghostty/.config/ghostty/config.ghostty"),
|
||||
)
|
||||
.unwrap();
|
||||
cfg.finalize().unwrap();
|
||||
|
||||
assert!(cfg.diagnostics.empty());
|
||||
@@ -4997,8 +5019,11 @@ mod tests {
|
||||
let data = "abnormal-command-exit-runtime = 2500\n";
|
||||
let mut reader = Cursor::new(data.as_bytes().to_vec());
|
||||
let mut cfg = Config::default();
|
||||
cfg.load_reader(&mut reader, "/home/ghostty/.config/ghostty/config.ghostty")
|
||||
.unwrap();
|
||||
cfg.load_reader(
|
||||
&mut reader,
|
||||
&EncodedOs::from_encoded("/home/ghostty/.config/ghostty/config.ghostty"),
|
||||
)
|
||||
.unwrap();
|
||||
cfg.finalize().unwrap();
|
||||
|
||||
assert!(cfg.diagnostics.empty());
|
||||
@@ -5141,11 +5166,11 @@ mod tests {
|
||||
cfg.load_iter(&mut i).unwrap();
|
||||
cfg.finalize().unwrap();
|
||||
|
||||
let expected = ghostty_os::expand_home("~/projects/ghostty")
|
||||
.unwrap_or_else(|_| "~/projects/ghostty".to_string());
|
||||
let expected = ghostty_os::expand_home(&EncodedOs::from_encoded("~/projects/ghostty"))
|
||||
.unwrap_or_else(|_| EncodedOs::from_encoded("~/projects/ghostty"));
|
||||
assert_eq!(
|
||||
cfg.working_directory.as_ref().unwrap().value().unwrap(),
|
||||
expected
|
||||
&expected
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5420,7 +5445,7 @@ mod tests {
|
||||
let mut cfg = Config::default();
|
||||
cfg.load_iter(&mut it(&[&format!("--config-file={}", a.display())]))
|
||||
.unwrap();
|
||||
cfg.expand_paths("/").unwrap();
|
||||
cfg.expand_paths(&EncodedOs::from_encoded("/")).unwrap();
|
||||
cfg.load_recursive_files().unwrap();
|
||||
assert!(
|
||||
cfg.diagnostics
|
||||
@@ -5435,7 +5460,7 @@ mod tests {
|
||||
let mut cfg = Config::default();
|
||||
cfg.load_iter(&mut it(&["--config-file=?/nonexistent/ghostty/file"]))
|
||||
.unwrap();
|
||||
cfg.expand_paths("/").unwrap();
|
||||
cfg.expand_paths(&EncodedOs::from_encoded("/")).unwrap();
|
||||
cfg.load_recursive_files().unwrap();
|
||||
assert!(cfg.diagnostics.empty());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! Opening the config for editing (port of `config/edit.zig`).
|
||||
|
||||
use std::{fs::OpenOptions, io, path::Path};
|
||||
use std::{fs::OpenOptions, io};
|
||||
|
||||
use ghostty_os::osbytes::EncodedOs;
|
||||
|
||||
use crate::file_load;
|
||||
|
||||
@@ -13,12 +15,16 @@ use crate::file_load;
|
||||
/// come first.
|
||||
///
|
||||
/// The config file (and its directory) is created if it doesn't exist.
|
||||
pub fn open_path() -> io::Result<String> {
|
||||
pub fn open_path() -> io::Result<EncodedOs> {
|
||||
// Get the path we should open
|
||||
let config_path = config_path()?;
|
||||
|
||||
// The config path is an encoded OS string: decode before touching the
|
||||
// filesystem.
|
||||
let real_path = config_path.to_path();
|
||||
|
||||
// Create config directory recursively.
|
||||
if let Some(config_dir) = Path::new(&config_path).parent() {
|
||||
if let Some(config_dir) = real_path.parent() {
|
||||
std::fs::create_dir_all(config_dir)?;
|
||||
}
|
||||
|
||||
@@ -26,7 +32,7 @@ pub fn open_path() -> io::Result<String> {
|
||||
match OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&config_path)
|
||||
.open(&real_path)
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
|
||||
@@ -41,15 +47,15 @@ fn xdg_err(e: ghostty_os::xdg::Error) -> io::Error {
|
||||
}
|
||||
|
||||
/// Returns the config path to use for open for the current OS.
|
||||
fn config_path() -> io::Result<String> {
|
||||
fn config_path() -> io::Result<EncodedOs> {
|
||||
let paths = config_path_candidates()?;
|
||||
|
||||
// Find the first path that exists and is non-empty. If no paths are
|
||||
// non-empty but at least one exists, we will return the first path that
|
||||
// exists.
|
||||
let mut exists: Option<&String> = None;
|
||||
let mut exists: Option<&EncodedOs> = None;
|
||||
for path in &paths {
|
||||
let meta = match std::fs::File::open(path).and_then(|f| f.metadata()) {
|
||||
let meta = match std::fs::File::open(path.to_path()).and_then(|f| f.metadata()) {
|
||||
Ok(m) => m,
|
||||
// File doesn't exist, continue.
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => continue,
|
||||
@@ -79,7 +85,7 @@ fn config_path() -> io::Result<String> {
|
||||
|
||||
/// Returns a list of possible paths the main config file could be
|
||||
/// in for the current OS.
|
||||
fn config_path_candidates() -> io::Result<Vec<String>> {
|
||||
fn config_path_candidates() -> io::Result<Vec<EncodedOs>> {
|
||||
let mut paths = Vec::with_capacity(4);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -92,3 +98,38 @@ fn config_path_candidates() -> io::Result<Vec<String>> {
|
||||
paths.push(file_load::legacy_default_xdg_path().map_err(xdg_err)?);
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix, not(target_os = "macos")))]
|
||||
mod tests {
|
||||
#![allow(clippy::unwrap_used)]
|
||||
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// "Open config" must create/return the file under the *decoded* XDG
|
||||
/// directory, not a second file at the escaped pathname.
|
||||
#[test]
|
||||
fn open_path_uses_decoded_non_utf8_xdg_dir() {
|
||||
let td = ghostty_os::TempDir::init().unwrap();
|
||||
let xdg = td.path().join(std::ffi::OsStr::from_bytes(b"cfg\xff"));
|
||||
let value = std::ffi::OsString::from_vec(xdg.as_os_str().as_bytes().to_vec());
|
||||
temp_env::with_var("XDG_CONFIG_HOME", Some(&value), || {
|
||||
let p = open_path().unwrap();
|
||||
let real = p.to_path();
|
||||
assert_eq!(real, xdg.join("ghostty/config.ghostty"));
|
||||
assert!(real.is_file());
|
||||
// Nothing was created at the escaped (encoded) pathname.
|
||||
assert_ne!(p.as_encoded().as_bytes(), real.as_os_str().as_bytes());
|
||||
assert!(!std::path::Path::new(p.as_encoded()).exists());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn candidates_are_xdg_paths_in_priority_order() {
|
||||
temp_env::with_var("XDG_CONFIG_HOME", Some("/x/cfg"), || {
|
||||
let c = config_path_candidates().unwrap();
|
||||
assert_eq!(c[0], "/x/cfg/ghostty/config.ghostty");
|
||||
assert_eq!(c[1], "/x/cfg/ghostty/config");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,10 @@
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
use ghostty_os::xdg;
|
||||
use ghostty_os::{osbytes::EncodedOs, xdg};
|
||||
|
||||
/// Default path for the XDG home configuration file.
|
||||
pub fn default_xdg_path() -> Result<String, xdg::Error> {
|
||||
pub fn default_xdg_path() -> Result<EncodedOs, xdg::Error> {
|
||||
xdg::config(&xdg::Options {
|
||||
subdir: Some("ghostty/config.ghostty"),
|
||||
home: None,
|
||||
@@ -18,7 +18,7 @@ pub fn default_xdg_path() -> Result<String, xdg::Error> {
|
||||
}
|
||||
|
||||
/// Ghostty <1.3.0 default path for the XDG home configuration file.
|
||||
pub fn legacy_default_xdg_path() -> Result<String, xdg::Error> {
|
||||
pub fn legacy_default_xdg_path() -> Result<EncodedOs, xdg::Error> {
|
||||
xdg::config(&xdg::Options {
|
||||
subdir: Some("ghostty/config"),
|
||||
home: None,
|
||||
@@ -26,7 +26,7 @@ pub fn legacy_default_xdg_path() -> Result<String, xdg::Error> {
|
||||
}
|
||||
|
||||
/// Preferred default path for the XDG home configuration file.
|
||||
pub fn preferred_xdg_path() -> Result<String, xdg::Error> {
|
||||
pub fn preferred_xdg_path() -> Result<EncodedOs, xdg::Error> {
|
||||
// If the XDG path exists, use that.
|
||||
let xdg_path = default_xdg_path()?;
|
||||
if open(&xdg_path).is_ok() {
|
||||
@@ -45,28 +45,28 @@ pub fn preferred_xdg_path() -> Result<String, xdg::Error> {
|
||||
|
||||
/// Default path for the macOS Application Support configuration file.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn default_app_support_path() -> Result<String, xdg::Error> {
|
||||
pub fn default_app_support_path() -> Result<EncodedOs, xdg::Error> {
|
||||
app_support_dir("config.ghostty")
|
||||
}
|
||||
|
||||
/// Ghostty <1.3.0 default path for the macOS Application Support
|
||||
/// configuration file.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn legacy_default_app_support_path() -> Result<String, xdg::Error> {
|
||||
pub fn legacy_default_app_support_path() -> Result<EncodedOs, xdg::Error> {
|
||||
app_support_dir("config")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn app_support_dir(sub_path: &str) -> Result<String, xdg::Error> {
|
||||
fn app_support_dir(sub_path: &str) -> Result<EncodedOs, xdg::Error> {
|
||||
ghostty_os::macos::app_support_dir(sub_path)
|
||||
.map(|p| ghostty_os::osbytes::path_to_config_string(&p))
|
||||
.map(|p| EncodedOs::from_path(&p))
|
||||
.map_err(|_| xdg::Error::AppleApiFailed)
|
||||
}
|
||||
|
||||
/// Preferred default path for the macOS Application Support configuration
|
||||
/// file.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn preferred_app_support_path() -> Result<String, xdg::Error> {
|
||||
pub fn preferred_app_support_path() -> Result<EncodedOs, xdg::Error> {
|
||||
Ok(preferred_of(
|
||||
default_app_support_path()?,
|
||||
legacy_default_app_support_path()?,
|
||||
@@ -76,7 +76,7 @@ pub fn preferred_app_support_path() -> Result<String, xdg::Error> {
|
||||
/// Pick between a new path and its legacy path: the new one if it exists,
|
||||
/// else the legacy one if it exists, else the new one.
|
||||
#[cfg(any(target_os = "macos", test))]
|
||||
fn preferred_of(new: String, legacy: String) -> String {
|
||||
fn preferred_of(new: EncodedOs, legacy: EncodedOs) -> EncodedOs {
|
||||
// If the new path exists, use that.
|
||||
if open(&new).is_ok() {
|
||||
return new;
|
||||
@@ -95,9 +95,9 @@ fn preferred_of(new: String, legacy: String) -> String {
|
||||
/// one if it exists, else Application Support.
|
||||
#[cfg(any(target_os = "macos", test))]
|
||||
fn preferred_macos(
|
||||
app_support_path: String,
|
||||
xdg_path: &dyn Fn() -> Result<String, xdg::Error>,
|
||||
) -> Result<String, xdg::Error> {
|
||||
app_support_path: EncodedOs,
|
||||
xdg_path: &dyn Fn() -> Result<EncodedOs, xdg::Error>,
|
||||
) -> Result<EncodedOs, xdg::Error> {
|
||||
// macOS prefers the Application Support directory if it exists.
|
||||
if open(&app_support_path).is_ok() {
|
||||
return Ok(app_support_path);
|
||||
@@ -119,7 +119,7 @@ fn preferred_macos(
|
||||
/// This doesn't create or populate the file with any default
|
||||
/// contents; downstream callers must handle this.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn preferred_default_file_path() -> Result<String, xdg::Error> {
|
||||
pub fn preferred_default_file_path() -> Result<EncodedOs, xdg::Error> {
|
||||
preferred_macos(preferred_app_support_path()?, &preferred_xdg_path)
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ pub fn preferred_default_file_path() -> Result<String, xdg::Error> {
|
||||
/// This doesn't create or populate the file with any default
|
||||
/// contents; downstream callers must handle this.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn preferred_default_file_path() -> Result<String, xdg::Error> {
|
||||
pub fn preferred_default_file_path() -> Result<EncodedOs, xdg::Error> {
|
||||
// All other platforms use XDG only
|
||||
preferred_xdg_path()
|
||||
}
|
||||
@@ -158,21 +158,24 @@ impl std::error::Error for OpenFileError {}
|
||||
/// Opens the file at the given path and returns the file handle
|
||||
/// if it exists and is non-empty. This also constrains the possible
|
||||
/// errors to a smaller set that we can explicitly handle.
|
||||
pub fn open(path: &str) -> Result<File, OpenFileError> {
|
||||
assert!(ghostty_os::osbytes::config_str_to_path(path).is_absolute());
|
||||
pub fn open(path: &EncodedOs) -> Result<File, OpenFileError> {
|
||||
let real = path.to_path();
|
||||
assert!(real.is_absolute());
|
||||
|
||||
// `path` may carry the reversible byte escape of non-UTF-8 names.
|
||||
let file = File::open(ghostty_os::osbytes::config_str_to_os(path)).map_err(|err| {
|
||||
let file = File::open(&real).map_err(|err| {
|
||||
if err.kind() == std::io::ErrorKind::NotFound {
|
||||
OpenFileError::FileNotFound
|
||||
} else {
|
||||
log::warn!("unexpected file open error path={path} err={err}");
|
||||
log::warn!(
|
||||
"unexpected file open error path={} err={err}",
|
||||
real.display()
|
||||
);
|
||||
OpenFileError::FileOpenFailed
|
||||
}
|
||||
})?;
|
||||
|
||||
let meta = file.metadata().map_err(|err| {
|
||||
log::warn!("error getting file stat path={path} err={err}");
|
||||
log::warn!("error getting file stat path={} err={err}", real.display());
|
||||
OpenFileError::FileOpenFailed
|
||||
})?;
|
||||
if !meta.is_file() {
|
||||
@@ -190,21 +193,25 @@ pub fn open(path: &str) -> Result<File, OpenFileError> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn e(s: &str) -> EncodedOs {
|
||||
EncodedOs::from_encoded(s)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_missing_dir_and_empty() {
|
||||
assert_eq!(
|
||||
open("/nonexistent/ghostty/file").unwrap_err(),
|
||||
open(&e("/nonexistent/ghostty/file")).unwrap_err(),
|
||||
OpenFileError::FileNotFound
|
||||
);
|
||||
assert_eq!(open("/tmp").unwrap_err(), OpenFileError::NotAFile);
|
||||
assert_eq!(open(&e("/tmp")).unwrap_err(), OpenFileError::NotAFile);
|
||||
let p = std::env::temp_dir().join(format!("ghostty-file-load-{}", std::process::id()));
|
||||
std::fs::write(&p, "").unwrap();
|
||||
assert_eq!(
|
||||
open(p.to_str().unwrap()).unwrap_err(),
|
||||
open(&EncodedOs::from_path(&p)).unwrap_err(),
|
||||
OpenFileError::FileIsEmpty
|
||||
);
|
||||
std::fs::write(&p, "a=b").unwrap();
|
||||
assert!(open(p.to_str().unwrap()).is_ok());
|
||||
assert!(open(&EncodedOs::from_path(&p)).is_ok());
|
||||
std::fs::remove_file(&p).unwrap();
|
||||
}
|
||||
|
||||
@@ -212,18 +219,18 @@ mod tests {
|
||||
fn preferred_of_prefers_existing_new_then_legacy() {
|
||||
let dir = std::env::temp_dir().join(format!("ghostty-pref-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let new = dir.join("config.ghostty").to_str().unwrap().to_owned();
|
||||
let legacy = dir.join("config").to_str().unwrap().to_owned();
|
||||
let _ = std::fs::remove_file(&new);
|
||||
let _ = std::fs::remove_file(&legacy);
|
||||
let new = EncodedOs::from_path(&dir.join("config.ghostty"));
|
||||
let legacy = EncodedOs::from_path(&dir.join("config"));
|
||||
let _ = std::fs::remove_file(new.to_path());
|
||||
let _ = std::fs::remove_file(legacy.to_path());
|
||||
|
||||
// Neither exists: the new one.
|
||||
assert_eq!(preferred_of(new.clone(), legacy.clone()), new);
|
||||
// Only legacy exists: the legacy one.
|
||||
std::fs::write(&legacy, "a=b").unwrap();
|
||||
std::fs::write(legacy.to_path(), "a=b").unwrap();
|
||||
assert_eq!(preferred_of(new.clone(), legacy.clone()), legacy);
|
||||
// Both exist: the new one.
|
||||
std::fs::write(&new, "a=b").unwrap();
|
||||
std::fs::write(new.to_path(), "a=b").unwrap();
|
||||
assert_eq!(preferred_of(new.clone(), legacy.clone()), new);
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
@@ -232,19 +239,19 @@ mod tests {
|
||||
fn preferred_macos_order() {
|
||||
let dir = std::env::temp_dir().join(format!("ghostty-pref-mac-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let app = dir.join("app").to_str().unwrap().to_owned();
|
||||
let xdg_p = dir.join("xdg").to_str().unwrap().to_owned();
|
||||
let _ = std::fs::remove_file(&app);
|
||||
let _ = std::fs::remove_file(&xdg_p);
|
||||
let app = EncodedOs::from_path(&dir.join("app"));
|
||||
let xdg_p = EncodedOs::from_path(&dir.join("xdg"));
|
||||
let _ = std::fs::remove_file(app.to_path());
|
||||
let _ = std::fs::remove_file(xdg_p.to_path());
|
||||
let xdg_fn = || Ok(xdg_p.clone());
|
||||
|
||||
// Neither exists: app support.
|
||||
assert_eq!(preferred_macos(app.clone(), &xdg_fn).unwrap(), app);
|
||||
// XDG exists only: XDG.
|
||||
std::fs::write(&xdg_p, "a=b").unwrap();
|
||||
std::fs::write(xdg_p.to_path(), "a=b").unwrap();
|
||||
assert_eq!(preferred_macos(app.clone(), &xdg_fn).unwrap(), xdg_p);
|
||||
// App support exists: app support wins.
|
||||
std::fs::write(&app, "a=b").unwrap();
|
||||
std::fs::write(app.to_path(), "a=b").unwrap();
|
||||
assert_eq!(preferred_macos(app.clone(), &xdg_fn).unwrap(), app);
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
@@ -252,8 +259,8 @@ mod tests {
|
||||
#[test]
|
||||
fn default_paths_end_with_expected_names() {
|
||||
if let (Ok(a), Ok(b)) = (default_xdg_path(), legacy_default_xdg_path()) {
|
||||
assert!(a.ends_with("ghostty/config.ghostty"));
|
||||
assert!(b.ends_with("ghostty/config"));
|
||||
assert!(a.ends_with_plain("ghostty/config.ghostty"));
|
||||
assert!(b.ends_with_plain("ghostty/config"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
path::{Component, PathBuf},
|
||||
};
|
||||
|
||||
use ghostty_os::osbytes::{config_str_to_path, path_to_config_string};
|
||||
use ghostty_os::osbytes::EncodedOs;
|
||||
|
||||
use crate::{
|
||||
c_value::CValue,
|
||||
@@ -20,21 +20,21 @@ use crate::{
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Path {
|
||||
/// No error if the file does not exist.
|
||||
Optional(String),
|
||||
Optional(EncodedOs),
|
||||
|
||||
/// The file is required to exist.
|
||||
Required(String),
|
||||
Required(EncodedOs),
|
||||
}
|
||||
|
||||
impl Default for Path {
|
||||
fn default() -> Self {
|
||||
Path::Required(String::new())
|
||||
Path::Required(EncodedOs::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// Lexically resolve `base/path` like `std.fs.path.resolve`.
|
||||
fn resolve_lexical(base: &str, path: &str) -> String {
|
||||
let joined = config_str_to_path(base).join(config_str_to_path(path));
|
||||
fn resolve_lexical(base: &EncodedOs, path: &EncodedOs) -> EncodedOs {
|
||||
let joined = base.to_path().join(path.to_path());
|
||||
let mut out = PathBuf::new();
|
||||
for c in joined.components() {
|
||||
match c {
|
||||
@@ -45,29 +45,24 @@ fn resolve_lexical(base: &str, path: &str) -> String {
|
||||
other => out.push(other.as_os_str()),
|
||||
}
|
||||
}
|
||||
path_to_config_string(&out)
|
||||
EncodedOs::from_path(&out)
|
||||
}
|
||||
|
||||
impl Path {
|
||||
/// The inner path string.
|
||||
#[must_use]
|
||||
pub fn path(&self) -> &str {
|
||||
pub fn path(&self) -> &EncodedOs {
|
||||
match self {
|
||||
Path::Optional(p) | Path::Required(p) => p,
|
||||
}
|
||||
}
|
||||
|
||||
fn path_mut(&mut self) -> &mut String {
|
||||
fn path_mut(&mut self) -> &mut EncodedOs {
|
||||
match self {
|
||||
Path::Optional(p) | Path::Required(p) => p,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn len(&self) -> usize {
|
||||
self.path().len()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.path().is_empty()
|
||||
@@ -98,9 +93,9 @@ impl Path {
|
||||
}
|
||||
|
||||
Ok(Some(if optional {
|
||||
Path::Optional(value.to_string())
|
||||
Path::Optional(EncodedOs::from_encoded(value))
|
||||
} else {
|
||||
Path::Required(value.to_string())
|
||||
Path::Required(EncodedOs::from_encoded(value))
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -110,16 +105,15 @@ impl Path {
|
||||
/// `base` is the base directory to expand relative paths; it must be an
|
||||
/// absolute path. Errors are added to `diags` if they occur; an `Err` is
|
||||
/// only returned if the base directory cannot be opened.
|
||||
pub fn expand(&mut self, base: &str, diags: &mut DiagnosticList) -> std::io::Result<()> {
|
||||
// Config strings carry non-UTF-8 bytes via a reversible escape
|
||||
// encoding; filesystem calls use the decoded OS paths.
|
||||
let base_path = config_str_to_path(base);
|
||||
pub fn expand(&mut self, base: &EncodedOs, diags: &mut DiagnosticList) -> std::io::Result<()> {
|
||||
// Filesystem calls use the decoded OS paths.
|
||||
let base_path = base.to_path();
|
||||
assert!(base_path.is_absolute());
|
||||
|
||||
let path = self.path().to_string();
|
||||
let path = self.path().clone();
|
||||
|
||||
// If it is already absolute we can ignore it.
|
||||
if path.is_empty() || config_str_to_path(&path).is_absolute() {
|
||||
if path.is_empty() || path.to_path().is_absolute() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -127,21 +121,27 @@ impl Path {
|
||||
// home directory. We explicitly look for "~/" because we don't
|
||||
// support alternate users such as "~alice/"
|
||||
// (Windows isn't supported yet: Zig skips this there.)
|
||||
if !cfg!(windows) && path.starts_with("~/") {
|
||||
if !cfg!(windows) && path.starts_with_plain("~/") {
|
||||
match ghostty_os::expand_home(&path) {
|
||||
Ok(expanded) => {
|
||||
log::debug!("expanding file path from home directory: path={expanded}");
|
||||
log::debug!(
|
||||
"expanding file path from home directory: path={}",
|
||||
expanded.as_encoded()
|
||||
);
|
||||
*self.path_mut() = expanded;
|
||||
}
|
||||
Err(err) => {
|
||||
diags.append(Diagnostic {
|
||||
message: format!("error expanding home directory for path {path}: {err}"),
|
||||
message: format!(
|
||||
"error expanding home directory for path {}: {err}",
|
||||
path.as_encoded()
|
||||
),
|
||||
..Diagnostic::default()
|
||||
});
|
||||
|
||||
// Blank this path so that we don't attempt to resolve it
|
||||
// again
|
||||
*self = Path::Required(String::new());
|
||||
*self = Path::Required(EncodedOs::new());
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
@@ -150,9 +150,9 @@ impl Path {
|
||||
// Make sure the base directory can be opened.
|
||||
std::fs::read_dir(&base_path)?;
|
||||
|
||||
let full = base_path.join(config_str_to_path(&path));
|
||||
let full = base_path.join(path.to_path());
|
||||
let abs = match std::fs::canonicalize(&full) {
|
||||
Ok(p) => path_to_config_string(&p),
|
||||
Ok(p) => EncodedOs::from_path(&p),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
// The file doesn't exist. Try to resolve the relative path
|
||||
// another way.
|
||||
@@ -160,17 +160,21 @@ impl Path {
|
||||
}
|
||||
Err(err) => {
|
||||
diags.append(Diagnostic {
|
||||
message: format!("error resolving file path {path}: {err}"),
|
||||
message: format!("error resolving file path {}: {err}", path.as_encoded()),
|
||||
..Diagnostic::default()
|
||||
});
|
||||
|
||||
// Blank this path so that we don't attempt to resolve it again
|
||||
*self = Path::Required(String::new());
|
||||
*self = Path::Required(EncodedOs::new());
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
log::debug!("expanding file path relative={path} abs={abs}");
|
||||
log::debug!(
|
||||
"expanding file path relative={} abs={}",
|
||||
path.as_encoded(),
|
||||
abs.as_encoded()
|
||||
);
|
||||
*self.path_mut() = abs;
|
||||
Ok(())
|
||||
}
|
||||
@@ -180,11 +184,11 @@ impl ConfigField for Path {
|
||||
fn c_value(&self) -> CValue {
|
||||
match self {
|
||||
Path::Optional(p) => CValue::Path {
|
||||
path: p.clone(),
|
||||
path: p.as_encoded().to_owned(),
|
||||
optional: true,
|
||||
},
|
||||
Path::Required(p) => CValue::Path {
|
||||
path: p.clone(),
|
||||
path: p.as_encoded().to_owned(),
|
||||
optional: false,
|
||||
},
|
||||
}
|
||||
@@ -205,8 +209,8 @@ impl ConfigField for Path {
|
||||
/// Used by formatter.
|
||||
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Path::Optional(p) => writeln!(f.writer, "{} = ?{p}", f.name),
|
||||
Path::Required(p) => writeln!(f.writer, "{} = {p}", f.name),
|
||||
Path::Optional(p) => writeln!(f.writer, "{} = ?{}", f.name, p.as_encoded()),
|
||||
Path::Required(p) => writeln!(f.writer, "{} = {}", f.name, p.as_encoded()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,7 +226,7 @@ pub struct RepeatablePath {
|
||||
|
||||
impl RepeatablePath {
|
||||
/// Expand all the paths relative to the base directory.
|
||||
pub fn expand(&mut self, base: &str, diags: &mut DiagnosticList) -> std::io::Result<()> {
|
||||
pub fn expand(&mut self, base: &EncodedOs, diags: &mut DiagnosticList) -> std::io::Result<()> {
|
||||
for path in &mut self.value {
|
||||
path.expand(base, diags)?;
|
||||
}
|
||||
@@ -267,6 +271,10 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::formatter::format_entry;
|
||||
|
||||
fn e(s: &str) -> EncodedOs {
|
||||
EncodedOs::from_encoded(s)
|
||||
}
|
||||
|
||||
fn fmt<T: ConfigField>(v: &T) -> String {
|
||||
let mut s = String::new();
|
||||
format_entry("a", v, &mut s).unwrap();
|
||||
@@ -276,13 +284,13 @@ mod tests {
|
||||
#[test]
|
||||
fn path_parse() {
|
||||
let p = |s| Path::parse(Some(s)).unwrap().unwrap();
|
||||
assert_eq!(p("config.1"), Path::Required("config.1".into()));
|
||||
assert_eq!(p("?config.2"), Path::Optional("config.2".into()));
|
||||
assert_eq!(p("\"?config.3\""), Path::Required("?config.3".into()));
|
||||
assert_eq!(p("?\"config.4\""), Path::Optional("config.4".into()));
|
||||
assert_eq!(p("?"), Path::Optional(String::new()));
|
||||
assert_eq!(p("\"\""), Path::Required(String::new()));
|
||||
assert_eq!(p("?\"\""), Path::Optional(String::new()));
|
||||
assert_eq!(p("config.1"), Path::Required(e("config.1")));
|
||||
assert_eq!(p("?config.2"), Path::Optional(e("config.2")));
|
||||
assert_eq!(p("\"?config.3\""), Path::Required(e("?config.3")));
|
||||
assert_eq!(p("?\"config.4\""), Path::Optional(e("config.4")));
|
||||
assert_eq!(p("?"), Path::Optional(EncodedOs::new()));
|
||||
assert_eq!(p("\"\""), Path::Required(EncodedOs::new()));
|
||||
assert_eq!(p("?\"\""), Path::Optional(EncodedOs::new()));
|
||||
assert_eq!(Path::parse(None), Err(ArgError::ValueRequired));
|
||||
assert_eq!(Path::parse(Some("")), Ok(None));
|
||||
}
|
||||
@@ -292,21 +300,21 @@ mod tests {
|
||||
let mut item = Path::default();
|
||||
|
||||
item.parse_cli(Some("config.1")).unwrap();
|
||||
assert_eq!(item, Path::Required("config.1".into()));
|
||||
assert_eq!(item, Path::Required(e("config.1")));
|
||||
|
||||
item.parse_cli(Some("?config.2")).unwrap();
|
||||
assert_eq!(item, Path::Optional("config.2".into()));
|
||||
assert_eq!(item, Path::Optional(e("config.2")));
|
||||
|
||||
item.parse_cli(Some("\"?config.3\"")).unwrap();
|
||||
assert_eq!(item, Path::Required("?config.3".into()));
|
||||
assert_eq!(item, Path::Required(e("?config.3")));
|
||||
|
||||
// Zero-length values, ignored
|
||||
item.parse_cli(Some("?")).unwrap();
|
||||
assert_eq!(item, Path::Required("?config.3".into()));
|
||||
assert_eq!(item, Path::Required(e("?config.3")));
|
||||
item.parse_cli(Some("\"\"")).unwrap();
|
||||
assert_eq!(item, Path::Required("?config.3".into()));
|
||||
assert_eq!(item, Path::Required(e("?config.3")));
|
||||
item.parse_cli(Some("?\"\"")).unwrap();
|
||||
assert_eq!(item, Path::Required("?config.3".into()));
|
||||
assert_eq!(item, Path::Required(e("?config.3")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -339,9 +347,9 @@ mod tests {
|
||||
|
||||
assert_eq!(list.value.len(), 3);
|
||||
|
||||
assert_eq!(list.value[0], Path::Required("config.1".into()));
|
||||
assert_eq!(list.value[1], Path::Optional("config.2".into()));
|
||||
assert_eq!(list.value[2], Path::Required("?config.3".into()));
|
||||
assert_eq!(list.value[0], Path::Required(e("config.1")));
|
||||
assert_eq!(list.value[1], Path::Optional(e("config.2")));
|
||||
assert_eq!(list.value[2], Path::Required(e("?config.3")));
|
||||
|
||||
list.parse_cli(Some("")).unwrap();
|
||||
assert_eq!(list.value.len(), 0);
|
||||
@@ -370,13 +378,13 @@ mod tests {
|
||||
#[test]
|
||||
fn path_expand_relative_nonexistent_and_absolute() {
|
||||
let mut diags = DiagnosticList::default();
|
||||
let mut p = Path::Required("../nope/x.conf".into());
|
||||
p.expand("/tmp", &mut diags).unwrap();
|
||||
assert_eq!(p, Path::Required("/nope/x.conf".into()));
|
||||
let mut p = Path::Required(e("../nope/x.conf"));
|
||||
p.expand(&e("/tmp"), &mut diags).unwrap();
|
||||
assert_eq!(p, Path::Required(e("/nope/x.conf")));
|
||||
|
||||
let mut p = Path::Optional("/abs/path".into());
|
||||
p.expand("/tmp", &mut diags).unwrap();
|
||||
assert_eq!(p, Path::Optional("/abs/path".into()));
|
||||
let mut p = Path::Optional(e("/abs/path"));
|
||||
p.expand(&e("/tmp"), &mut diags).unwrap();
|
||||
assert_eq!(p, Path::Optional(e("/abs/path")));
|
||||
assert!(diags.is_empty());
|
||||
}
|
||||
|
||||
@@ -390,12 +398,12 @@ mod tests {
|
||||
std::fs::write(&target, b"x").unwrap();
|
||||
std::os::unix::fs::symlink(&target, base.join("link")).unwrap();
|
||||
|
||||
let mut p = Path::Required("link".into());
|
||||
let mut p = Path::Required(e("link"));
|
||||
let mut diags = DiagnosticList::default();
|
||||
p.expand(&path_to_config_string(&base), &mut diags).unwrap();
|
||||
p.expand(&EncodedOs::from_path(&base), &mut diags).unwrap();
|
||||
assert_eq!(diags.items().len(), 0);
|
||||
// The canonical (non-UTF-8) target is preserved byte for byte.
|
||||
assert_eq!(config_str_to_path(p.path()), target);
|
||||
assert!(std::fs::metadata(config_str_to_path(p.path())).is_ok());
|
||||
assert_eq!(p.path().to_path(), target);
|
||||
assert!(std::fs::metadata(p.path().to_path()).is_ok());
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,7 @@
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
use ghostty_os::osbytes::{
|
||||
config_str_to_os,
|
||||
config_str_to_path,
|
||||
os_to_config_string,
|
||||
path_to_config_string,
|
||||
};
|
||||
use ghostty_os::osbytes::EncodedOs;
|
||||
|
||||
use crate::diagnostics::{Diagnostic, DiagnosticList};
|
||||
|
||||
@@ -33,7 +28,7 @@ impl Location {
|
||||
/// invalid for any reason. For example, it is perfectly valid to
|
||||
/// install and run Ghostty without the resources directory.
|
||||
#[must_use]
|
||||
pub fn dir(self, resources_dir: Option<&str>) -> Option<String> {
|
||||
pub fn dir(self, resources_dir: Option<&EncodedOs>) -> Option<EncodedOs> {
|
||||
match self {
|
||||
Location::User => ghostty_os::xdg::config(&ghostty_os::xdg::Options {
|
||||
subdir: Some("ghostty/themes"),
|
||||
@@ -42,9 +37,7 @@ impl Location {
|
||||
.ok(),
|
||||
Location::Resources => {
|
||||
let base = resources_dir?;
|
||||
Some(path_to_config_string(
|
||||
&config_str_to_path(base).join("themes"),
|
||||
))
|
||||
Some(EncodedOs::from_path(&base.to_path().join("themes")))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,24 +47,24 @@ impl Location {
|
||||
/// order of priority.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LocationIterator {
|
||||
pub resources_dir: Option<String>,
|
||||
pub resources_dir: Option<EncodedOs>,
|
||||
pub i: usize,
|
||||
}
|
||||
|
||||
impl LocationIterator {
|
||||
#[must_use]
|
||||
pub fn new(resources_dir: Option<&str>) -> Self {
|
||||
pub fn new(resources_dir: Option<&EncodedOs>) -> Self {
|
||||
Self {
|
||||
resources_dir: resources_dir.map(str::to_string),
|
||||
resources_dir: resources_dir.cloned(),
|
||||
i: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_location(&mut self) -> Option<(Location, String)> {
|
||||
pub fn next_location(&mut self) -> Option<(Location, EncodedOs)> {
|
||||
while self.i < Location::ALL.len() {
|
||||
let location = Location::ALL[self.i];
|
||||
self.i += 1;
|
||||
if let Some(dir) = location.dir(self.resources_dir.as_deref()) {
|
||||
if let Some(dir) = location.dir(self.resources_dir.as_ref()) {
|
||||
return Some((location, dir));
|
||||
}
|
||||
}
|
||||
@@ -86,7 +79,7 @@ impl LocationIterator {
|
||||
/// An opened theme.
|
||||
#[derive(Debug)]
|
||||
pub struct OpenedTheme {
|
||||
pub path: String,
|
||||
pub path: EncodedOs,
|
||||
pub file: File,
|
||||
}
|
||||
|
||||
@@ -160,31 +153,34 @@ fn check_regular(file: &File, theme: &str, diags: &mut DiagnosticList) -> bool {
|
||||
/// the theme resolves to something other than a regular file a diagnostic entry
|
||||
/// will be added to the list and None will be returned.
|
||||
pub fn open(
|
||||
theme: &str,
|
||||
resources_dir: Option<&str>,
|
||||
theme: &EncodedOs,
|
||||
resources_dir: Option<&EncodedOs>,
|
||||
diags: &mut DiagnosticList,
|
||||
) -> Option<OpenedTheme> {
|
||||
let theme_path = theme.to_path();
|
||||
let theme_text = theme.as_encoded();
|
||||
|
||||
// Absolute themes are loaded a different path.
|
||||
if config_str_to_path(theme).is_absolute() {
|
||||
if theme_path.is_absolute() {
|
||||
let file = open_absolute(theme, diags)?;
|
||||
if !check_regular(&file, theme, diags) {
|
||||
if !check_regular(&file, theme_text, diags) {
|
||||
return None;
|
||||
}
|
||||
return Some(OpenedTheme {
|
||||
path: theme.to_string(),
|
||||
path: theme.clone(),
|
||||
file,
|
||||
});
|
||||
}
|
||||
|
||||
let basename = config_str_to_path(theme)
|
||||
let basename = theme_path
|
||||
.file_name()
|
||||
.map(os_to_config_string)
|
||||
.map(EncodedOs::from_os)
|
||||
.unwrap_or_default();
|
||||
if theme != basename {
|
||||
if *theme != basename {
|
||||
diag(
|
||||
diags,
|
||||
format!(
|
||||
"theme \"{theme}\" cannot include path separators unless it is an absolute path"
|
||||
"theme \"{theme_text}\" cannot include path separators unless it is an absolute path"
|
||||
),
|
||||
);
|
||||
return None;
|
||||
@@ -194,10 +190,10 @@ pub fn open(
|
||||
// one that exists.
|
||||
let mut it = LocationIterator::new(resources_dir);
|
||||
while let Some((_, dir)) = it.next_location() {
|
||||
let path = path_to_config_string(&config_str_to_path(&dir).join(config_str_to_os(theme)));
|
||||
match File::open(config_str_to_path(&path)) {
|
||||
let path = EncodedOs::from_path(&dir.to_path().join(theme.to_os()));
|
||||
match File::open(path.to_path()) {
|
||||
Ok(file) => {
|
||||
if !check_regular(&file, theme, diags) {
|
||||
if !check_regular(&file, theme_text, diags) {
|
||||
return None;
|
||||
}
|
||||
return Some(OpenedTheme { path, file });
|
||||
@@ -208,7 +204,10 @@ pub fn open(
|
||||
Err(err) => {
|
||||
diag(
|
||||
diags,
|
||||
format!("failed to load theme \"{theme}\" from the file \"{path}\": {err}"),
|
||||
format!(
|
||||
"failed to load theme \"{theme_text}\" from the file \"{}\": {err}",
|
||||
path.as_encoded()
|
||||
),
|
||||
);
|
||||
return None;
|
||||
}
|
||||
@@ -219,10 +218,13 @@ pub fn open(
|
||||
// our iterator, reiterate over in order to build a better error message.
|
||||
it.reset();
|
||||
while let Some((_, dir)) = it.next_location() {
|
||||
let path = path_to_config_string(&config_str_to_path(&dir).join(config_str_to_os(theme)));
|
||||
let path = EncodedOs::from_path(&dir.to_path().join(theme.to_os()));
|
||||
diag(
|
||||
diags,
|
||||
format!("theme \"{theme}\" not found, tried path \"{path}\""),
|
||||
format!(
|
||||
"theme \"{theme_text}\" not found, tried path \"{}\"",
|
||||
path.as_encoded()
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -232,8 +234,9 @@ pub fn open(
|
||||
/// Open the given theme from an absolute path. If there are any errors
|
||||
/// then messages will be appended to the given error list and None is
|
||||
/// returned.
|
||||
pub fn open_absolute(theme: &str, diags: &mut DiagnosticList) -> Option<File> {
|
||||
match File::open(config_str_to_path(theme)) {
|
||||
pub fn open_absolute(theme: &EncodedOs, diags: &mut DiagnosticList) -> Option<File> {
|
||||
let theme = theme.as_encoded();
|
||||
match File::open(EncodedOs::from_encoded(theme).to_path()) {
|
||||
Ok(f) => Some(f),
|
||||
Err(err) => {
|
||||
if err.kind() == std::io::ErrorKind::NotFound {
|
||||
@@ -257,10 +260,14 @@ pub fn open_absolute(theme: &str, diags: &mut DiagnosticList) -> Option<File> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn e(s: &str) -> EncodedOs {
|
||||
EncodedOs::from_encoded(s)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_path_separators_rejected() {
|
||||
let mut d = DiagnosticList::default();
|
||||
assert!(open("a/b", None, &mut d).is_none());
|
||||
assert!(open(&e("a/b"), None, &mut d).is_none());
|
||||
assert!(
|
||||
d.items()[0]
|
||||
.message
|
||||
@@ -271,13 +278,13 @@ mod tests {
|
||||
#[test]
|
||||
fn theme_absolute_missing_and_dir() {
|
||||
let mut d = DiagnosticList::default();
|
||||
assert!(open("/nonexistent/ghostty/theme", None, &mut d).is_none());
|
||||
assert!(open(&e("/nonexistent/ghostty/theme"), None, &mut d).is_none());
|
||||
assert_eq!(
|
||||
d.items()[0].message,
|
||||
"failed to load theme from the path \"/nonexistent/ghostty/theme\""
|
||||
);
|
||||
let mut d = DiagnosticList::default();
|
||||
assert!(open("/tmp", None, &mut d).is_none());
|
||||
assert!(open(&e("/tmp"), None, &mut d).is_none());
|
||||
assert_eq!(
|
||||
d.items()[0].message,
|
||||
"not reading theme from \"/tmp\": it is a directory"
|
||||
@@ -290,8 +297,8 @@ mod tests {
|
||||
std::fs::create_dir_all(dir.join("themes")).unwrap();
|
||||
std::fs::write(dir.join("themes/mytheme-zzz"), "background = #000000\n").unwrap();
|
||||
let mut d = DiagnosticList::default();
|
||||
let t = open("mytheme-zzz", Some(dir.to_str().unwrap()), &mut d).unwrap();
|
||||
assert!(t.path.ends_with("themes/mytheme-zzz"));
|
||||
let t = open(&e("mytheme-zzz"), Some(&EncodedOs::from_path(&dir)), &mut d).unwrap();
|
||||
assert!(t.path.ends_with_plain("themes/mytheme-zzz"));
|
||||
assert!(d.is_empty());
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
@@ -299,7 +306,7 @@ mod tests {
|
||||
#[test]
|
||||
fn theme_not_found_lists_tried_paths() {
|
||||
let mut d = DiagnosticList::default();
|
||||
assert!(open("nope-zzz", Some("/nonexistent-res"), &mut d).is_none());
|
||||
assert!(open(&e("nope-zzz"), Some(&e("/nonexistent-res")), &mut d).is_none());
|
||||
assert!(d.items().iter().any(|x| x.message
|
||||
== "theme \"nope-zzz\" not found, tried path \"/nonexistent-res/themes/nope-zzz\""));
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ pub enum WorkingDirectory {
|
||||
|
||||
/// Use an explicit working directory path. This may be not be
|
||||
/// expanded until finalize is called.
|
||||
Path(String),
|
||||
Path(ghostty_os::osbytes::EncodedOs),
|
||||
}
|
||||
|
||||
impl ConfigField for WorkingDirectory {
|
||||
@@ -169,7 +169,7 @@ impl ConfigField for WorkingDirectory {
|
||||
*self = match input {
|
||||
"home" => WorkingDirectory::Home,
|
||||
"inherit" => WorkingDirectory::Inherit,
|
||||
_ => WorkingDirectory::Path(input.to_string()),
|
||||
_ => WorkingDirectory::Path(ghostty_os::osbytes::EncodedOs::from_encoded(input)),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
@@ -178,7 +178,7 @@ impl ConfigField for WorkingDirectory {
|
||||
match self {
|
||||
WorkingDirectory::Home => write_str(f, "home"),
|
||||
WorkingDirectory::Inherit => write_str(f, "inherit"),
|
||||
WorkingDirectory::Path(p) => write_str(f, p),
|
||||
WorkingDirectory::Path(p) => write_str(f, p.as_encoded()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,7 +190,7 @@ impl WorkingDirectory {
|
||||
return;
|
||||
};
|
||||
|
||||
if !path.starts_with("~/") {
|
||||
if !path.starts_with_plain("~/") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -198,7 +198,8 @@ impl WorkingDirectory {
|
||||
Ok(e) => e,
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"error expanding home directory for working-directory path={path}: {err}"
|
||||
"error expanding home directory for working-directory path={}: {err}",
|
||||
path.as_encoded()
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -211,7 +212,7 @@ impl WorkingDirectory {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn value(&self) -> Option<&str> {
|
||||
pub fn value(&self) -> Option<&ghostty_os::osbytes::EncodedOs> {
|
||||
match self {
|
||||
WorkingDirectory::Path(p) => Some(p),
|
||||
WorkingDirectory::Home | WorkingDirectory::Inherit => None,
|
||||
@@ -1044,6 +1045,8 @@ impl ConfigField for RepeatableLink {
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use ghostty_os::osbytes::EncodedOs;
|
||||
|
||||
use super::*;
|
||||
use crate::cli_args::ConfigField;
|
||||
|
||||
@@ -1069,18 +1072,28 @@ mod tests {
|
||||
parse(&mut wd, "home").unwrap();
|
||||
assert_eq!(wd, WorkingDirectory::Home);
|
||||
parse(&mut wd, "~/projects/ghostty").unwrap();
|
||||
assert_eq!(wd, WorkingDirectory::Path("~/projects/ghostty".into()));
|
||||
assert_eq!(
|
||||
wd,
|
||||
WorkingDirectory::Path(ghostty_os::osbytes::EncodedOs::from_encoded(
|
||||
"~/projects/ghostty"
|
||||
))
|
||||
);
|
||||
parse(&mut wd, "\"/tmp path\"").unwrap();
|
||||
assert_eq!(wd, WorkingDirectory::Path("/tmp path".into()));
|
||||
assert_eq!(
|
||||
wd,
|
||||
WorkingDirectory::Path(ghostty_os::osbytes::EncodedOs::from_encoded("/tmp path"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn working_directory_finalize() {
|
||||
let mut wd = WorkingDirectory::Path("~/projects/ghostty".into());
|
||||
let mut wd = WorkingDirectory::Path(ghostty_os::osbytes::EncodedOs::from_encoded(
|
||||
"~/projects/ghostty",
|
||||
));
|
||||
wd.finalize();
|
||||
let expected = ghostty_os::expand_home("~/projects/ghostty")
|
||||
.unwrap_or_else(|_| "~/projects/ghostty".to_string());
|
||||
assert_eq!(wd.value().unwrap(), expected);
|
||||
let expected = ghostty_os::expand_home(&EncodedOs::from_encoded("~/projects/ghostty"))
|
||||
.unwrap_or_else(|_| EncodedOs::from_encoded("~/projects/ghostty"));
|
||||
assert_eq!(wd.value().unwrap(), &expected);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user