@livx.cc/appwrap 0.43.0 → 0.46.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,497 @@
1
+ // WKWebView adapter for the webview-control primitive (macOS). Lifted from bridge_mac.rs
2
+ // (Phase 1 extraction) with the transport/protocol stripped OUT: this module emits RAW page
3
+ // payloads via an `on_message` callback and RAW eval/snapshot results via per-call `on_done`
4
+ // callbacks. It knows nothing of the bod-browser wire format, the TCP Sender, or the
5
+ // {type:"pageMsg"} / {id,result} framing — those live in the control-channel adapter (bridge_mac.rs).
6
+ use std::cell::RefCell;
7
+ use std::sync::atomic::{AtomicBool, Ordering};
8
+ use std::sync::Mutex;
9
+
10
+ use objc2::rc::Retained;
11
+ use objc2::runtime::AnyObject;
12
+ use objc2::runtime::ProtocolObject;
13
+ use objc2::{define_class, msg_send, AnyThread, DefinedClass, MainThreadMarker, MainThreadOnly};
14
+ use objc2_app_kit::{
15
+ NSAutoresizingMaskOptions, NSBackingStoreType, NSBitmapImageFileType, NSBitmapImageRep, NSImage,
16
+ NSView, NSWindow, NSWindowDelegate, NSWindowStyleMask,
17
+ };
18
+ use objc2_foundation::{
19
+ NSDictionary, NSError, NSNotification, NSObject, NSObjectProtocol, NSPoint, NSRect, NSSize,
20
+ NSString, NSURL, NSURLRequest, NSUUID,
21
+ };
22
+ use objc2_web_kit::{
23
+ WKContentWorld, WKScriptMessage, WKScriptMessageHandler, WKUserContentController, WKUserScript,
24
+ WKUserScriptInjectionTime, WKWebView, WKWebViewConfiguration, WKWebsiteDataStore,
25
+ };
26
+
27
+ use crate::popup;
28
+ use crate::profiles;
29
+ use crate::Mount;
30
+
31
+ /// Autoresizing mask that pins a mounted webview to fill its superview on resize — the same
32
+ /// width+height-sizable mask popup windows use, applied on every (re)mount.
33
+ const FILL_MASK: NSAutoresizingMaskOptions =
34
+ NSAutoresizingMaskOptions(NSAutoresizingMaskOptions::ViewWidthSizable.0 | NSAutoresizingMaskOptions::ViewHeightSizable.0);
35
+
36
+ /// Raw page->host message sink. Receives the UNWRAPPED WKScriptMessage body string (the page's
37
+ /// `webkit.messageHandlers.bridgeShim.postMessage(...)` payload). The adapter wraps it into its
38
+ /// transport envelope — the primitive stays framing-agnostic. `Send` (it crosses onto the main
39
+ /// thread with the create op) but not `Sync` — the handler is MainThreadOnly and only ever invoked
40
+ /// on the main thread, mirroring the original bare `Sender`.
41
+ pub type MessageSink = Box<dyn Fn(String) + Send>;
42
+
43
+ /// Result of creating a profile window — the store facts the harness asserts. The adapter builds
44
+ /// the wire JSON from this; the primitive returns plain data.
45
+ pub struct CreateResult {
46
+ pub persistent: bool,
47
+ pub store_uuid: String,
48
+ }
49
+
50
+ /// One profile window's public identity for `list_webviews`.
51
+ pub struct WebviewInfo {
52
+ pub label: String,
53
+ pub profile_id: String,
54
+ }
55
+
56
+ /// Spec for creating a webview via the embedded facade (Phase 2). Mirrors the control-channel
57
+ /// `createWindow` op fields plus the `mount` target (the adapter always builds `Mount::TopLevel`).
58
+ pub struct CreateSpec {
59
+ pub label: String,
60
+ pub url: String,
61
+ pub profile_id: String,
62
+ pub init_js: String,
63
+ pub mount: Mount,
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Page -> host transport: a native WKScriptMessageHandler ("bridgeShim").
68
+ // WebKit blocks ws://127.0.0.1 (and http beacons) from https pages as mixed content — NO loopback
69
+ // exemption like Chrome — so the content layer posts results via
70
+ // webkit.messageHandlers.bridgeShim.postMessage(json) instead. We hand the RAW payload to the
71
+ // on_message sink; the adapter does the framing.
72
+ // ---------------------------------------------------------------------------
73
+ pub struct MsgHandlerIvars {
74
+ on_message: MessageSink,
75
+ }
76
+
77
+ define_class!(
78
+ #[unsafe(super(NSObject))]
79
+ #[thread_kind = MainThreadOnly]
80
+ #[name = "AppwrapBridgeMsgHandler"]
81
+ #[ivars = MsgHandlerIvars]
82
+ struct MsgHandler;
83
+
84
+ unsafe impl NSObjectProtocol for MsgHandler {}
85
+
86
+ unsafe impl WKScriptMessageHandler for MsgHandler {
87
+ #[unsafe(method(userContentController:didReceiveScriptMessage:))]
88
+ fn did_receive(&self, _ucc: &WKUserContentController, message: &WKScriptMessage) {
89
+ let body = unsafe { message.body() };
90
+ if let Ok(s) = body.downcast::<NSString>() {
91
+ (self.ivars().on_message)(s.to_string());
92
+ }
93
+ }
94
+ }
95
+ );
96
+
97
+ /// Install the bridgeShim WKScriptMessageHandler on an arbitrary WKWebView (used by the adapter for
98
+ /// the legacy tauri-window path — the profile-window path installs it on the config's UCC before
99
+ /// creation). The UCC retains the handler, so no external retention is needed here. Main-thread only.
100
+ pub unsafe fn install_message_handler(wk: *mut WKWebView, mtm: MainThreadMarker, on_message: MessageSink) {
101
+ let wk = &*wk;
102
+ let handler = MsgHandler::alloc(mtm).set_ivars(MsgHandlerIvars { on_message });
103
+ let handler: Retained<MsgHandler> = msg_send![super(handler), init];
104
+ let ucc = wk.configuration().userContentController();
105
+ ucc.addScriptMessageHandler_name(
106
+ ProtocolObject::from_ref(&*handler),
107
+ &NSString::from_str("bridgeShim"),
108
+ );
109
+ }
110
+
111
+ /// A manually-created profile window. Strong refs kept alive until close (or windowWillClose).
112
+ /// Only touched on the main thread; the unsafe Send exists solely so the registry can be a static
113
+ /// Mutex (same pattern as popup_mac).
114
+ struct ProfileWindow {
115
+ label: String,
116
+ profile_id: String,
117
+ window: Retained<NSWindow>,
118
+ webview: Retained<WKWebView>,
119
+ /// Set true by the window-close delegate on a USER (red-button) close.
120
+ closed: AtomicBool,
121
+ _handler: Retained<MsgHandler>,
122
+ /// Per-window SSO-popup delegate (weak uiDelegate → held here). Opaque; lifetime only.
123
+ _popup_delegate: Retained<AnyObject>,
124
+ /// NSWindowDelegate that flags `closed` on a user close (weak setDelegate → held here).
125
+ _win_delegate: Retained<ProfileWinDelegate>,
126
+ }
127
+ unsafe impl Send for ProfileWindow {}
128
+ static PROFILE_WINDOWS: Mutex<Vec<ProfileWindow>> = Mutex::new(Vec::new());
129
+
130
+ // NSWindowDelegate for profile windows — flags the matching registry entry `closed` when the user
131
+ // closes the window (red button), so it drops out of list_webviews. Matches by NSWindow identity
132
+ // (the notification's object). Main-thread only.
133
+ define_class!(
134
+ #[unsafe(super(NSObject))]
135
+ #[thread_kind = MainThreadOnly]
136
+ #[name = "AppwrapProfileWinDelegate"]
137
+ struct ProfileWinDelegate;
138
+
139
+ unsafe impl NSObjectProtocol for ProfileWinDelegate {}
140
+
141
+ unsafe impl NSWindowDelegate for ProfileWinDelegate {
142
+ #[unsafe(method(windowWillClose:))]
143
+ fn window_will_close(&self, notification: &NSNotification) {
144
+ let Some(obj) = notification.object() else { return };
145
+ let key = Retained::as_ptr(&obj) as usize;
146
+ let v = PROFILE_WINDOWS.lock().unwrap();
147
+ if let Some(p) = v.iter().find(|p| Retained::as_ptr(&p.window) as usize == key) {
148
+ eprintln!("[bridge-shim] profile window {} user-closed", p.label);
149
+ // Detach from the superview so a ChildOf-mounted webview isn't left as a zombie in
150
+ // the host pane once `reap_closed` drops the registry `Retained` (see `close`).
151
+ p.webview.removeFromSuperview();
152
+ p.closed.store(true, Ordering::Relaxed);
153
+ }
154
+ }
155
+ }
156
+ );
157
+
158
+ /// Retained clone of a profile window's WKWebView (op routing), or None if the label is unknown /
159
+ /// already closed. The adapter uses this to decide whether an op targets a profile window (else it
160
+ /// falls back to its legacy tauri path).
161
+ pub fn profile_webview(label: &str) -> Option<Retained<WKWebView>> {
162
+ PROFILE_WINDOWS
163
+ .lock()
164
+ .unwrap()
165
+ .iter()
166
+ .find(|p| p.label == label && !p.closed.load(Ordering::Relaxed))
167
+ .map(|p| p.webview.clone())
168
+ }
169
+
170
+ /// Public identities of the live (non-closed) profile windows.
171
+ pub fn list_webviews() -> Vec<WebviewInfo> {
172
+ PROFILE_WINDOWS
173
+ .lock()
174
+ .unwrap()
175
+ .iter()
176
+ .filter(|p| !p.closed.load(Ordering::Relaxed))
177
+ .map(|p| WebviewInfo { label: p.label.clone(), profile_id: p.profile_id.clone() })
178
+ .collect()
179
+ }
180
+
181
+ /// Drop entries flagged `closed` (user-closed windows). Called at safe points only — never from
182
+ /// within a delegate callback, so the delegate isn't released mid-call.
183
+ fn reap_closed() {
184
+ let mut v = PROFILE_WINDOWS.lock().unwrap();
185
+ v.retain(|p| !p.closed.load(Ordering::Relaxed));
186
+ }
187
+
188
+ /// Close + drop a profile window (releases its NSWindow + WKWebView). Main-thread only. Returns
189
+ /// false if no such profile window (caller falls back to tauri). The removed entry is held on the
190
+ /// stack across window.close() so its window delegate stays alive while AppKit fires windowWillClose.
191
+ pub fn close(label: &str) -> bool {
192
+ let entry = {
193
+ let mut v = PROFILE_WINDOWS.lock().unwrap();
194
+ v.iter().position(|p| p.label == label).map(|i| v.remove(i))
195
+ };
196
+ match entry {
197
+ Some(p) => {
198
+ // Detach the WKWebView from its superview BEFORE dropping the registry `Retained`. In
199
+ // `Mount::ChildOf` (embedded) mode the webview is a subview of the HOST pane (retained
200
+ // by the host's subview array), NOT the home window's contentView — so `window.close()`
201
+ // alone leaves it alive and VISIBLE in the host pane (a zombie). Harmless for TopLevel
202
+ // (the webview IS the home window's contentView, released with the window anyway).
203
+ p.webview.removeFromSuperview();
204
+ p.window.close();
205
+ true
206
+ }
207
+ None => false,
208
+ }
209
+ }
210
+
211
+ /// Drive NSWindow performClose: (the red-button path) WITHOUT touching the registry, so the
212
+ /// windowWillClose delegate is the sole remover — proves user-close hygiene headlessly. Returns
213
+ /// false if no such profile window. Main-thread only.
214
+ pub fn user_close(label: &str) -> bool {
215
+ let win = PROFILE_WINDOWS
216
+ .lock()
217
+ .unwrap()
218
+ .iter()
219
+ .find(|p| p.label == label)
220
+ .map(|p| p.window.clone());
221
+ match win {
222
+ Some(w) => {
223
+ w.performClose(None);
224
+ true
225
+ }
226
+ None => false,
227
+ }
228
+ }
229
+
230
+ /// Create a top-level profile window (the standalone/MCP shape — `Mount::TopLevel`). Thin wrapper
231
+ /// over [`create_webview`]; kept as-is so the control-channel adapter (bridge_mac.rs) is unchanged
232
+ /// and its behavior byte-identical. Main-thread only.
233
+ pub fn create_profile_window(
234
+ mtm: MainThreadMarker,
235
+ label: &str,
236
+ url: &str,
237
+ profile_id: &str,
238
+ init_js: &str,
239
+ on_message: MessageSink,
240
+ ) -> Result<CreateResult, String> {
241
+ create_webview(
242
+ mtm,
243
+ CreateSpec {
244
+ label: label.to_string(),
245
+ url: url.to_string(),
246
+ profile_id: profile_id.to_string(),
247
+ init_js: init_js.to_string(),
248
+ mount: Mount::TopLevel,
249
+ },
250
+ on_message,
251
+ )
252
+ }
253
+
254
+ /// Create a manual NSWindow + WKWebView bound to `profile_id`'s persistent data store, inject the
255
+ /// bridge content bundle + bridgeShim handler, mount it per `spec.mount`, and register it. Runs on
256
+ /// the main thread (the adapter/host marshals it there). Returns the store facts.
257
+ ///
258
+ /// The webview ALWAYS owns a hidden home `NSWindow` (the `TopLevel` target), created here with
259
+ /// `setReleasedWhenClosed(false)` so a host-window close never frees the view. For `TopLevel` the
260
+ /// window is shown and holds the webview as its content view (identical to the pre-Phase-2 path);
261
+ /// for `ChildOf(handle)` the window stays unshown and the webview is added as a subview of the
262
+ /// host-owned pane. `set_mount` later moves the same live `WKWebView` between the two.
263
+ pub fn create_webview(
264
+ mtm: MainThreadMarker,
265
+ spec: CreateSpec,
266
+ on_message: MessageSink,
267
+ ) -> Result<CreateResult, String> {
268
+ let CreateSpec { label, url, profile_id, init_js, mount } = spec;
269
+ let (label, url, profile_id, init_js) = (label.as_str(), url.as_str(), profile_id.as_str(), init_js.as_str());
270
+ let uuid_str = profiles::uuid_for(profile_id);
271
+ let nsuuid = NSUUID::initWithUUIDString(NSUUID::alloc(), &NSString::from_str(&uuid_str))
272
+ .ok_or_else(|| format!("bad uuid for profile {profile_id}: {uuid_str}"))?;
273
+ let store = unsafe { WKWebsiteDataStore::dataStoreForIdentifier(&nsuuid, mtm) };
274
+ let persistent = unsafe { store.isPersistent() };
275
+
276
+ let cfg = unsafe { WKWebViewConfiguration::new(mtm) };
277
+ unsafe { cfg.setWebsiteDataStore(&store) };
278
+
279
+ // bridgeShim message handler + content bundle (documentStart) on this cfg's UCC.
280
+ let ucc = unsafe { cfg.userContentController() };
281
+ let handler = MsgHandler::alloc(mtm).set_ivars(MsgHandlerIvars { on_message });
282
+ let handler: Retained<MsgHandler> = unsafe { msg_send![super(handler), init] };
283
+ unsafe {
284
+ ucc.addScriptMessageHandler_name(
285
+ ProtocolObject::from_ref(&*handler),
286
+ &NSString::from_str("bridgeShim"),
287
+ );
288
+ let script = WKUserScript::initWithSource_injectionTime_forMainFrameOnly(
289
+ WKUserScript::alloc(mtm),
290
+ &NSString::from_str(init_js),
291
+ WKUserScriptInjectionTime::AtDocumentStart,
292
+ true,
293
+ );
294
+ ucc.addUserScript(&script);
295
+ }
296
+
297
+ let rect = NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(1100.0, 800.0));
298
+ let style = NSWindowStyleMask::Titled
299
+ | NSWindowStyleMask::Closable
300
+ | NSWindowStyleMask::Miniaturizable
301
+ | NSWindowStyleMask::Resizable;
302
+ let window = unsafe {
303
+ NSWindow::initWithContentRect_styleMask_backing_defer(
304
+ mtm.alloc::<NSWindow>(),
305
+ rect,
306
+ style,
307
+ NSBackingStoreType::Buffered,
308
+ false,
309
+ )
310
+ };
311
+ unsafe { window.setReleasedWhenClosed(false) };
312
+
313
+ let webview = unsafe {
314
+ WKWebView::initWithFrame_configuration(mtm.alloc::<WKWebView>(), window.frame(), &cfg)
315
+ };
316
+ unsafe {
317
+ webview.setCustomUserAgent(Some(&NSString::from_str(popup::SAFARI_UA)));
318
+ window.setTitle(&NSString::from_str(label));
319
+ }
320
+
321
+ // Per-window SSO-popup delegate + window-close delegate. Both are weak refs on AppKit/WebKit →
322
+ // retained in the registry entry below.
323
+ let popup_delegate: Retained<AnyObject> =
324
+ popup::install_for_profile(Retained::as_ptr(&webview) as *mut std::ffi::c_void, mtm);
325
+ let win_delegate = ProfileWinDelegate::alloc(mtm).set_ivars(());
326
+ let win_delegate: Retained<ProfileWinDelegate> = unsafe { msg_send![super(win_delegate), init] };
327
+ window.setDelegate(Some(ProtocolObject::from_ref(&*win_delegate)));
328
+ window.center();
329
+
330
+ // Initial mount: TopLevel shows the home window with the webview as content view (the
331
+ // pre-Phase-2 behavior, byte-identical); ChildOf leaves the home window unshown and adds the
332
+ // webview into the host pane. Same reparent primitive as set_mount, applied at birth.
333
+ unsafe { mount_view(mtm, &webview, &window, mount) };
334
+
335
+ let nsurl = NSURL::URLWithString(&NSString::from_str(url))
336
+ .ok_or_else(|| format!("bad url: {url}"))?;
337
+ let req = NSURLRequest::requestWithURL(&nsurl);
338
+ let _ = unsafe { webview.loadRequest(&req) };
339
+
340
+ reap_closed();
341
+ PROFILE_WINDOWS.lock().unwrap().push(ProfileWindow {
342
+ label: label.to_string(),
343
+ profile_id: profile_id.to_string(),
344
+ window,
345
+ webview,
346
+ closed: AtomicBool::new(false),
347
+ _handler: handler,
348
+ _popup_delegate: popup_delegate,
349
+ _win_delegate: win_delegate,
350
+ });
351
+ eprintln!(
352
+ "[bridge-shim] profile window {label} profile={profile_id} store={uuid_str} persistent={persistent}"
353
+ );
354
+ Ok(CreateResult { persistent, store_uuid: uuid_str })
355
+ }
356
+
357
+ // ---------------------------------------------------------------------------
358
+ // Phase 2: NSView-reparent (tear-off / dock). The SAME live WKWebView object is moved between its
359
+ // home NSWindow (TopLevel) and a host-owned pane NSView (ChildOf) — page + WKWebsiteDataStore
360
+ // session + WKScriptMessageHandler bridge + SSO uiDelegate all survive because it is never
361
+ // re-created. Productionized from the Phase-0 spike. Main-thread only (WKWebView/NSView are not
362
+ // thread-safe); zero-frame subviews render blank, so we always set frame = target bounds + a
363
+ // fill autoresizing mask.
364
+ // ---------------------------------------------------------------------------
365
+
366
+ /// Reparent `webview` to `mount`, using `home_window` as the TopLevel target. Caller holds the
367
+ /// main thread. `ChildOf` dereferences the host `NSView*` per the [`crate::ViewHandle`] contract.
368
+ unsafe fn mount_view(
369
+ _mtm: MainThreadMarker,
370
+ webview: &WKWebView,
371
+ home_window: &NSWindow,
372
+ mount: Mount,
373
+ ) {
374
+ webview.removeFromSuperview();
375
+ match mount {
376
+ Mount::TopLevel => {
377
+ let size = home_window.frame().size;
378
+ webview.setFrame(NSRect::new(NSPoint::new(0.0, 0.0), size));
379
+ webview.setAutoresizingMask(FILL_MASK);
380
+ home_window.setContentView(Some(webview));
381
+ home_window.makeKeyAndOrderFront(None);
382
+ }
383
+ Mount::ChildOf(handle) => {
384
+ // The host owns this NSView; we only add a subview link (contract: valid, main-thread,
385
+ // host keeps it alive while mounted). Order the home window out so it isn't a stray.
386
+ let host = &*(handle.as_ptr() as *mut NSView);
387
+ let bounds = host.bounds();
388
+ webview.setFrame(bounds);
389
+ webview.setAutoresizingMask(FILL_MASK);
390
+ host.addSubview(webview);
391
+ home_window.orderOut(None);
392
+ }
393
+ }
394
+ }
395
+
396
+ /// Move a live profile webview between top-level and child-of-a-host-pane (tear-off / dock).
397
+ /// Returns false if `label` is unknown or already closed. Main-thread only.
398
+ pub fn set_mount(mtm: MainThreadMarker, label: &str, mount: Mount) -> bool {
399
+ let parts = PROFILE_WINDOWS
400
+ .lock()
401
+ .unwrap()
402
+ .iter()
403
+ .find(|p| p.label == label && !p.closed.load(Ordering::Relaxed))
404
+ .map(|p| (p.webview.clone(), p.window.clone()));
405
+ match parts {
406
+ Some((webview, window)) => {
407
+ unsafe { mount_view(mtm, &webview, &window, mount) };
408
+ true
409
+ }
410
+ None => false,
411
+ }
412
+ }
413
+
414
+ /// Navigate any WKWebView (profile or tauri) to `url`. Main-thread only.
415
+ pub unsafe fn navigate(wk: *mut WKWebView, url: &str) -> Result<(), String> {
416
+ let wk = &*wk;
417
+ match NSURL::URLWithString(&NSString::from_str(url)) {
418
+ Some(nsurl) => {
419
+ let req = NSURLRequest::requestWithURL(&nsurl);
420
+ let _ = wk.loadRequest(&req);
421
+ Ok(())
422
+ }
423
+ None => Err(format!("bad url: {url}")),
424
+ }
425
+ }
426
+
427
+ /// Fire-and-forget JS injection. Main-thread only.
428
+ pub unsafe fn inject_script(wk: *mut WKWebView, js: &str) {
429
+ let wk = &*wk;
430
+ wk.evaluateJavaScript_completionHandler(&NSString::from_str(js), None);
431
+ }
432
+
433
+ /// callAsyncJavaScript on a raw WKWebView ptr — CSP-immune eval with an awaited return value.
434
+ /// Delivers the RAW stringified JS return value ("null" or the NSString) via `on_done`; the adapter
435
+ /// wraps it into the wire envelope. Main-thread only (the completion block also lands there).
436
+ pub unsafe fn eval_async(
437
+ wk: *mut WKWebView,
438
+ js: String,
439
+ on_done: impl FnOnce(Result<String, String>) + 'static,
440
+ ) {
441
+ let wk = &*wk;
442
+ let mtm = MainThreadMarker::new().expect("eval_async runs on main");
443
+ let on_done = RefCell::new(Some(on_done));
444
+ let block = block2::RcBlock::new(move |result: *mut AnyObject, error: *mut NSError| {
445
+ let Some(cb) = on_done.borrow_mut().take() else { return };
446
+ if !error.is_null() {
447
+ cb(Err((*error).localizedDescription().to_string()));
448
+ return;
449
+ }
450
+ let out = if result.is_null() {
451
+ "null".to_string()
452
+ } else if let Ok(ns) = (&*result).downcast_ref::<NSString>().ok_or(()) {
453
+ ns.to_string()
454
+ } else {
455
+ "null".to_string()
456
+ };
457
+ cb(Ok(out));
458
+ });
459
+ wk.callAsyncJavaScript_arguments_inFrame_inContentWorld_completionHandler(
460
+ &NSString::from_str(&js),
461
+ None,
462
+ None,
463
+ &WKContentWorld::pageWorld(mtm),
464
+ Some(&block),
465
+ );
466
+ }
467
+
468
+ /// WKWebView takeSnapshot -> NSImage -> PNG bytes, delivered via `on_done`. The adapter base64s +
469
+ /// frames it. Main-thread only (the completion block also lands there).
470
+ pub unsafe fn snapshot(wk: *mut WKWebView, on_done: impl FnOnce(Result<Vec<u8>, String>) + 'static) {
471
+ let wk = &*wk;
472
+ let on_done = RefCell::new(Some(on_done));
473
+ let block = block2::RcBlock::new(move |image: *mut NSImage, error: *mut NSError| {
474
+ let Some(cb) = on_done.borrow_mut().take() else { return };
475
+ if image.is_null() {
476
+ let msg = if error.is_null() { "snapshot failed".to_string() } else { (*error).localizedDescription().to_string() };
477
+ cb(Err(msg));
478
+ return;
479
+ }
480
+ let img = &*image;
481
+ let Some(tiff) = img.TIFFRepresentation() else {
482
+ cb(Err("no TIFF representation".into()));
483
+ return;
484
+ };
485
+ let Some(rep) = NSBitmapImageRep::initWithData(NSBitmapImageRep::alloc(), &tiff) else {
486
+ cb(Err("bitmap rep failed".into()));
487
+ return;
488
+ };
489
+ let props = NSDictionary::new();
490
+ let Some(png) = rep.representationUsingType_properties(NSBitmapImageFileType::PNG, &props) else {
491
+ cb(Err("png encode failed".into()));
492
+ return;
493
+ };
494
+ cb(Ok(png.to_vec()));
495
+ });
496
+ wk.takeSnapshotWithConfiguration_completionHandler(None, &block);
497
+ }
@@ -52,7 +52,9 @@ name = "appwrap-desktop-shell"
52
52
  version = "0.1.0"
