ashlar_native/
mac_shell.rs1use std::sync::mpsc::{Receiver, channel};
18
19use crate::{NativeError, Result, Waker};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct Reopen {
24 pub has_visible_windows: bool,
26}
27
28pub fn quiet_requested(env_prefix: &str) -> bool {
30 std::env::var(format!("{env_prefix}_QUIET")).is_ok_and(|v| quiet_value(&v))
31}
32
33fn quiet_value(v: &str) -> bool {
34 let v = v.trim();
35 !(v.is_empty() || v == "0" || ["false", "no", "off"].iter().any(|f| v.eq_ignore_ascii_case(f)))
36}
37
38pub fn guard_appearance() -> Result<()> {
42 imp::guard_appearance()
43}
44
45pub fn listen_reopen(waker: Waker) -> Result<Receiver<Reopen>> {
49 let (tx, rx) = channel();
50 imp::listen_reopen(tx, waker)?;
51 Ok(rx)
52}
53
54#[cfg(target_os = "macos")]
55#[allow(unsafe_code)]
56mod imp {
57 use std::ffi::{CStr, c_void};
58 use std::sync::atomic::{AtomicUsize, Ordering};
59 use std::sync::mpsc::Sender;
60 use std::sync::{Mutex, PoisonError};
61
62 use objc2::runtime::{AnyClass, AnyObject, Bool, Imp, ProtocolObject, Sel};
63 use objc2::{MainThreadMarker, msg_send, sel};
64 use objc2_app_kit::NSApplication;
65 use objc2_foundation::{NSKeyValueChangeNewKey, NSKeyValueChangeOldKey, NSString};
66
67 use super::*;
68
69 type Observe = extern "C-unwind" fn(*mut AnyObject, Sel, *mut AnyObject, *mut AnyObject, *mut AnyObject, *mut c_void);
72
73 static ORIGINAL: AtomicUsize = AtomicUsize::new(0);
75
76 fn is_appearance(obj: *mut AnyObject) -> bool {
78 let Some(cls) = AnyClass::get(c"NSAppearance") else { return false };
79 !obj.is_null() && unsafe { msg_send![obj, isKindOfClass: cls] }
82 }
83
84 fn safe_change(key_path: *mut AnyObject, change: *mut AnyObject) -> bool {
87 if key_path.is_null() || change.is_null() {
88 return false;
89 }
90 let ours = NSString::from_str("effectiveAppearance");
91 let same: bool = unsafe { msg_send![key_path, isEqual: &*ours] };
93 if !same {
94 return false;
95 }
96 let (old, new): (*mut AnyObject, *mut AnyObject) =
98 unsafe { (msg_send![change, objectForKey: NSKeyValueChangeOldKey], msg_send![change, objectForKey: NSKeyValueChangeNewKey]) };
99 is_appearance(old) && is_appearance(new)
100 }
101
102 extern "C-unwind" fn observe(
103 this: *mut AnyObject,
104 cmd: Sel,
105 key_path: *mut AnyObject,
106 object: *mut AnyObject,
107 change: *mut AnyObject,
108 context: *mut c_void,
109 ) {
110 let ok = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| safe_change(key_path, change))).unwrap_or(false);
111 let original = ORIGINAL.load(Ordering::SeqCst);
112 if !ok || original == 0 {
113 log::debug!("ignored an appearance change without an appearance (screen capture?)");
114 return;
115 }
116 let f: Observe = unsafe { std::mem::transmute::<usize, Observe>(original) };
119 f(this, cmd, key_path, object, change, context);
120 }
121
122 pub fn guard_appearance() -> Result<()> {
123 if MainThreadMarker::new().is_none() {
124 return Err(NativeError::Os("guard the appearance observer on the main thread".into()));
125 }
126 let Some(cls) = AnyClass::get(c"WinitWindowDelegate") else {
127 return Err(NativeError::Os("no window yet: guard the appearance observer after the first window opens".into()));
128 };
129 let Some(method) = cls.instance_method(sel!(observeValueForKeyPath:ofObject:change:context:)) else {
130 return Err(NativeError::Unsupported);
131 };
132 let ours = observe as Observe;
133 if method.implementation() as usize == ours as usize {
134 return Ok(());
135 }
136 let imp: Imp = unsafe { std::mem::transmute::<Observe, Imp>(ours) };
139 let before = unsafe { method.set_implementation(imp) };
143 ORIGINAL.store(before as usize, Ordering::SeqCst);
144 Ok(())
145 }
146
147 static REOPEN: Mutex<Option<(Sender<Reopen>, Waker)>> = Mutex::new(None);
150
151 fn report(visible: bool) {
152 let sink = REOPEN.lock().unwrap_or_else(PoisonError::into_inner).clone();
153 if let Some((tx, wake)) = sink
154 && tx.send(Reopen { has_visible_windows: visible }).is_ok()
155 {
156 wake();
157 }
158 }
159
160 extern "C-unwind" fn should_reopen(_this: *mut AnyObject, _cmd: Sel, _app: *mut AnyObject, visible: Bool) -> Bool {
163 let _ = std::panic::catch_unwind(|| report(visible.as_bool()));
164 Bool::YES
165 }
166
167 pub fn listen_reopen(tx: Sender<Reopen>, waker: Waker) -> Result<()> {
168 let Some(mtm) = MainThreadMarker::new() else {
169 return Err(NativeError::Os("listen for Dock reopen on the main thread".into()));
170 };
171 let app = NSApplication::sharedApplication(mtm);
172 let Some(delegate) = app.delegate() else {
173 return Err(NativeError::Os("the application has no delegate yet; listen once the event loop runs".into()));
174 };
175 let obj: &AnyObject = ProtocolObject::as_ref(&*delegate);
176 let cls: &AnyClass = obj.class();
177 let name = sel!(applicationShouldHandleReopen:hasVisibleWindows:);
178 type Reopened = extern "C-unwind" fn(*mut AnyObject, Sel, *mut AnyObject, Bool) -> Bool;
179 let ours = should_reopen as Reopened;
180 match cls.instance_method(name) {
181 Some(m) if m.implementation() as usize == ours as usize => {}
182 Some(_) => return Err(NativeError::Os("the application delegate already answers Dock reopen".into())),
183 None => {
184 const TYPES: &CStr = c"c@:@c"; let imp: Imp = unsafe { std::mem::transmute::<Reopened, Imp>(ours) };
188 let added = unsafe { objc2::ffi::class_addMethod(cls as *const AnyClass as *mut AnyClass, name, imp, TYPES.as_ptr()) };
192 if !added.as_bool() {
193 return Err(NativeError::Os("could not add the reopen handler to the application delegate".into()));
194 }
195 app.setDelegate(Some(&delegate));
197 }
198 }
199 *REOPEN.lock().unwrap_or_else(PoisonError::into_inner) = Some((tx, waker));
200 Ok(())
201 }
202}
203
204#[cfg(not(target_os = "macos"))]
205mod imp {
206 use std::sync::mpsc::Sender;
207
208 use super::*;
209
210 pub fn guard_appearance() -> Result<()> {
211 Err(NativeError::Unsupported)
212 }
213
214 pub fn listen_reopen(_tx: Sender<Reopen>, _waker: Waker) -> Result<()> {
215 Err(NativeError::Unsupported)
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn quiet_values() {
225 for on in ["1", "yes", "true", " on "] {
226 assert!(quiet_value(on), "{on:?}");
227 }
228 for off in ["", " ", "0", "false", "NO", "off"] {
229 assert!(!quiet_value(off), "{off:?}");
230 }
231 assert!(!quiet_requested("ASHLAR_TEST_SURELY_UNSET_PREFIX"));
232 }
233
234 #[test]
235 fn off_the_main_thread_is_an_error_not_a_crash() {
236 assert!(guard_appearance().is_err());
238 assert!(listen_reopen(crate::no_waker()).is_err());
239 }
240}