feat(macos): port Ghostty's macOS code paths (Metal, CoreText, Darwin)
Ports the macOS-specific Zig code behind cfg(target_os = "macos"): - renderer: a GraphicsApi trait with generic::Renderer<G>; Metal backend (all renderer/metal/* files, shaders.metal verbatim, runtime library compile, IOSurface layer, CVDisplayLink), shadertoy -> MSL via naga; OpenGL remains the backend elsewhere. - font: CoreText face, shaper and discovery with build.rs backend selection (coretext default; coretext-freetype / coretext-harfbuzz / coretext-noshape features), no fontconfig/freetype/harfbuzz on Apple. - input: KeymapDarwin via UCKeyTranslate, option-as-alt, mac keycodes. - os: macos, mach, cf_release_thread, NSProcessInfo args and macOS branches; config: macOS defaults, keybinds, Application Support paths. - termio/core: login(1) wrapping, openpty/execvpe, Darwin child exit handling, os_log logging. Compile-checked for aarch64/x86_64-apple-darwin (clippy -D warnings clean) using the cross setup in docs/rewrite/notes/macos-M1.md; cannot run here. The embedded runtime/C API (embedded.zig) is the next step. Also includes the cross-platform integration edits shared by the crash, verification, Windows and macOS work (Cargo target-gating, thread crash-context hooks, apprt-gtk surface/IME refactor, termio/os cfg gates), a cfg fix so ghostty-config builds on wasm32, and an allow for the intentional recursion in the crash overflow test. Test Plan: - cargo test --workspace (3224 passed) - cargo clippy --workspace --all-targets -- -D warnings - cargo check --target aarch64-apple-darwin -p ghostty-renderer (with the documented cross env)
This commit is contained in:
1 parent
3c8fae1089
commit
452b782149
124 files changed
+14936
-1242
No files matched your search
@@ -204,7 +204,13 @@ crate::config_struct! {
|
||||
/// On Linux with GTK, font size is scaled according to both display-wide and
|
||||
/// text-specific scaling factors, which are often managed by your desktop
|
||||
/// environment (e.g. the GNOME display scale and large text settings).
|
||||
"font-size" => font_size: f32 = 12.0,
|
||||
"font-size" => font_size: f32 = if cfg!(target_os = "macos") {
|
||||
// On macOS we default a little bigger since this tends to look
|
||||
// better. This is purely subjective but this is easy to modify.
|
||||
13.0
|
||||
} else {
|
||||
12.0
|
||||
},
|
||||
|
||||
/// A repeatable configuration to set one or more font variations values for
|
||||
/// a variable font. A variable font is a single font, usually with a filename
|
||||
@@ -339,7 +345,11 @@ crate::config_struct! {
|
||||
/// but without any of the darkening artifacts.
|
||||
///
|
||||
/// Available since: 1.1.0
|
||||
"alpha-blending" => alpha_blending: AlphaBlending = AlphaBlending::LinearCorrected,
|
||||
"alpha-blending" => alpha_blending: AlphaBlending = if cfg!(target_os = "macos") {
|
||||
AlphaBlending::Native
|
||||
} else {
|
||||
AlphaBlending::LinearCorrected
|
||||
},
|
||||
|
||||
/// All of the configurations behavior adjust various metrics determined by the
|
||||
/// font. The values can be integers (1, -1, etc.) or a percentage (20%, -15%,
|
||||
@@ -2353,7 +2363,11 @@ crate::config_struct! {
|
||||
/// paste is always enabled even if this is `false`.
|
||||
///
|
||||
/// The default value is true on Linux and macOS.
|
||||
"copy-on-select" => copy_on_select: CopyOnSelect = CopyOnSelect::True,
|
||||
"copy-on-select" => copy_on_select: CopyOnSelect = if cfg!(any(target_os = "linux", target_os = "macos")) {
|
||||
CopyOnSelect::True
|
||||
} else {
|
||||
CopyOnSelect::False
|
||||
},
|
||||
|
||||
/// The action to take when the user right-clicks on the terminal surface.
|
||||
///
|
||||
@@ -2433,7 +2447,7 @@ crate::config_struct! {
|
||||
/// On Linux, if this is `true`, Ghostty can delay quitting fully until a
|
||||
/// configurable amount of time has passed after the last window is closed.
|
||||
/// See the documentation of `quit-after-last-window-closed-delay`.
|
||||
"quit-after-last-window-closed" => quit_after_last_window_closed: bool = true,
|
||||
"quit-after-last-window-closed" => quit_after_last_window_closed: bool = cfg!(target_os = "linux"),
|
||||
|
||||
/// Controls how long Ghostty will stay running after the last open surface has
|
||||
/// been closed. This only has an effect if `quit-after-last-window-closed` is
|
||||
@@ -2649,7 +2663,7 @@ crate::config_struct! {
|
||||
/// accessible than on macOS, meaning that it is more preferable to keep the
|
||||
/// quick terminal open until the user has completed their task.
|
||||
/// This default may change in the future.
|
||||
"quick-terminal-autohide" => quick_terminal_autohide: bool = false,
|
||||
"quick-terminal-autohide" => quick_terminal_autohide: bool = cfg!(target_os = "macos"),
|
||||
|
||||
/// This configuration option determines the behavior of the quick terminal
|
||||
/// when switching between macOS spaces. macOS spaces are virtual desktops
|
||||
@@ -3430,7 +3444,11 @@ crate::config_struct! {
|
||||
/// * `always` - Always use cgroups.
|
||||
/// * `single-instance` - Enable cgroups only for Ghostty instances launched
|
||||
/// as single-instance applications (see gtk-single-instance).
|
||||
"linux-cgroup" => linux_cgroup: LinuxCgroup = LinuxCgroup::SingleInstance,
|
||||
"linux-cgroup" => linux_cgroup: LinuxCgroup = if cfg!(target_os = "linux") {
|
||||
LinuxCgroup::SingleInstance
|
||||
} else {
|
||||
LinuxCgroup::Never
|
||||
},
|
||||
|
||||
/// Memory limit for any individual terminal process (tab, split, window,
|
||||
/// etc.) in bytes. If this is unset then no memory limit will be set.
|
||||
@@ -3947,6 +3965,48 @@ impl Config {
|
||||
}
|
||||
};
|
||||
|
||||
// On macOS load the app support directory as well
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let legacy_app_support_path = crate::file_load::legacy_default_app_support_path()
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
let app_support_path = crate::file_load::preferred_app_support_path()
|
||||
.map_err(|e| Error::Other(e.to_string()))?;
|
||||
let app_support_loaded = {
|
||||
let legacy_app_support_action = self.load_optional_file(&legacy_app_support_path);
|
||||
|
||||
// The app support path and legacy may be the same, since we
|
||||
// use the `preferred` call above. If its the same, avoid
|
||||
// a double-load.
|
||||
let app_support_action = if legacy_app_support_path == app_support_path {
|
||||
OptionalFileAction::NotFound
|
||||
} else {
|
||||
self.load_optional_file(&app_support_path)
|
||||
};
|
||||
|
||||
if app_support_action != OptionalFileAction::NotFound
|
||||
&& legacy_app_support_action != OptionalFileAction::NotFound
|
||||
{
|
||||
log::warn!(
|
||||
"both config files `{legacy_app_support_path}` and `{app_support_path}` exist."
|
||||
);
|
||||
log::warn!("loading them both in that order");
|
||||
true
|
||||
} else {
|
||||
app_support_action != OptionalFileAction::NotFound
|
||||
|| legacy_app_support_action != OptionalFileAction::NotFound
|
||||
}
|
||||
};
|
||||
|
||||
// If both files are not found, then we create a template file.
|
||||
// For macOS, we only create the template file in the app support
|
||||
if !app_support_loaded && !xdg_loaded {
|
||||
if let Err(err) = Self::write_config_template(&app_support_path) {
|
||||
log::warn!("error creating template config file err={err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
if !xdg_loaded {
|
||||
if let Err(err) = Self::write_config_template(&xdg_path) {
|
||||
log::warn!("error creating template config file err={err}");
|
||||
@@ -3957,10 +4017,14 @@ impl Config {
|
||||
|
||||
/// Load and parse the CLI args.
|
||||
pub fn load_cli_args(&mut self) -> Result<(), Error> {
|
||||
let argv: Vec<std::ffi::OsString> = std::env::args_os().collect();
|
||||
// (On macOS this reads NSProcessInfo, libc argc/argv are not valid
|
||||
// for NSApplicationMain-based applications.)
|
||||
let argv: Vec<std::ffi::OsString> = ghostty_os::args::iterator().collect();
|
||||
|
||||
// Fast-path if we are Linux/BSD and have no args.
|
||||
if argv.len() <= 1 {
|
||||
// Fast-path if we are Linux/BSD and have no args. Everything else
|
||||
// (Windows, macOS) has to at least try because it may not use
|
||||
// std.os.argv.
|
||||
if cfg!(any(target_os = "linux", target_os = "freebsd")) && argv.len() <= 1 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -3975,19 +4039,9 @@ impl Config {
|
||||
// styling, etc. based on the command.
|
||||
//
|
||||
// See: https://github.com/Vladimir-csp/xdg-terminal-exec
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
{
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
#[cfg(not(unix))]
|
||||
trait OsStrExt {
|
||||
fn as_bytes(&self) -> &[u8];
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
impl OsStrExt for std::ffi::OsString {
|
||||
fn as_bytes(&self) -> &[u8] {
|
||||
self.as_encoded_bytes()
|
||||
}
|
||||
}
|
||||
let cargs: Vec<std::ffi::CString> = argv
|
||||
.iter()
|
||||
.filter_map(|a| std::ffi::CString::new(a.as_bytes()).ok())
|
||||
@@ -4533,7 +4587,31 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
// We need the passwd entry for the remainder
|
||||
self.platform_defaults(wd)
|
||||
}
|
||||
|
||||
/// Windows has no passwd database: default to cmd.exe and the
|
||||
/// %HOMEDRIVE%%HOMEPATH% home directory.
|
||||
#[cfg(windows)]
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn platform_defaults(&mut self, wd: &mut WorkingDirectory) -> Result<(), Error> {
|
||||
if self.command.is_none() {
|
||||
log::warn!("no default shell found, will default to using cmd");
|
||||
self.command = Some(Command::Shell("cmd.exe".to_owned()));
|
||||
}
|
||||
|
||||
if *wd == WorkingDirectory::Home {
|
||||
*wd = match ghostty_os::homedir::home() {
|
||||
Ok(Some(home)) => WorkingDirectory::Path(home),
|
||||
_ => WorkingDirectory::Inherit,
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The passwd-based part of [`Config::lookup_shell_defaults`].
|
||||
#[cfg(not(windows))]
|
||||
fn platform_defaults(&mut self, wd: &mut WorkingDirectory) -> Result<(), Error> {
|
||||
let pw = ghostty_os::passwd::get()?;
|
||||
if self.command.is_none() {
|
||||
if let Some(sh) = pw.shell {
|
||||
@@ -4645,6 +4723,21 @@ fn file_kind_name(meta: &std::fs::Metadata) -> &'static str {
|
||||
/// We should keep the set of behaviors that depend on this as small
|
||||
/// as possible because magic sucks, but each place is well documented.
|
||||
fn probable_cli_environment() -> bool {
|
||||
// Windows has its own problems, just ignore it for now since its not a
|
||||
// real supported target.
|
||||
if cfg!(windows) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// On macOS, we don't want to detect `open` calls as CLI envs.
|
||||
// Our desktop detection on macOS is very accurate due to how
|
||||
// processes are launched on macOS, so if we detect we're launched
|
||||
// from the app bundle then we're not in a CLI environment.
|
||||
#[cfg(target_os = "macos")]
|
||||
if ghostty_os::launched_from_desktop() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we have TERM_PROGRAM set to a non-empty value, we assume
|
||||
// a graphical terminal environment.
|
||||
if let Some(v) = std::env::var_os("TERM_PROGRAM") {
|
||||
|
||||
@@ -9,8 +9,8 @@ use crate::file_load;
|
||||
/// On Linux, this will use the file at the XDG config path. This is the
|
||||
/// only valid path for Linux so we don't need to check for other paths.
|
||||
///
|
||||
/// (On macOS both XDG and AppSupport paths are valid; macOS is not
|
||||
/// supported by this port.)
|
||||
/// On macOS, both XDG and AppSupport paths are valid; the AppSupport paths
|
||||
/// come first.
|
||||
///
|
||||
/// The config file (and its directory) is created if it doesn't exist.
|
||||
pub fn open_path() -> io::Result<String> {
|
||||
@@ -80,8 +80,15 @@ 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>> {
|
||||
Ok(vec![
|
||||
file_load::default_xdg_path().map_err(xdg_err)?,
|
||||
file_load::legacy_default_xdg_path().map_err(xdg_err)?,
|
||||
])
|
||||
let mut paths = Vec::with_capacity(4);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
paths.push(file_load::default_app_support_path().map_err(xdg_err)?);
|
||||
paths.push(file_load::legacy_default_app_support_path().map_err(xdg_err)?);
|
||||
}
|
||||
|
||||
paths.push(file_load::default_xdg_path().map_err(xdg_err)?);
|
||||
paths.push(file_load::legacy_default_xdg_path().map_err(xdg_err)?);
|
||||
Ok(paths)
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
//! Default config file locations and opening (port of `config/file_load.zig`).
|
||||
//!
|
||||
//! macOS Application Support paths are not supported (Linux only).
|
||||
//! On macOS the Application Support paths are used in addition to XDG; the
|
||||
//! helpers that only need `open` + two candidate paths are platform
|
||||
//! independent (and unit tested everywhere), the Application Support lookup
|
||||
//! itself uses `NSFileManager` and is macOS-only.
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
@@ -40,13 +43,94 @@ pub fn preferred_xdg_path() -> Result<String, xdg::Error> {
|
||||
Ok(xdg_path)
|
||||
}
|
||||
|
||||
/// Default path for the macOS Application Support configuration file.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn default_app_support_path() -> Result<String, 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> {
|
||||
app_support_dir("config")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn app_support_dir(sub_path: &str) -> Result<String, xdg::Error> {
|
||||
ghostty_os::macos::app_support_dir(sub_path)
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.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> {
|
||||
Ok(preferred_of(
|
||||
default_app_support_path()?,
|
||||
legacy_default_app_support_path()?,
|
||||
))
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
// If the new path exists, use that.
|
||||
if open(&new).is_ok() {
|
||||
return new;
|
||||
}
|
||||
|
||||
// Try the legacy path
|
||||
if open(&legacy).is_ok() {
|
||||
return legacy;
|
||||
}
|
||||
|
||||
// Legacy path and new path both don't exist. Return the new one.
|
||||
new
|
||||
}
|
||||
|
||||
/// macOS: prefer the Application Support file if it exists, else the XDG
|
||||
/// 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> {
|
||||
// macOS prefers the Application Support directory if it exists.
|
||||
if open(&app_support_path).is_ok() {
|
||||
return Ok(app_support_path);
|
||||
}
|
||||
|
||||
// Try the XDG path if it exists
|
||||
let xdg_path = xdg_path()?;
|
||||
if open(&xdg_path).is_ok() {
|
||||
return Ok(xdg_path);
|
||||
}
|
||||
|
||||
// If neither file exists, use app support
|
||||
Ok(app_support_path)
|
||||
}
|
||||
|
||||
/// Returns the path to the preferred default configuration file.
|
||||
/// This is the file where users should place their configuration.
|
||||
///
|
||||
/// 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> {
|
||||
// All supported platforms use XDG only
|
||||
preferred_macos(preferred_app_support_path()?, &preferred_xdg_path)
|
||||
}
|
||||
|
||||
/// Returns the path to the preferred default configuration file.
|
||||
/// This is the file where users should place their configuration.
|
||||
///
|
||||
/// 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> {
|
||||
// All other platforms use XDG only
|
||||
preferred_xdg_path()
|
||||
}
|
||||
|
||||
@@ -123,6 +207,47 @@ mod tests {
|
||||
std::fs::remove_file(&p).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
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);
|
||||
|
||||
// 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();
|
||||
assert_eq!(preferred_of(new.clone(), legacy.clone()), legacy);
|
||||
// Both exist: the new one.
|
||||
std::fs::write(&new, "a=b").unwrap();
|
||||
assert_eq!(preferred_of(new.clone(), legacy.clone()), new);
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
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 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();
|
||||
assert_eq!(preferred_macos(app.clone(), &xdg_fn).unwrap(), xdg_p);
|
||||
// App support exists: app support wins.
|
||||
std::fs::write(&app, "a=b").unwrap();
|
||||
assert_eq!(preferred_macos(app.clone(), &xdg_fn).unwrap(), app);
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_paths_end_with_expected_names() {
|
||||
if let (Ok(a), Ok(b)) = (default_xdg_path(), legacy_default_xdg_path()) {
|
||||
|
||||
@@ -12,6 +12,7 @@ use ghostty_input::{
|
||||
CopyToClipboard,
|
||||
Flags,
|
||||
InspectorMode,
|
||||
NavigateSearch,
|
||||
Set,
|
||||
SplitDirection,
|
||||
SplitFocusDirection,
|
||||
@@ -22,7 +23,6 @@ use ghostty_input::{
|
||||
WriteScreen,
|
||||
set::{EntryFormatter as BindingEntryFormatter, Value},
|
||||
},
|
||||
ctrl_or_super,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -93,23 +93,32 @@ fn to_arg_error(e: ghostty_input::binding::Error) -> ArgError {
|
||||
}
|
||||
|
||||
impl Keybinds {
|
||||
/// Reset to the default keybinds (Zig: `init`). Linux/GTK variant.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
/// Reset to the default keybinds (Zig: `init`) for the build target.
|
||||
pub fn init(&mut self) {
|
||||
self.init_for(ghostty_input::platform::is_darwin());
|
||||
}
|
||||
|
||||
/// Reset to the default keybinds for Darwin (`true`) or non-Darwin
|
||||
/// (`false`). Zig selects with `builtin.target.os.tag.isDarwin()` at
|
||||
/// compile time; the parameter lets both variants be tested anywhere.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn init_for(&mut self, darwin: bool) {
|
||||
self.set = Set::default();
|
||||
self.tables = Vec::new();
|
||||
self.chain_target = ChainTarget::Root;
|
||||
|
||||
let none = Mods::default();
|
||||
let cs = mods(true, true, false, false);
|
||||
// `inputpkg.ctrlOrSuper` for the requested platform.
|
||||
let cos = |m: Mods| ghostty_input::key::ctrl_or_super_for(m, darwin);
|
||||
let set = &mut self.set;
|
||||
|
||||
// keybinds for opening and reloading config
|
||||
set.put(
|
||||
uni(',', ctrl_or_super(mods(false, true, false, false))),
|
||||
uni(',', cos(mods(false, true, false, false))),
|
||||
Action::ReloadConfig,
|
||||
);
|
||||
set.put(uni(',', ctrl_or_super(none)), Action::OpenConfig);
|
||||
set.put(uni(',', cos(none)), Action::OpenConfig);
|
||||
|
||||
{
|
||||
set.put(
|
||||
@@ -124,44 +133,51 @@ impl Keybinds {
|
||||
// The order of these blocks is important. The *last* added keybind for a given action is
|
||||
// what will display in the menu. We want the more typical keybinds after this block to be
|
||||
// the standard
|
||||
set.put(
|
||||
phys(Key::Insert, mods(true, false, false, false)),
|
||||
Action::CopyToClipboard(CopyToClipboard::Mixed),
|
||||
);
|
||||
set.put(
|
||||
phys(Key::Insert, mods(false, true, false, false)),
|
||||
Action::PasteFromClipboard,
|
||||
);
|
||||
if !darwin {
|
||||
set.put(
|
||||
phys(Key::Insert, mods(true, false, false, false)),
|
||||
Action::CopyToClipboard(CopyToClipboard::Mixed),
|
||||
);
|
||||
set.put(
|
||||
phys(Key::Insert, mods(false, true, false, false)),
|
||||
Action::PasteFromClipboard,
|
||||
);
|
||||
}
|
||||
|
||||
// On macOS we default to super but Linux ctrl+shift since
|
||||
// ctrl+c is to kill the process.
|
||||
let m = if darwin {
|
||||
mods(false, false, false, true)
|
||||
} else {
|
||||
cs
|
||||
};
|
||||
set.put_flags(
|
||||
uni('c', cs),
|
||||
uni('c', m),
|
||||
Action::CopyToClipboard(CopyToClipboard::Mixed),
|
||||
performable(),
|
||||
);
|
||||
set.put_flags(uni('v', cs), Action::PasteFromClipboard, performable());
|
||||
set.put_flags(uni('v', m), Action::PasteFromClipboard, performable());
|
||||
}
|
||||
|
||||
// Increase font size mapping for keyboards with dedicated plus keys (like german)
|
||||
// Note: this order matters below because the C API will only return
|
||||
// the last keybinding for a given action. The macOS app uses this to
|
||||
// set the expected keybind for the menu.
|
||||
set.put(uni('=', ctrl_or_super(none)), Action::IncreaseFontSize(1.0));
|
||||
set.put(uni('+', ctrl_or_super(none)), Action::IncreaseFontSize(1.0));
|
||||
set.put(uni('-', ctrl_or_super(none)), Action::DecreaseFontSize(1.0));
|
||||
set.put(uni('0', ctrl_or_super(none)), Action::ResetFontSize);
|
||||
set.put(uni('=', cos(none)), Action::IncreaseFontSize(1.0));
|
||||
set.put(uni('+', cos(none)), Action::IncreaseFontSize(1.0));
|
||||
set.put(uni('-', cos(none)), Action::DecreaseFontSize(1.0));
|
||||
set.put(uni('0', cos(none)), Action::ResetFontSize);
|
||||
|
||||
set.put(
|
||||
uni('j', mods(true, true, false, true)),
|
||||
Action::WriteScreenFile(WriteScreen::COPY),
|
||||
);
|
||||
set.put(
|
||||
uni('j', ctrl_or_super(mods(false, true, false, false))),
|
||||
uni('j', cos(mods(false, true, false, false))),
|
||||
Action::WriteScreenFile(WriteScreen::PASTE),
|
||||
);
|
||||
set.put(
|
||||
uni('j', ctrl_or_super(mods(false, true, true, false))),
|
||||
uni('j', cos(mods(false, true, true, false))),
|
||||
Action::WriteScreenFile(WriteScreen::OPEN),
|
||||
);
|
||||
|
||||
@@ -181,92 +197,94 @@ impl Keybinds {
|
||||
}
|
||||
|
||||
// Tabs common to all platforms
|
||||
let ctrl = mods(true, false, false, false);
|
||||
set.put(phys(Key::Tab, cs), Action::PreviousTab);
|
||||
set.put(
|
||||
phys(Key::Tab, mods(true, false, false, false)),
|
||||
Action::NextTab,
|
||||
);
|
||||
set.put(phys(Key::Tab, ctrl), Action::NextTab);
|
||||
|
||||
// Windowing
|
||||
set.put(uni('n', cs), Action::NewWindow);
|
||||
set.put(uni('w', cs), Action::CloseSurface);
|
||||
set.put(uni('q', cs), Action::Quit);
|
||||
set.put(
|
||||
phys(Key::F4, mods(false, false, true, false)),
|
||||
Action::CloseWindow,
|
||||
);
|
||||
set.put(uni('t', cs), Action::NewTab);
|
||||
set.put(uni('w', cs), Action::CloseTab(CloseTabMode::This));
|
||||
set.put_flags(phys(Key::ArrowLeft, cs), Action::PreviousTab, performable());
|
||||
set.put_flags(phys(Key::ArrowRight, cs), Action::NextTab, performable());
|
||||
let ctrl = mods(true, false, false, false);
|
||||
set.put_flags(phys(Key::PageUp, ctrl), Action::PreviousTab, performable());
|
||||
set.put_flags(phys(Key::PageDown, ctrl), Action::NextTab, performable());
|
||||
set.put(uni('o', cs), Action::NewSplit(SplitDirection::Right));
|
||||
set.put(uni('e', cs), Action::NewSplit(SplitDirection::Down));
|
||||
let ctrl_super = mods(true, false, false, true);
|
||||
set.put_flags(
|
||||
uni('[', ctrl_super),
|
||||
Action::GotoSplit(SplitFocusDirection::Previous),
|
||||
performable(),
|
||||
);
|
||||
set.put_flags(
|
||||
uni(']', ctrl_super),
|
||||
Action::GotoSplit(SplitFocusDirection::Next),
|
||||
performable(),
|
||||
);
|
||||
let ctrl_alt = mods(true, false, true, false);
|
||||
for (k, d) in [
|
||||
(Key::ArrowUp, SplitFocusDirection::Up),
|
||||
(Key::ArrowDown, SplitFocusDirection::Down),
|
||||
(Key::ArrowLeft, SplitFocusDirection::Left),
|
||||
(Key::ArrowRight, SplitFocusDirection::Right),
|
||||
] {
|
||||
set.put_flags(phys(k, ctrl_alt), Action::GotoSplit(d), performable());
|
||||
}
|
||||
|
||||
// Resizing splits
|
||||
let super_ctrl_shift = mods(true, true, false, true);
|
||||
for (k, d) in [
|
||||
(Key::ArrowUp, SplitResizeDirection::Up),
|
||||
(Key::ArrowDown, SplitResizeDirection::Down),
|
||||
(Key::ArrowLeft, SplitResizeDirection::Left),
|
||||
(Key::ArrowRight, SplitResizeDirection::Right),
|
||||
] {
|
||||
if !darwin {
|
||||
set.put(uni('n', cs), Action::NewWindow);
|
||||
set.put(uni('w', cs), Action::CloseSurface);
|
||||
set.put(uni('q', cs), Action::Quit);
|
||||
set.put(
|
||||
phys(Key::F4, mods(false, false, true, false)),
|
||||
Action::CloseWindow,
|
||||
);
|
||||
set.put(uni('t', cs), Action::NewTab);
|
||||
set.put(uni('w', cs), Action::CloseTab(CloseTabMode::This));
|
||||
set.put_flags(phys(Key::ArrowLeft, cs), Action::PreviousTab, performable());
|
||||
set.put_flags(phys(Key::ArrowRight, cs), Action::NextTab, performable());
|
||||
set.put_flags(phys(Key::PageUp, ctrl), Action::PreviousTab, performable());
|
||||
set.put_flags(phys(Key::PageDown, ctrl), Action::NextTab, performable());
|
||||
set.put(uni('o', cs), Action::NewSplit(SplitDirection::Right));
|
||||
set.put(uni('e', cs), Action::NewSplit(SplitDirection::Down));
|
||||
let ctrl_super = mods(true, false, false, true);
|
||||
set.put_flags(
|
||||
phys(k, super_ctrl_shift),
|
||||
Action::ResizeSplit(SplitResizeParameter(d, 10)),
|
||||
uni('[', ctrl_super),
|
||||
Action::GotoSplit(SplitFocusDirection::Previous),
|
||||
performable(),
|
||||
);
|
||||
set.put_flags(
|
||||
uni(']', ctrl_super),
|
||||
Action::GotoSplit(SplitFocusDirection::Next),
|
||||
performable(),
|
||||
);
|
||||
let ctrl_alt = mods(true, false, true, false);
|
||||
for (k, d) in [
|
||||
(Key::ArrowUp, SplitFocusDirection::Up),
|
||||
(Key::ArrowDown, SplitFocusDirection::Down),
|
||||
(Key::ArrowLeft, SplitFocusDirection::Left),
|
||||
(Key::ArrowRight, SplitFocusDirection::Right),
|
||||
] {
|
||||
set.put_flags(phys(k, ctrl_alt), Action::GotoSplit(d), performable());
|
||||
}
|
||||
|
||||
// Resizing splits
|
||||
let super_ctrl_shift = mods(true, true, false, true);
|
||||
for (k, d) in [
|
||||
(Key::ArrowUp, SplitResizeDirection::Up),
|
||||
(Key::ArrowDown, SplitResizeDirection::Down),
|
||||
(Key::ArrowLeft, SplitResizeDirection::Left),
|
||||
(Key::ArrowRight, SplitResizeDirection::Right),
|
||||
] {
|
||||
set.put_flags(
|
||||
phys(k, super_ctrl_shift),
|
||||
Action::ResizeSplit(SplitResizeParameter(d, 10)),
|
||||
performable(),
|
||||
);
|
||||
}
|
||||
|
||||
// Viewport scrolling
|
||||
set.put(phys(Key::Home, shift), Action::ScrollToTop);
|
||||
set.put(phys(Key::End, shift), Action::ScrollToBottom);
|
||||
set.put(phys(Key::PageUp, shift), Action::ScrollPageUp);
|
||||
set.put(phys(Key::PageDown, shift), Action::ScrollPageDown);
|
||||
|
||||
// Semantic prompts
|
||||
set.put(phys(Key::PageUp, cs), Action::JumpToPrompt(-1));
|
||||
set.put(phys(Key::PageDown, cs), Action::JumpToPrompt(1));
|
||||
|
||||
// Search
|
||||
set.put_flags(uni('f', cs), Action::StartSearch, performable());
|
||||
set.put_flags(phys(Key::Escape, none), Action::EndSearch, performable());
|
||||
|
||||
// Inspector, matching Chromium
|
||||
set.put(uni('i', cs), Action::Inspector(InspectorMode::Toggle));
|
||||
|
||||
// Terminal
|
||||
set.put(uni('a', cs), Action::SelectAll);
|
||||
|
||||
// Selection clipboard paste
|
||||
set.put(phys(Key::Insert, shift), Action::PasteFromSelection);
|
||||
}
|
||||
|
||||
// Viewport scrolling
|
||||
set.put(phys(Key::Home, shift), Action::ScrollToTop);
|
||||
set.put(phys(Key::End, shift), Action::ScrollToBottom);
|
||||
set.put(phys(Key::PageUp, shift), Action::ScrollPageUp);
|
||||
set.put(phys(Key::PageDown, shift), Action::ScrollPageDown);
|
||||
|
||||
// Semantic prompts
|
||||
set.put(phys(Key::PageUp, cs), Action::JumpToPrompt(-1));
|
||||
set.put(phys(Key::PageDown, cs), Action::JumpToPrompt(1));
|
||||
|
||||
// Search
|
||||
set.put_flags(uni('f', cs), Action::StartSearch, performable());
|
||||
set.put_flags(phys(Key::Escape, none), Action::EndSearch, performable());
|
||||
|
||||
// Inspector, matching Chromium
|
||||
set.put(uni('i', cs), Action::Inspector(InspectorMode::Toggle));
|
||||
|
||||
// Terminal
|
||||
set.put(uni('a', cs), Action::SelectAll);
|
||||
|
||||
// Selection clipboard paste
|
||||
set.put(phys(Key::Insert, shift), Action::PasteFromSelection);
|
||||
|
||||
{
|
||||
// On macOS we default to super but everywhere else
|
||||
// is alt.
|
||||
let m = mods(false, false, true, false);
|
||||
let m = if darwin {
|
||||
mods(false, false, false, true)
|
||||
} else {
|
||||
mods(false, false, true, false)
|
||||
};
|
||||
|
||||
// Cmd/Alt+N for goto tab N
|
||||
let digits = [
|
||||
@@ -279,6 +297,14 @@ impl Keybinds {
|
||||
Key::Digit7,
|
||||
Key::Digit8,
|
||||
];
|
||||
// On macOS we keep these not performable so that the keyboard
|
||||
// shortcuts in tabs work. In the future the correct fix is to
|
||||
// fix the reverse mapping lookup to allow us to lookup
|
||||
// performable keybinds conditionally.
|
||||
let tab_flags = Flags {
|
||||
performable: !darwin,
|
||||
..Flags::default()
|
||||
};
|
||||
for (i, key) in digits.into_iter().enumerate() {
|
||||
let n = i + 1;
|
||||
let c = char::from(b'0' + n as u8);
|
||||
@@ -287,30 +313,149 @@ impl Keybinds {
|
||||
// this shortcut. Namely, AZERTY doesn't produce unicode `N`
|
||||
// for their digit keys (they're on shifted keys on the same
|
||||
// physical keys).
|
||||
set.put_flags(phys(key, m), Action::GotoTab(n), performable());
|
||||
set.put_flags(phys(key, m), Action::GotoTab(n), tab_flags);
|
||||
// Important: this must be the LAST binding set so that the
|
||||
// libghostty trigger API returns this one for the action,
|
||||
// so that things like the macOS tab bar key equivalent label
|
||||
// work properly.
|
||||
set.put_flags(uni(c, m), Action::GotoTab(n), performable());
|
||||
set.put_flags(uni(c, m), Action::GotoTab(n), tab_flags);
|
||||
}
|
||||
set.put_flags(uni('9', m), Action::LastTab, performable());
|
||||
// See comment above with the numeric goto_tab
|
||||
set.put_flags(uni('9', m), Action::LastTab, tab_flags);
|
||||
}
|
||||
|
||||
// Toggle fullscreen
|
||||
set.put(
|
||||
phys(Key::Enter, ctrl_or_super(none)),
|
||||
Action::ToggleFullscreen,
|
||||
);
|
||||
set.put(phys(Key::Enter, cos(none)), Action::ToggleFullscreen);
|
||||
|
||||
// Toggle zoom a split
|
||||
set.put(
|
||||
phys(Key::Enter, ctrl_or_super(shift)),
|
||||
Action::ToggleSplitZoom,
|
||||
);
|
||||
set.put(phys(Key::Enter, cos(shift)), Action::ToggleSplitZoom);
|
||||
|
||||
// Toggle command palette, matches VSCode
|
||||
set.put(uni('p', ctrl_or_super(shift)), Action::ToggleCommandPalette);
|
||||
set.put(uni('p', cos(shift)), Action::ToggleCommandPalette);
|
||||
|
||||
// Mac-specific keyboard bindings.
|
||||
if darwin {
|
||||
Self::init_mac_bindings(set);
|
||||
}
|
||||
}
|
||||
|
||||
/// The Mac-specific default keybinds (tail of Zig `Keybinds.init`).
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn init_mac_bindings(set: &mut Set) {
|
||||
let sup = mods(false, false, false, true);
|
||||
let sup_shift = mods(false, true, false, true);
|
||||
let sup_alt = mods(false, false, true, true);
|
||||
let sup_ctrl = mods(true, false, false, true);
|
||||
|
||||
set.put(uni('q', sup), Action::Quit);
|
||||
set.put_flags(uni('k', sup), Action::ClearScreen, performable());
|
||||
set.put(uni('a', sup), Action::SelectAll);
|
||||
|
||||
// Undo/redo
|
||||
set.put_flags(uni('t', sup_shift), Action::Undo, performable());
|
||||
set.put_flags(uni('z', sup), Action::Undo, performable());
|
||||
set.put_flags(uni('z', sup_shift), Action::Redo, performable());
|
||||
|
||||
// Viewport scrolling
|
||||
set.put(phys(Key::Home, sup), Action::ScrollToTop);
|
||||
set.put(phys(Key::End, sup), Action::ScrollToBottom);
|
||||
set.put(phys(Key::PageUp, sup), Action::ScrollPageUp);
|
||||
set.put(phys(Key::PageDown, sup), Action::ScrollPageDown);
|
||||
set.put_flags(uni('j', sup), Action::ScrollToSelection, performable());
|
||||
|
||||
// Semantic prompts
|
||||
set.put(phys(Key::ArrowUp, sup_shift), Action::JumpToPrompt(-1));
|
||||
set.put(phys(Key::ArrowDown, sup_shift), Action::JumpToPrompt(1));
|
||||
|
||||
// Mac windowing
|
||||
set.put(uni('n', sup), Action::NewWindow);
|
||||
set.put(uni('w', sup), Action::CloseSurface);
|
||||
set.put(uni('w', sup_alt), Action::CloseTab(CloseTabMode::This));
|
||||
set.put(uni('w', sup_shift), Action::CloseWindow);
|
||||
set.put(
|
||||
uni('w', mods(false, true, true, true)),
|
||||
Action::CloseAllWindows,
|
||||
);
|
||||
set.put(uni('t', sup), Action::NewTab);
|
||||
set.put(uni('[', sup_shift), Action::PreviousTab);
|
||||
set.put(uni(']', sup_shift), Action::NextTab);
|
||||
set.put(uni('d', sup), Action::NewSplit(SplitDirection::Right));
|
||||
set.put(uni('d', sup_shift), Action::NewSplit(SplitDirection::Down));
|
||||
set.put(
|
||||
uni('[', sup),
|
||||
Action::GotoSplit(SplitFocusDirection::Previous),
|
||||
);
|
||||
set.put(uni(']', sup), Action::GotoSplit(SplitFocusDirection::Next));
|
||||
for (k, d) in [
|
||||
(Key::ArrowUp, SplitFocusDirection::Up),
|
||||
(Key::ArrowDown, SplitFocusDirection::Down),
|
||||
(Key::ArrowLeft, SplitFocusDirection::Left),
|
||||
(Key::ArrowRight, SplitFocusDirection::Right),
|
||||
] {
|
||||
set.put(phys(k, sup_alt), Action::GotoSplit(d));
|
||||
}
|
||||
for (k, d) in [
|
||||
(Key::ArrowUp, SplitResizeDirection::Up),
|
||||
(Key::ArrowDown, SplitResizeDirection::Down),
|
||||
(Key::ArrowLeft, SplitResizeDirection::Left),
|
||||
(Key::ArrowRight, SplitResizeDirection::Right),
|
||||
] {
|
||||
set.put(
|
||||
phys(k, sup_ctrl),
|
||||
Action::ResizeSplit(SplitResizeParameter(d, 10)),
|
||||
);
|
||||
}
|
||||
set.put(uni('=', sup_ctrl), Action::EqualizeSplits);
|
||||
|
||||
// Jump to prompt, matches Terminal.app
|
||||
set.put(phys(Key::ArrowUp, sup), Action::JumpToPrompt(-1));
|
||||
set.put(phys(Key::ArrowDown, sup), Action::JumpToPrompt(1));
|
||||
|
||||
// Search
|
||||
set.put_flags(uni('f', sup), Action::StartSearch, performable());
|
||||
set.put_flags(uni('e', sup), Action::SearchSelection, performable());
|
||||
set.put_flags(uni('f', sup_shift), Action::EndSearch, performable());
|
||||
set.put_flags(
|
||||
phys(Key::Escape, Mods::default()),
|
||||
Action::EndSearch,
|
||||
performable(),
|
||||
);
|
||||
set.put_flags(
|
||||
uni('g', sup),
|
||||
Action::NavigateSearch(NavigateSearch::Next),
|
||||
performable(),
|
||||
);
|
||||
set.put_flags(
|
||||
uni('g', sup_shift),
|
||||
Action::NavigateSearch(NavigateSearch::Previous),
|
||||
performable(),
|
||||
);
|
||||
|
||||
// Inspector, matching Chromium
|
||||
set.put(uni('i', sup_alt), Action::Inspector(InspectorMode::Toggle));
|
||||
|
||||
// Alternate keybind, common to Mac programs
|
||||
set.put(uni('f', sup_ctrl), Action::ToggleFullscreen);
|
||||
|
||||
// Selection clipboard paste, matches Terminal.app
|
||||
set.put(uni('v', sup_shift), Action::PasteFromSelection);
|
||||
|
||||
// "Natural text editing" keybinds. This forces these keys to go back
|
||||
// to legacy encoding (not fixterms). It seems macOS users more than
|
||||
// others are used to these keys so we set them as defaults. If
|
||||
// people want to get back to the fixterm encoding they can set
|
||||
// the keybinds to `unbind`.
|
||||
set.put(phys(Key::ArrowRight, sup), Action::Text("\\x05".to_owned()));
|
||||
set.put(phys(Key::ArrowLeft, sup), Action::Text("\\x01".to_owned()));
|
||||
set.put(phys(Key::Backspace, sup), Action::Text("\\x15".to_owned()));
|
||||
set.put(
|
||||
phys(Key::ArrowLeft, mods(false, false, true, false)),
|
||||
Action::Esc("b".to_owned()),
|
||||
);
|
||||
set.put(
|
||||
phys(Key::ArrowRight, mods(false, false, true, false)),
|
||||
Action::Esc("f".to_owned()),
|
||||
);
|
||||
}
|
||||
|
||||
fn table_index(&self, name: &str) -> Option<usize> {
|
||||
@@ -850,4 +995,75 @@ mod tests {
|
||||
}
|
||||
assert_eq!(kb.set.bindings.len(), again.set.bindings.len());
|
||||
}
|
||||
|
||||
fn lookup(kb: &Keybinds, trigger: Trigger) -> Option<&Value> {
|
||||
kb.set.get(trigger)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_keybinds_darwin_use_super() {
|
||||
let mut kb = Keybinds::default();
|
||||
kb.init_for(true);
|
||||
let sup = mods(false, false, false, true);
|
||||
|
||||
// cmd+c copies (performable), cmd+q quits, cmd+t opens a tab.
|
||||
for (c, want) in [
|
||||
('c', Action::CopyToClipboard(CopyToClipboard::Mixed)),
|
||||
('q', Action::Quit),
|
||||
('t', Action::NewTab),
|
||||
('1', Action::GotoTab(1)),
|
||||
] {
|
||||
let Some(Value::Leaf(l)) = lookup(&kb, uni(c, sup)) else {
|
||||
panic!("missing super+{c}");
|
||||
};
|
||||
assert_eq!(l.action, want);
|
||||
}
|
||||
|
||||
// tab shortcuts are NOT performable on macOS.
|
||||
let Some(Value::Leaf(l)) = lookup(&kb, uni('1', sup)) else {
|
||||
panic!()
|
||||
};
|
||||
assert!(!l.flags.performable);
|
||||
let Some(Value::Leaf(l)) = lookup(&kb, uni('c', sup)) else {
|
||||
panic!()
|
||||
};
|
||||
assert!(l.flags.performable);
|
||||
|
||||
// No ctrl+shift+n window binding on macOS, no ctrl+insert copy.
|
||||
assert!(lookup(&kb, uni('n', mods(true, true, false, false))).is_none());
|
||||
assert!(lookup(&kb, phys(Key::Insert, mods(true, false, false, false))).is_none());
|
||||
|
||||
// Natural text editing.
|
||||
let Some(Value::Leaf(l)) = lookup(&kb, phys(Key::ArrowLeft, sup)) else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(l.action, Action::Text("\\x01".to_owned()));
|
||||
|
||||
// ctrlOrSuper picks super: super+comma opens the config.
|
||||
let Some(Value::Leaf(l)) = lookup(&kb, uni(',', sup)) else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(l.action, Action::OpenConfig);
|
||||
|
||||
// Formatted defaults re-parse to the same number of bindings.
|
||||
let out = fmt_kb("keybind", &kb);
|
||||
let mut again = Keybinds::default();
|
||||
for line in out.lines() {
|
||||
let v = line.strip_prefix("keybind = ").unwrap();
|
||||
again.parse_cli(Some(v)).unwrap();
|
||||
}
|
||||
assert_eq!(kb.set.bindings.len(), again.set.bindings.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_keybinds_non_darwin_use_ctrl_shift() {
|
||||
let mut kb = Keybinds::default();
|
||||
kb.init_for(false);
|
||||
let cs = mods(true, true, false, false);
|
||||
let Some(Value::Leaf(l)) = lookup(&kb, uni('c', cs)) else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(l.action, Action::CopyToClipboard(CopyToClipboard::Mixed));
|
||||
assert!(lookup(&kb, uni('q', mods(false, false, false, true))).is_none());
|
||||
}
|
||||
}
|
||||
@@ -120,7 +120,8 @@ impl Path {
|
||||
// Check if the path starts with a tilde and expand it to the
|
||||
// home directory. We explicitly look for "~/" because we don't
|
||||
// support alternate users such as "~alice/"
|
||||
if path.starts_with("~/") {
|
||||
// (Windows isn't supported yet: Zig skips this there.)
|
||||
if !cfg!(windows) && path.starts_with("~/") {
|
||||
match ghostty_os::expand_home(&path) {
|
||||
Ok(expanded) => {
|
||||
log::debug!("expanding file path from home directory: path={expanded}");
|
||||
|
||||
Reference in new issue
Block a user