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
76771bed0d
commit
bab49ee596
124 files changed
+14936
-1242
No files matched your search
@@ -15,11 +15,18 @@ use ghostty_os::{file::Rlimit, resourcesdir::ResourcesDir};
|
||||
pub struct Logging {
|
||||
/// Whether to log to stderr.
|
||||
pub stderr: bool,
|
||||
|
||||
/// Whether to log to macOS's unified logging. Enabled by default on
|
||||
/// macOS.
|
||||
pub macos: bool,
|
||||
}
|
||||
|
||||
impl Default for Logging {
|
||||
fn default() -> Self {
|
||||
Logging { stderr: true }
|
||||
Logging {
|
||||
stderr: true,
|
||||
macos: cfg!(target_vendor = "apple"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +39,18 @@ impl Logging {
|
||||
#[must_use]
|
||||
pub fn parse(v: &str) -> Logging {
|
||||
match v {
|
||||
"1" | "t" | "T" | "true" => return Logging { stderr: true },
|
||||
"0" | "f" | "F" | "false" => return Logging { stderr: false },
|
||||
"1" | "t" | "T" | "true" => {
|
||||
return Logging {
|
||||
stderr: true,
|
||||
macos: true,
|
||||
};
|
||||
}
|
||||
"0" | "f" | "F" | "false" => {
|
||||
return Logging {
|
||||
stderr: false,
|
||||
macos: false,
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let mut out = Logging::default();
|
||||
@@ -45,8 +62,7 @@ impl Logging {
|
||||
};
|
||||
match name {
|
||||
"stderr" => out.stderr = value,
|
||||
// The only other field (macOS unified logging) is a no-op.
|
||||
"macos" => {}
|
||||
"macos" => out.macos = value,
|
||||
_ => return Logging::default(),
|
||||
}
|
||||
}
|
||||
@@ -141,6 +157,14 @@ pub fn init(opts: InitOptions) -> &'static GlobalState {
|
||||
// As early as possible, initialize our resource limits.
|
||||
let rlimits = ResourceLimits::init();
|
||||
|
||||
// Initialize our crash reporting.
|
||||
if let Err(err) = ghostty_crash::init(&ghostty_crash::Options {
|
||||
release: ghostty_termio::stream_handler::VERSION_STRING.to_owned(),
|
||||
..ghostty_crash::Options::default()
|
||||
}) {
|
||||
log::warn!("sentry init failed, no crash capture available err={err}");
|
||||
}
|
||||
|
||||
// Find our resources directory once for the app so every launch
|
||||
// hereafter can use this cached value.
|
||||
let discover = opts
|
||||
@@ -173,12 +197,60 @@ pub fn init(opts: InitOptions) -> &'static GlobalState {
|
||||
/// `info(scope): message` on stderr.
|
||||
struct Logger {
|
||||
stderr: bool,
|
||||
/// macOS unified logging (`os_log`), only effective on Apple targets.
|
||||
macos: bool,
|
||||
max: log::LevelFilter,
|
||||
}
|
||||
|
||||
/// The scope shown in log output: the crate name without the `ghostty_`
|
||||
/// prefix (Zig scopes are `@tagName(scope)`).
|
||||
fn log_scope(target: &str) -> &str {
|
||||
let target = target.strip_prefix("ghostty_").unwrap_or(target);
|
||||
target.split("::").next().unwrap_or(target)
|
||||
}
|
||||
|
||||
/// The prefix used for macOS unified logging messages: nothing for the
|
||||
/// default scope, `scope: ` otherwise.
|
||||
#[cfg_attr(not(any(target_vendor = "apple", test)), allow(dead_code))]
|
||||
fn macos_message(scope: &str, args: &std::fmt::Arguments<'_>) -> String {
|
||||
if scope.is_empty() || scope == "default" {
|
||||
format!("{args}")
|
||||
} else {
|
||||
format!("{scope}: {args}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Bundle id used as the unified logging subsystem
|
||||
/// (`build_config.bundle_id`).
|
||||
#[cfg(target_vendor = "apple")]
|
||||
const BUNDLE_ID: &str = ghostty_os::i18n::BUNDLE_ID;
|
||||
|
||||
#[cfg(target_vendor = "apple")]
|
||||
fn log_macos(record: &log::Record<'_>) {
|
||||
// On Mac, we use unified logging. To view this:
|
||||
//
|
||||
// sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'
|
||||
//
|
||||
// macOS logging is thread safe so no need for locks/mutexes.
|
||||
//
|
||||
// Convert our levels to Mac levels.
|
||||
let level = match record.level() {
|
||||
log::Level::Debug | log::Level::Trace => oslog::Level::Debug,
|
||||
log::Level::Info => oslog::Level::Info,
|
||||
log::Level::Warn => oslog::Level::Error,
|
||||
log::Level::Error => oslog::Level::Fault,
|
||||
};
|
||||
let scope = log_scope(record.target());
|
||||
|
||||
// Initialize a logger. This is slow to do on every operation but we
|
||||
// shouldn't be logging too much.
|
||||
let logger = oslog::OsLog::new(BUNDLE_ID, scope);
|
||||
logger.with_level(level, &macos_message(scope, record.args()));
|
||||
}
|
||||
|
||||
impl log::Log for Logger {
|
||||
fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
|
||||
self.stderr && metadata.level() <= self.max
|
||||
(self.stderr || self.macos) && metadata.level() <= self.max
|
||||
}
|
||||
|
||||
fn log(&self, record: &log::Record<'_>) {
|
||||
@@ -186,19 +258,23 @@ impl log::Log for Logger {
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(target_vendor = "apple")]
|
||||
if self.macos {
|
||||
log_macos(record);
|
||||
}
|
||||
|
||||
// skip if we are not logging to stderr
|
||||
if !self.stderr {
|
||||
return;
|
||||
}
|
||||
|
||||
let level = match record.level() {
|
||||
log::Level::Error => "error",
|
||||
log::Level::Warn => "warning",
|
||||
log::Level::Info => "info",
|
||||
log::Level::Debug | log::Level::Trace => "debug",
|
||||
};
|
||||
let target = record.target();
|
||||
let scope = target
|
||||
.strip_prefix("ghostty_")
|
||||
.unwrap_or(target)
|
||||
.split("::")
|
||||
.next()
|
||||
.unwrap_or(target);
|
||||
let scope = log_scope(record.target());
|
||||
let _ = writeln!(
|
||||
std::io::stderr().lock(),
|
||||
"{level}({scope}): {}",
|
||||
@@ -223,6 +299,7 @@ fn init_logger(logging: Logging) {
|
||||
};
|
||||
let logger = Box::new(Logger {
|
||||
stderr: logging.stderr,
|
||||
macos: logging.macos,
|
||||
max,
|
||||
});
|
||||
if log::set_boxed_logger(logger).is_ok() {
|
||||
@@ -230,16 +307,37 @@ fn init_logger(logging: Logging) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleans up the global state. This doesn't _need_ to be called (Zig:
|
||||
/// `GlobalState.deinit`); it deactivates crash capture ("flush crash logs").
|
||||
pub fn deinit() {
|
||||
ghostty_crash::deinit();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn macos_log_message_prefix() {
|
||||
assert_eq!(
|
||||
macos_message("font", &format_args!("hi {}", 1)),
|
||||
"font: hi 1"
|
||||
);
|
||||
assert_eq!(macos_message("default", &format_args!("hi")), "hi");
|
||||
assert_eq!(log_scope("ghostty_font::collection"), "font");
|
||||
assert_eq!(log_scope("other::x"), "other");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logging_parse() {
|
||||
assert!(Logging::parse("stderr").stderr);
|
||||
assert!(!Logging::parse("no-stderr").stderr);
|
||||
// Unmentioned fields keep their default (stderr on).
|
||||
assert!(Logging::parse("macos").stderr);
|
||||
assert!(!Logging::parse("no-macos").macos);
|
||||
assert!(Logging::parse("macos").macos);
|
||||
assert!(Logging::parse("true").macos);
|
||||
assert!(!Logging::parse("0").macos);
|
||||
assert!(!Logging::parse("false").stderr);
|
||||
assert!(!Logging::parse("0").stderr);
|
||||
assert!(Logging::parse("true").stderr);
|
||||
|
||||
@@ -390,6 +390,7 @@ impl Surface {
|
||||
|
||||
/// `textCallback`: text input that is pasted verbatim.
|
||||
pub fn text_callback(&self, text: &str) -> Result<(), ClipboardError> {
|
||||
let _crash = self.crash_guard();
|
||||
self.complete_clipboard_paste(text.as_bytes(), true)
|
||||
}
|
||||
}
|
||||
@@ -906,6 +906,33 @@ impl Surface {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the key encoding options (Zig: `encodeKeyOpts`). On macOS the
|
||||
/// `macos-option-as-alt` value comes from the config, or is detected from
|
||||
/// the keyboard layout when unset.
|
||||
fn encode_key_opts(&self) -> key_encode::Options {
|
||||
let mut opts = {
|
||||
let t = self.lock_terminal();
|
||||
encode_options_from_terminal(&t)
|
||||
};
|
||||
if !cfg!(target_os = "macos") {
|
||||
return opts;
|
||||
}
|
||||
|
||||
opts.macos_option_as_alt = self.cfg().macos_option_as_alt.unwrap_or_else(|| {
|
||||
// If we don't have alt pressed, it doesn't matter what this
|
||||
// config is so we can just say "false" and avoid more expensive
|
||||
// checks below.
|
||||
if !self.mouse.borrow().mods.alt {
|
||||
return ghostty_input::OptionAsAlt::False;
|
||||
}
|
||||
|
||||
// Alt is pressed, we're on macOS. We break some encapsulation
|
||||
// here and assume libghostty for ease...
|
||||
self.rt_app.keyboard_layout().detect_option_as_alt()
|
||||
});
|
||||
opts
|
||||
}
|
||||
|
||||
/// Encodes the key event into a write request. Returns `None` if
|
||||
/// nothing was encoded.
|
||||
fn encode_key(
|
||||
@@ -914,10 +941,7 @@ impl Surface {
|
||||
insp_ev: Option<&mut inspector::KeyEvent>,
|
||||
) -> Option<IoMessage> {
|
||||
// Build our encoding options, which requires the lock.
|
||||
let opts = {
|
||||
let t = self.lock_terminal();
|
||||
encode_options_from_terminal(&t)
|
||||
};
|
||||
let opts = self.encode_key_opts();
|
||||
|
||||
let mut data: Vec<u8> = Vec::with_capacity(64);
|
||||
if let Err(err) = key_encode::encode(&mut data, event, &opts) {
|
||||
|
||||
@@ -90,8 +90,11 @@ use ghostty_font::{
|
||||
use ghostty_input::{Mods, MouseButton, MouseButtonState, ScrollMods};
|
||||
use ghostty_renderer::{
|
||||
Health,
|
||||
Renderer,
|
||||
generic::Host as RendererHost,
|
||||
// The generic renderer over the OpenGL API (the GTK runtime's graphics
|
||||
// API). `ghostty_renderer::Renderer` is the platform default, which is
|
||||
// Metal on Apple targets; the embedded (macOS) runtime provides its own
|
||||
// Metal-based surface setup (TODO(integration): L1).
|
||||
generic::{Host as RendererHost, Renderer},
|
||||
message::{FontGridChange, Message as RendererMessage},
|
||||
opengl::OpenGL,
|
||||
options::Options as RendererOptions,
|
||||
@@ -258,6 +261,9 @@ pub struct Surface {
|
||||
/// All our sizing information.
|
||||
pub(crate) size: Cell<Size>,
|
||||
|
||||
/// The dimensions recorded in crash reports (see `ghostty_crash`).
|
||||
crash_dims: Arc<ghostty_crash::SurfaceDims>,
|
||||
|
||||
/// The configuration derived from the main config.
|
||||
config: RefCell<Rc<DerivedConfig>>,
|
||||
|
||||
@@ -468,7 +474,7 @@ impl Surface {
|
||||
let renderer_state = Arc::clone(termio.renderer_state());
|
||||
|
||||
// Create the renderer thread
|
||||
let render_thread = ghostty_renderer::Thread::new(
|
||||
let mut render_thread = ghostty_renderer::Thread::new(
|
||||
&config,
|
||||
Arc::clone(&renderer),
|
||||
Arc::clone(&renderer_state),
|
||||
@@ -478,9 +484,14 @@ impl Surface {
|
||||
let _ = renderer_link.handle.set(renderer_handle.clone());
|
||||
|
||||
// Create the IO thread
|
||||
let io_thread = IoThread::new()?;
|
||||
let mut io_thread = IoThread::new()?;
|
||||
let io_stop = io_thread.stop_handle();
|
||||
|
||||
// Crash context shared with the renderer and IO threads.
|
||||
let crash_dims = ghostty_crash::SurfaceDims::new();
|
||||
render_thread.crash_dims = Some(Arc::clone(&crash_dims));
|
||||
io_thread.crash_dims = Some(Arc::clone(&crash_dims));
|
||||
|
||||
let surface = Rc::new(Surface {
|
||||
id,
|
||||
app: Rc::clone(&app),
|
||||
@@ -504,6 +515,7 @@ impl Surface {
|
||||
keyboard: RefCell::new(keyboard::Keyboard::default()),
|
||||
pressed_key: RefCell::new(None),
|
||||
size: Cell::new(size),
|
||||
crash_dims,
|
||||
config: RefCell::new(derived_config),
|
||||
config_conditional_state: Cell::new(app.config_conditional_state.get()),
|
||||
child_exited: Cell::new(false),
|
||||
@@ -971,8 +983,10 @@ impl Surface {
|
||||
// If our runtime was below some threshold then we assume that this
|
||||
// was an abnormal exit and we show an error message.
|
||||
if info.runtime_ms <= u64::from(self.cfg().abnormal_command_exit_runtime_ms) {
|
||||
// If the exit code is 0 then it was a good exit.
|
||||
if info.exit_code != 0 {
|
||||
// On macOS, our exit code detection doesn't work, possibly
|
||||
// because of our `login` wrapper. More investigation required.
|
||||
// Elsewhere, if the exit code is 0 then it was a good exit.
|
||||
if is_abnormal_exit(info.exit_code, cfg!(target_vendor = "apple")) {
|
||||
log::warn!("abnormal process exit detected, showing error message");
|
||||
|
||||
// Try and show a GUI message. If it returns true, don't do
|
||||
@@ -1084,13 +1098,17 @@ impl Surface {
|
||||
t.print_string(&runtime_str)?;
|
||||
t.set_attribute(Attribute::Unset)?;
|
||||
|
||||
let exit_code_str = info.exit_code.to_string();
|
||||
t.carriage_return();
|
||||
t.linefeed()?;
|
||||
t.print_string("Exit Code: ")?;
|
||||
t.set_attribute(Attribute::Fg8(ghostty_vt::color::Name::RED))?;
|
||||
t.print_string(&exit_code_str)?;
|
||||
t.set_attribute(Attribute::Unset)?;
|
||||
// We don't print this on macOS because the exit code is always 0
|
||||
// due to the way we launch the process.
|
||||
if !cfg!(target_vendor = "apple") {
|
||||
let exit_code_str = info.exit_code.to_string();
|
||||
t.carriage_return();
|
||||
t.linefeed()?;
|
||||
t.print_string("Exit Code: ")?;
|
||||
t.set_attribute(Attribute::Fg8(ghostty_vt::color::Name::RED))?;
|
||||
t.print_string(&exit_code_str)?;
|
||||
t.set_attribute(Attribute::Unset)?;
|
||||
}
|
||||
|
||||
t.carriage_return();
|
||||
t.linefeed()?;
|
||||
@@ -1436,6 +1454,31 @@ impl Surface {
|
||||
// ------------------------------------------------------------------
|
||||
// Fonts and sizes
|
||||
|
||||
/// Publish the current size for crash reports (Zig reads the surface
|
||||
/// size at crash time).
|
||||
fn sync_crash_dims(&self) {
|
||||
let s = self.size.get();
|
||||
let g = s.grid();
|
||||
self.crash_dims.set(ghostty_crash::DimsSnapshot {
|
||||
screen_width: s.screen.width,
|
||||
screen_height: s.screen.height,
|
||||
grid_columns: u32::from(g.columns),
|
||||
grid_rows: u32::from(g.rows),
|
||||
cell_width: s.cell.width,
|
||||
cell_height: s.cell.height,
|
||||
});
|
||||
}
|
||||
|
||||
/// Crash context for the main thread while inside a surface callback
|
||||
/// (Zig: `crashThreadState`).
|
||||
pub(crate) fn crash_guard(&self) -> ghostty_crash::ThreadGuard {
|
||||
self.sync_crash_dims();
|
||||
ghostty_crash::ThreadGuard::new(
|
||||
ghostty_crash::ThreadType::Main,
|
||||
Some(Arc::clone(&self.crash_dims)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Change the cell size for the terminal grid. This can happen as a
|
||||
/// result of changing the font size at runtime.
|
||||
fn set_cell_size(&self, size: CellSize) {
|
||||
@@ -1444,6 +1487,7 @@ impl Surface {
|
||||
s.cell = size;
|
||||
self.size.set(s);
|
||||
self.balance_padding_if_needed();
|
||||
self.sync_crash_dims();
|
||||
|
||||
// Notify the terminal
|
||||
self.queue_io(IoMessage::Resize(self.size.get()));
|
||||
@@ -1545,6 +1589,8 @@ impl Surface {
|
||||
);
|
||||
}
|
||||
|
||||
self.sync_crash_dims();
|
||||
|
||||
// Mail the IO thread
|
||||
self.queue_io(IoMessage::Resize(s));
|
||||
}
|
||||
@@ -1624,6 +1670,7 @@ impl Surface {
|
||||
/// Callback for when the surface is fully visible or not, regardless of
|
||||
/// focus state.
|
||||
pub fn occlusion_callback(&self, visible: bool) -> Result<(), CoreError> {
|
||||
let _crash = self.crash_guard();
|
||||
self.renderer_handle.send(RendererMessage::Visible(visible));
|
||||
self.queue_render();
|
||||
Ok(())
|
||||
@@ -1666,6 +1713,7 @@ impl Surface {
|
||||
|
||||
/// Schedule a render (Zig: `refreshCallback`).
|
||||
pub fn refresh_callback(&self) -> Result<(), CoreError> {
|
||||
let _crash = self.crash_guard();
|
||||
// The point of this callback is to schedule a render, so do that.
|
||||
self.queue_render();
|
||||
Ok(())
|
||||
@@ -1837,6 +1885,13 @@ impl Drop for Surface {
|
||||
/// (`rt_surface.defaultTermioEnv()`, which already removed the variables it
|
||||
/// does not want to leak); only if it provides nothing (the Zig error path)
|
||||
/// do we fall back to the process environment (`getEnvMap`).
|
||||
/// Whether a quick child exit counts as abnormal. On Darwin our exit code
|
||||
/// detection doesn't work (the `login` wrapper), so every quick exit is
|
||||
/// treated as abnormal; elsewhere only non-zero exit codes are.
|
||||
fn is_abnormal_exit(exit_code: u32, darwin: bool) -> bool {
|
||||
darwin || exit_code != 0
|
||||
}
|
||||
|
||||
fn rt_surface_env(rt: &dyn RtSurface) -> ghostty_os::EnvMap {
|
||||
let rt_env = rt.default_termio_env();
|
||||
if rt_env.is_empty() {
|
||||
@@ -1854,14 +1909,17 @@ impl CoreSurface for Surface {
|
||||
}
|
||||
|
||||
fn key_callback(&self, event: ghostty_input::KeyEvent<'_>) -> Result<InputEffect, CoreError> {
|
||||
let _crash = self.crash_guard();
|
||||
Surface::key_callback(self, event)
|
||||
}
|
||||
|
||||
fn preedit_callback(&self, text: Option<&str>) -> Result<(), CoreError> {
|
||||
let _crash = self.crash_guard();
|
||||
Surface::preedit_callback(self, text)
|
||||
}
|
||||
|
||||
fn focus_callback(&self, focused: bool) -> Result<(), CoreError> {
|
||||
let _crash = self.crash_guard();
|
||||
Surface::focus_callback(self, focused)
|
||||
}
|
||||
|
||||
@@ -1871,26 +1929,32 @@ impl CoreSurface for Surface {
|
||||
button: MouseButton,
|
||||
mods: Mods,
|
||||
) -> Result<bool, CoreError> {
|
||||
let _crash = self.crash_guard();
|
||||
Surface::mouse_button_callback(self, state, button, mods)
|
||||
}
|
||||
|
||||
fn cursor_pos_callback(&self, pos: CursorPos, mods: Option<Mods>) -> Result<(), CoreError> {
|
||||
let _crash = self.crash_guard();
|
||||
Surface::cursor_pos_callback(self, pos, mods)
|
||||
}
|
||||
|
||||
fn scroll_callback(&self, xoff: f64, yoff: f64, mods: ScrollMods) -> Result<(), CoreError> {
|
||||
let _crash = self.crash_guard();
|
||||
Surface::scroll_callback(self, xoff, yoff, mods)
|
||||
}
|
||||
|
||||
fn mouse_pressure_callback(&self, stage: ghostty_input::MousePressureStage, pressure: f64) {
|
||||
let _crash = self.crash_guard();
|
||||
Surface::mouse_pressure_callback(self, stage, pressure);
|
||||
}
|
||||
|
||||
fn size_callback(&self, size: SurfaceSize) -> Result<(), CoreError> {
|
||||
let _crash = self.crash_guard();
|
||||
Surface::size_callback(self, size)
|
||||
}
|
||||
|
||||
fn content_scale_callback(&self, scale: ContentScale) -> Result<(), CoreError> {
|
||||
let _crash = self.crash_guard();
|
||||
Surface::content_scale_callback(self, scale)
|
||||
}
|
||||
|
||||
@@ -1903,6 +1967,7 @@ impl CoreSurface for Surface {
|
||||
}
|
||||
|
||||
fn color_scheme_callback(&self, scheme: ColorScheme) -> Result<(), CoreError> {
|
||||
let _crash = self.crash_guard();
|
||||
Surface::color_scheme_callback(self, scheme)
|
||||
}
|
||||
|
||||
@@ -2012,3 +2077,16 @@ impl CoreSurface for Surface {
|
||||
Surface::render_inspector(self, ui);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod darwin_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn abnormal_exit_detection_per_platform() {
|
||||
assert!(!is_abnormal_exit(0, false));
|
||||
assert!(is_abnormal_exit(1, false));
|
||||
assert!(is_abnormal_exit(0, true));
|
||||
assert!(is_abnormal_exit(1, true));
|
||||
}
|
||||
}
|
||||
@@ -103,13 +103,43 @@ fn is_mouse_mode_override_state(mods: Mods) -> bool {
|
||||
/// cause a rectangle select.
|
||||
#[must_use]
|
||||
pub fn is_rectangle_select_state(mods: Mods) -> bool {
|
||||
mods.ctrl_or_super() && mods.alt
|
||||
is_rectangle_select_state_for(mods, ghostty_input::platform::is_darwin())
|
||||
}
|
||||
|
||||
/// [`is_rectangle_select_state`] for an explicit platform (Zig picks with
|
||||
/// `builtin.target.os.tag.isDarwin()` at comptime): on Darwin option alone
|
||||
/// selects a rectangle, elsewhere it is ctrl+alt.
|
||||
#[must_use]
|
||||
pub fn is_rectangle_select_state_for(mods: Mods, darwin: bool) -> bool {
|
||||
if darwin {
|
||||
mods.alt
|
||||
} else {
|
||||
mods.ctrl_or_super() && mods.alt
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rectangle_select_state_per_platform() {
|
||||
let alt = Mods {
|
||||
alt: true,
|
||||
..Mods::default()
|
||||
};
|
||||
let ctrl_alt = Mods {
|
||||
alt: true,
|
||||
ctrl: true,
|
||||
..Mods::default()
|
||||
};
|
||||
// macOS: alt alone.
|
||||
assert!(is_rectangle_select_state_for(alt, true));
|
||||
// Elsewhere: ctrl+alt.
|
||||
assert!(!is_rectangle_select_state_for(alt, false));
|
||||
assert!(is_rectangle_select_state_for(ctrl_alt, false));
|
||||
}
|
||||
|
||||
fn m(
|
||||
physical_key: Key,
|
||||
mouse_event: MouseEvents,
|
||||
|
||||
Reference in new issue
Block a user