53
53
  dependencies = [
54
54
  "arboard",
55
+ "base64 0.22.1",
55
56
  "block2",
57
+ "libc",
56
58
  "objc2",
57
59
  "objc2-app-kit",
58
60
  "objc2-authentication-services",
@@ -68,6 +70,7 @@ dependencies = [
68
70
  "tauri-plugin-deep-link",
69
71
  "tauri-plugin-notification",
70
72
  "tauri-plugin-window-state",
73
+ "webview-control",
71
74
  ]
72
75
 
73
76
  [[package]]
@@ -4624,6 +4627,18 @@ dependencies = [
4624
4627
  "system-deps",
4625
4628
  ]
4626
4629
 
4630
+ [[package]]
4631
+ name = "webview-control"
4632
+ version = "0.1.0"
4633
+ dependencies = [
4634
+ "block2",
4635
+ "objc2",
4636
+ "objc2-app-kit",
4637
+ "objc2-foundation",
4638
+ "objc2-web-kit",
4639
+ "serde_json",
4640
+ ]
4641
+
4627
4642
  [[package]]
4628
4643
  name = "webview2-com"
4629
4644
  version = "0.38.2"
@@ -15,10 +15,15 @@ tauri-plugin-autostart = "2"
15
15
  serde = { version = "1", features = ["derive"] }
16
16
  serde_json = "1"
17
17
  arboard = "3"
18
+ libc = "0.2" # desktop.server: setsid the local-server child + SIGTERM its whole process group on teardown
18
19
 
19
20
  # macOS oauth.authorize — ASWebAuthenticationSession (system-browser auth sheet, exact parity with the
20
21
  # iOS shell path in runtime/app/shell/handlers-oauth.ts: auto-close on redirect, user-cancel signal).
21
22
  [target.'cfg(target_os = "macos")'.dependencies]
23
+ # The generalized native webview primitive (per-profile store, eval, snapshot, popup, bridgeShim).
24
+ # objc2* versions MUST stay identical to those below — the WKWebView pointer crosses the boundary.
25
+ webview-control = { path = "../crates/webview-control" }
26
+ base64 = "0.22" # SPIKE: bridge_mac screenshot PNG encoding
22
27
  objc2 = "0.6"
23
28
  objc2-foundation = "0.3"
24
29
  objc2-app-kit = "0.3"