@livx.cc/appwrap 0.46.1 → 0.46.3
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.
- package/package.json +1 -1
- package/runtime/App_Resources/Android/src/main/AndroidManifest.xml +1 -0
- package/runtime/app/shell/custom-webview.android.ts +19 -2
- package/runtime-desktop/bridge-shim/shim.ts +12 -197
- package/runtime-desktop/chrome/appwrap-browser-chrome.html +120 -0
- package/runtime-desktop/crates/webview-control/Cargo.lock +330 -0
- package/runtime-desktop/crates/webview-control/Cargo.toml +28 -0
- package/runtime-desktop/crates/webview-control/examples/host_harness.rs +338 -0
- package/runtime-desktop/crates/webview-control/src/embedded.rs +121 -0
- package/runtime-desktop/crates/webview-control/src/lib.rs +99 -0
- package/runtime-desktop/crates/webview-control/src/popup.rs +522 -0
- package/runtime-desktop/crates/webview-control/src/profiles.rs +57 -0
- package/runtime-desktop/crates/webview-control/src/wkwebview_backend.rs +540 -0
- package/runtime-desktop/src-tauri/Cargo.lock +13 -0
- package/runtime-desktop/src-tauri/Cargo.toml +6 -1
- package/runtime-desktop/src-tauri/capabilities/default.json +1 -1
- package/runtime-desktop/src-tauri/src/bridge_mac.rs +126 -431
- package/runtime-desktop/src-tauri/src/browser_tab.rs +445 -0
- package/runtime-desktop/src-tauri/src/main.rs +90 -2
- package/runtime-desktop/src-tauri/src/popup_mac.rs +5 -519
- package/runtime-desktop/src-tauri/tauri.conf.json +1 -0
- package/src/cli.ts +26 -1
- package/src/config.ts +10 -1
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
// browser_tab — Tauri commands for an EMBEDDED, tear-off/dock-able browser tab (project "browser-tab"
|
|
2
|
+
// Phase 3). Thin front-end-facing wrapper over `webview_control::embedded::Controller`: the same live
|
|
3
|
+
// WKWebView (page + WKWebsiteDataStore session + bridgeShim) is mounted `ChildOf` a host pane NSView
|
|
4
|
+
// and torn off / docked with `set_mount`.
|
|
5
|
+
//
|
|
6
|
+
// R2 (agent-drive, DRY): the tab is created in the crate-level registry keyed by `id` (its label), so
|
|
7
|
+
// the control-channel adapter (`bridge_mac.rs`) — and thus the mcp-desktop-browser MCP — can drive
|
|
8
|
+
// this SAME embedded tab (navigate/eval/screenshot/close) by that id, with NO new TS<->Rust binding.
|
|
9
|
+
//
|
|
10
|
+
// Landmine (a): the pane NSView* is fetched + the webview mounted on the MAIN THREAD, AFTER the pane
|
|
11
|
+
// container has non-zero bounds (we create the container at the caller-supplied paneRect), else blank.
|
|
12
|
+
#[cfg(target_os = "macos")]
|
|
13
|
+
mod imp {
|
|
14
|
+
use std::collections::HashMap;
|
|
15
|
+
use std::ffi::c_void;
|
|
16
|
+
use std::sync::mpsc::channel;
|
|
17
|
+
use std::sync::{Mutex, OnceLock};
|
|
18
|
+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
19
|
+
|
|
20
|
+
use objc2::rc::Retained;
|
|
21
|
+
use objc2::{MainThreadMarker, MainThreadOnly};
|
|
22
|
+
use objc2_app_kit::{NSAutoresizingMaskOptions, NSView, NSWindow};
|
|
23
|
+
use objc2_foundation::{NSPoint, NSRect, NSSize};
|
|
24
|
+
use objc2_web_kit::WKWebView;
|
|
25
|
+
use serde::Deserialize;
|
|
26
|
+
use tauri::{Manager, WebviewUrl, WebviewWindowBuilder};
|
|
27
|
+
|
|
28
|
+
use webview_control::embedded::Controller;
|
|
29
|
+
use webview_control::{CreateSpec, Mount, ViewHandle};
|
|
30
|
+
|
|
31
|
+
/// Fixed height (points) of the sub-window chrome strip (‹ › ⟳ + address bar + profile▾). The
|
|
32
|
+
/// controlled WKWebView pane is pinned BELOW this within the sub-window's contentView.
|
|
33
|
+
pub const CHROME_H: f64 = 44.0;
|
|
34
|
+
|
|
35
|
+
/// Autoresizing mask keeping the controlled-webview container pinned below the chrome bar while it
|
|
36
|
+
/// tracks window resize: both margins fixed (44pt top gap, 0 bottom), width+height flexible.
|
|
37
|
+
fn container_mask() -> NSAutoresizingMaskOptions {
|
|
38
|
+
NSAutoresizingMaskOptions(
|
|
39
|
+
NSAutoresizingMaskOptions::ViewWidthSizable.0 | NSAutoresizingMaskOptions::ViewHeightSizable.0,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// A unique sub-window id / bridge tab label.
|
|
44
|
+
fn new_subwin_id() -> String {
|
|
45
|
+
let ms = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_millis()).unwrap_or(0);
|
|
46
|
+
format!("subwin-{ms}")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Web-DOM pane rect the front-end reserves (top-left origin, CSS px == points here since the
|
|
50
|
+
/// shell runs at the window's backing scale). We flip Y to AppKit's bottom-left origin at mount.
|
|
51
|
+
#[derive(Deserialize, Clone, Copy)]
|
|
52
|
+
pub struct PaneRect {
|
|
53
|
+
pub x: f64,
|
|
54
|
+
pub y: f64,
|
|
55
|
+
pub width: f64,
|
|
56
|
+
pub height: f64,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// id -> host pane container NSView* (as usize). The container is retained by being a subview of
|
|
60
|
+
// the window contentView; we hold only its pointer to reposition / reparent / remove it. Access
|
|
61
|
+
// is main-thread only (all commands marshal via run_on_main_thread), the Mutex just satisfies the
|
|
62
|
+
// `static` Send/Sync bound for the raw pointer.
|
|
63
|
+
fn containers() -> &'static Mutex<HashMap<String, usize>> {
|
|
64
|
+
static CONTAINERS: OnceLock<Mutex<HashMap<String, usize>>> = OnceLock::new();
|
|
65
|
+
CONTAINERS.get_or_init(|| Mutex::new(HashMap::new()))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
unsafe fn appkit_rect(content: &NSView, r: PaneRect) -> NSRect {
|
|
69
|
+
let ch = content.bounds().size.height;
|
|
70
|
+
NSRect::new(
|
|
71
|
+
NSPoint::new(r.x, ch - r.y - r.height),
|
|
72
|
+
NSSize::new(r.width, r.height),
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// The main window's contentView (parent of the shell WKWebView). Main-thread only.
|
|
77
|
+
unsafe fn content_view(win: &NSWindow) -> Retained<NSView> {
|
|
78
|
+
win.contentView().expect("main window has a contentView")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
pub fn create(
|
|
82
|
+
app: tauri::AppHandle,
|
|
83
|
+
id: String,
|
|
84
|
+
url: String,
|
|
85
|
+
profile_id: String,
|
|
86
|
+
rect: PaneRect,
|
|
87
|
+
) -> Result<(), String> {
|
|
88
|
+
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
89
|
+
// with_webview runs its closure on the AppKit main thread. Build the pane container at
|
|
90
|
+
// non-zero bounds FIRST (landmine a), then mount the webview ChildOf it — all on main.
|
|
91
|
+
let (tx, rx) = std::sync::mpsc::channel();
|
|
92
|
+
win.with_webview(move |w| {
|
|
93
|
+
let res = (|| -> Result<(), String> {
|
|
94
|
+
let mtm = MainThreadMarker::new().ok_or("not main thread")?;
|
|
95
|
+
unsafe {
|
|
96
|
+
let wk = &*(w.inner() as *mut WKWebView);
|
|
97
|
+
let ns_win = wk.window().ok_or("shell webview has no window")?;
|
|
98
|
+
let content = content_view(&ns_win);
|
|
99
|
+
let frame = appkit_rect(&content, rect);
|
|
100
|
+
let container = NSView::initWithFrame(NSView::alloc(mtm), frame);
|
|
101
|
+
content.addSubview(&container);
|
|
102
|
+
let cptr = Retained::as_ptr(&container) as *mut c_void;
|
|
103
|
+
let handle = ViewHandle::from_ptr(cptr);
|
|
104
|
+
let spec = CreateSpec {
|
|
105
|
+
label: id.clone(),
|
|
106
|
+
url: url.clone(),
|
|
107
|
+
profile_id: profile_id.clone(),
|
|
108
|
+
init_js: String::new(),
|
|
109
|
+
mount: Mount::ChildOf(handle),
|
|
110
|
+
};
|
|
111
|
+
Controller::new().create_webview(mtm, spec, Box::new(|_raw| {}))?;
|
|
112
|
+
// contentView holds a strong ref via addSubview; store only the pointer.
|
|
113
|
+
containers().lock().unwrap().insert(id.clone(), cptr as usize);
|
|
114
|
+
}
|
|
115
|
+
Ok(())
|
|
116
|
+
})();
|
|
117
|
+
let _ = tx.send(res);
|
|
118
|
+
})
|
|
119
|
+
.map_err(|e| e.to_string())?;
|
|
120
|
+
rx.recv().map_err(|e| e.to_string())?
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
pub fn set_mount(app: tauri::AppHandle, id: String, mode: String) -> Result<(), String> {
|
|
124
|
+
let (tx, rx) = std::sync::mpsc::channel();
|
|
125
|
+
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
126
|
+
win.run_on_main_thread(move || {
|
|
127
|
+
let res = (|| -> Result<(), String> {
|
|
128
|
+
let mtm = MainThreadMarker::new().ok_or("not main thread")?;
|
|
129
|
+
let mount = match mode.as_str() {
|
|
130
|
+
"toplevel" => Mount::TopLevel,
|
|
131
|
+
"child" => {
|
|
132
|
+
let ptr = *containers().lock().unwrap().get(&id).ok_or("no pane for id")?;
|
|
133
|
+
unsafe { Mount::ChildOf(ViewHandle::from_ptr(ptr as *mut c_void)) }
|
|
134
|
+
}
|
|
135
|
+
other => return Err(format!("bad mount mode: {other}")),
|
|
136
|
+
};
|
|
137
|
+
if Controller::new().set_mount(mtm, &id, mount) {
|
|
138
|
+
Ok(())
|
|
139
|
+
} else {
|
|
140
|
+
Err(format!("no webview {id}"))
|
|
141
|
+
}
|
|
142
|
+
})();
|
|
143
|
+
let _ = tx.send(res);
|
|
144
|
+
})
|
|
145
|
+
.map_err(|e| e.to_string())?;
|
|
146
|
+
rx.recv().map_err(|e| e.to_string())?
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
pub fn navigate(app: tauri::AppHandle, id: String, url: String) -> Result<(), String> {
|
|
150
|
+
let (tx, rx) = std::sync::mpsc::channel();
|
|
151
|
+
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
152
|
+
win.run_on_main_thread(move || {
|
|
153
|
+
let res = (|| -> Result<(), String> {
|
|
154
|
+
let mtm = MainThreadMarker::new().ok_or("not main thread")?;
|
|
155
|
+
Controller::new().navigate(mtm, &id, &url)
|
|
156
|
+
})();
|
|
157
|
+
let _ = tx.send(res);
|
|
158
|
+
})
|
|
159
|
+
.map_err(|e| e.to_string())?;
|
|
160
|
+
rx.recv().map_err(|e| e.to_string())?
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
pub fn close(app: tauri::AppHandle, id: String) -> Result<(), String> {
|
|
164
|
+
let (tx, rx) = std::sync::mpsc::channel();
|
|
165
|
+
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
166
|
+
win.run_on_main_thread(move || {
|
|
167
|
+
let res = (|| -> Result<(), String> {
|
|
168
|
+
let mtm = MainThreadMarker::new().ok_or("not main thread")?;
|
|
169
|
+
Controller::new().close(mtm, &id);
|
|
170
|
+
if let Some(ptr) = containers().lock().unwrap().remove(&id) {
|
|
171
|
+
unsafe {
|
|
172
|
+
let container = &*(ptr as *mut NSView);
|
|
173
|
+
container.removeFromSuperview();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
Ok(())
|
|
177
|
+
})();
|
|
178
|
+
let _ = tx.send(res);
|
|
179
|
+
})
|
|
180
|
+
.map_err(|e| e.to_string())?;
|
|
181
|
+
rx.recv().map_err(|e| e.to_string())?
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---- Sub-window: a standalone Tauri chrome window + a controlled WKWebView pane below the bar ----
|
|
185
|
+
|
|
186
|
+
/// Bridge init-JS + page sink for a sub-window webview, so it registers as a bod-browser tab when
|
|
187
|
+
/// the shim is running, and is inert-but-functional otherwise (empty init, no-op sink).
|
|
188
|
+
fn bridge_wiring(id: &str) -> (String, webview_control::MessageSink) {
|
|
189
|
+
let init_js = crate::bridge_mac::bridge_init_js(id).unwrap_or_default();
|
|
190
|
+
// Live self-healing sink: delivers to whichever shim is connected at send-time (None-safe),
|
|
191
|
+
// so a sub-window created while the shim is down becomes agent-exposed on a later connect.
|
|
192
|
+
let sink = crate::bridge_mac::bridge_sink();
|
|
193
|
+
(init_js, sink)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// Mount a controlled WKWebView `ChildOf` a pane pinned below the chrome bar in `win`'s content
|
|
197
|
+
/// view, wiring the bridge for agent-exposure. Runs on the main thread (via `with_webview`).
|
|
198
|
+
fn mount_pane(win: &tauri::WebviewWindow, id: &str, profile_id: &str, url: &str) -> Result<(), String> {
|
|
199
|
+
let (tx, rx) = channel();
|
|
200
|
+
let (id, profile_id, url) = (id.to_string(), profile_id.to_string(), url.to_string());
|
|
201
|
+
win.with_webview(move |w| {
|
|
202
|
+
let res = (|| -> Result<(), String> {
|
|
203
|
+
let mtm = MainThreadMarker::new().ok_or("not main thread")?;
|
|
204
|
+
unsafe {
|
|
205
|
+
let wk = &*(w.inner() as *mut WKWebView);
|
|
206
|
+
let ns_win = wk.window().ok_or("chrome webview has no window")?;
|
|
207
|
+
let content = content_view(&ns_win);
|
|
208
|
+
let b = content.bounds().size;
|
|
209
|
+
let rect = PaneRect { x: 0.0, y: CHROME_H, width: b.width, height: (b.height - CHROME_H).max(0.0) };
|
|
210
|
+
let frame = appkit_rect(&content, rect);
|
|
211
|
+
let container = NSView::initWithFrame(NSView::alloc(mtm), frame);
|
|
212
|
+
container.setAutoresizingMask(container_mask());
|
|
213
|
+
content.addSubview(&container);
|
|
214
|
+
let cptr = Retained::as_ptr(&container) as *mut c_void;
|
|
215
|
+
let handle = ViewHandle::from_ptr(cptr);
|
|
216
|
+
let (init_js, sink) = bridge_wiring(&id);
|
|
217
|
+
let spec = CreateSpec {
|
|
218
|
+
label: id.clone(),
|
|
219
|
+
url: url.clone(),
|
|
220
|
+
profile_id: profile_id.clone(),
|
|
221
|
+
init_js,
|
|
222
|
+
mount: Mount::ChildOf(handle),
|
|
223
|
+
};
|
|
224
|
+
Controller::new().create_webview(mtm, spec, sink)?;
|
|
225
|
+
containers().lock().unwrap().insert(id.clone(), cptr as usize);
|
|
226
|
+
}
|
|
227
|
+
Ok(())
|
|
228
|
+
})();
|
|
229
|
+
let _ = tx.send(res);
|
|
230
|
+
})
|
|
231
|
+
.map_err(|e| e.to_string())?;
|
|
232
|
+
rx.recv().map_err(|e| e.to_string())?
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// Clean up a sub-window's controlled webview + container. Main-thread only (called from the
|
|
236
|
+
/// window's Destroyed event, which fires on the main thread).
|
|
237
|
+
fn teardown(id: &str, mtm: MainThreadMarker) {
|
|
238
|
+
Controller::new().close(mtm, id);
|
|
239
|
+
if let Some(ptr) = containers().lock().unwrap().remove(id) {
|
|
240
|
+
unsafe { (*(ptr as *mut NSView)).removeFromSuperview() };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// Open a sub-window: build the Tauri chrome window (loads the bundled chrome HTML asset, `?id=`
|
|
245
|
+
/// stamped so it binds to this webview), then mount the controlled webview below the chrome bar.
|
|
246
|
+
pub fn open(app: tauri::AppHandle, profile_id: String) -> Result<String, String> {
|
|
247
|
+
// Two DISTINCT labels: the Tauri chrome window (`win_id`) and the controlled WKWebView it
|
|
248
|
+
// hosts (`view_id = <win_id>-view`). Sharing one label double-listed it in `listWindows`
|
|
249
|
+
// (once from `app.webview_windows()`, once from the crate registry). The chrome HTML, its
|
|
250
|
+
// invokes, the container/registry key, the bridge `__BRIDGE_LABEL`, and what agents drive all
|
|
251
|
+
// key on `view_id`; only the OS window keeps `win_id`.
|
|
252
|
+
let win_id = new_subwin_id();
|
|
253
|
+
let view_id = format!("{win_id}-view");
|
|
254
|
+
let chrome_url = format!("appwrap-browser-chrome.html?id={view_id}");
|
|
255
|
+
let win = WebviewWindowBuilder::new(&app, &win_id, WebviewUrl::App(chrome_url.into()))
|
|
256
|
+
.title("Browser")
|
|
257
|
+
.inner_size(1100.0, 800.0)
|
|
258
|
+
.build()
|
|
259
|
+
.map_err(|e| e.to_string())?;
|
|
260
|
+
// Tear the controlled webview down when the sub-window closes, so it doesn't linger as a
|
|
261
|
+
// registry zombie / stray bod-browser tab.
|
|
262
|
+
let idc = view_id.clone();
|
|
263
|
+
win.on_window_event(move |ev| {
|
|
264
|
+
if matches!(ev, tauri::WindowEvent::Destroyed) {
|
|
265
|
+
if let Some(mtm) = MainThreadMarker::new() {
|
|
266
|
+
teardown(&idc, mtm);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
mount_pane(&win, &view_id, &profile_id, "about:blank")?;
|
|
271
|
+
Ok(view_id)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// Re-fire the `register` beacon on every open sub-window controlled webview. Called when the
|
|
275
|
+
/// shim (re)connects: their live sink can now deliver, but an already-loaded document won't
|
|
276
|
+
/// re-run its init-JS `register()`, so nudge each via the bridgeShim handler to re-post it.
|
|
277
|
+
/// Idempotent + inert when the bridge feature is off (no bridgeShim handler → the try/catch eats
|
|
278
|
+
/// it) and when there are no sub-windows.
|
|
279
|
+
pub fn reregister_all(app: &tauri::AppHandle) {
|
|
280
|
+
let ids: Vec<String> = containers().lock().unwrap().keys().cloned().collect();
|
|
281
|
+
if ids.is_empty() {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
let _ = app.run_on_main_thread(move || {
|
|
285
|
+
if let Some(mtm) = MainThreadMarker::new() {
|
|
286
|
+
let c = Controller::new();
|
|
287
|
+
for id in &ids {
|
|
288
|
+
let js = "try{if(window.__BRIDGE_LABEL){webkit.messageHandlers.bridgeShim.postMessage(JSON.stringify({label:window.__BRIDGE_LABEL,type:'register',url:location.href,title:document.title}))}}catch(e){}".to_string();
|
|
289
|
+
let _ = c.inject_script(mtm, id, &js);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/// Swap the sub-window's controlled webview to another profile: profile is immutable per-webview,
|
|
296
|
+
/// so close the current one (keeps the pane container) and recreate `ChildOf` the SAME container
|
|
297
|
+
/// with the new profile. The Tauri chrome window is untouched.
|
|
298
|
+
pub fn switch_profile(app: tauri::AppHandle, id: String, profile_id: String) -> Result<(), String> {
|
|
299
|
+
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
300
|
+
let (tx, rx) = channel();
|
|
301
|
+
win.run_on_main_thread(move || {
|
|
302
|
+
let res = (|| -> Result<(), String> {
|
|
303
|
+
let mtm = MainThreadMarker::new().ok_or("not main thread")?;
|
|
304
|
+
let ptr = *containers().lock().unwrap().get(&id).ok_or("no pane for id")?;
|
|
305
|
+
Controller::new().close(mtm, &id);
|
|
306
|
+
unsafe {
|
|
307
|
+
let handle = ViewHandle::from_ptr(ptr as *mut c_void);
|
|
308
|
+
let (init_js, sink) = bridge_wiring(&id);
|
|
309
|
+
let spec = CreateSpec {
|
|
310
|
+
label: id.clone(),
|
|
311
|
+
url: "about:blank".to_string(),
|
|
312
|
+
profile_id: profile_id.clone(),
|
|
313
|
+
init_js,
|
|
314
|
+
mount: Mount::ChildOf(handle),
|
|
315
|
+
};
|
|
316
|
+
Controller::new().create_webview(mtm, spec, sink)?;
|
|
317
|
+
}
|
|
318
|
+
Ok(())
|
|
319
|
+
})();
|
|
320
|
+
let _ = tx.send(res);
|
|
321
|
+
})
|
|
322
|
+
.map_err(|e| e.to_string())?;
|
|
323
|
+
rx.recv().map_err(|e| e.to_string())?
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/// Route a nav op (`back`/`forward`/`reload`) to the controlled webview `id` on the main thread.
|
|
327
|
+
pub fn nav_op(app: tauri::AppHandle, id: String, op: &'static str) -> Result<(), String> {
|
|
328
|
+
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
329
|
+
let (tx, rx) = channel();
|
|
330
|
+
win.run_on_main_thread(move || {
|
|
331
|
+
let res = (|| -> Result<(), String> {
|
|
332
|
+
let mtm = MainThreadMarker::new().ok_or("not main thread")?;
|
|
333
|
+
let c = Controller::new();
|
|
334
|
+
match op {
|
|
335
|
+
"back" => c.go_back(mtm, &id),
|
|
336
|
+
"forward" => c.go_forward(mtm, &id),
|
|
337
|
+
"reload" => c.reload(mtm, &id),
|
|
338
|
+
other => Err(format!("bad nav op: {other}")),
|
|
339
|
+
}
|
|
340
|
+
})();
|
|
341
|
+
let _ = tx.send(res);
|
|
342
|
+
})
|
|
343
|
+
.map_err(|e| e.to_string())?;
|
|
344
|
+
rx.recv().map_err(|e| e.to_string())?
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/// Read the controlled webview `id`'s current `{href,title}` as a JSON string (for the address bar).
|
|
348
|
+
pub fn current_url(app: tauri::AppHandle, id: String) -> Result<String, String> {
|
|
349
|
+
let win = app.get_webview_window("main").ok_or("no main window")?;
|
|
350
|
+
let (tx, rx) = channel();
|
|
351
|
+
win.run_on_main_thread(move || {
|
|
352
|
+
let mtm = match MainThreadMarker::new() {
|
|
353
|
+
Some(m) => m,
|
|
354
|
+
None => { let _ = tx.send(Err("not main thread".to_string())); return; }
|
|
355
|
+
};
|
|
356
|
+
Controller::new().eval_async(
|
|
357
|
+
mtm,
|
|
358
|
+
&id,
|
|
359
|
+
"return JSON.stringify({href:location.href,title:document.title});".to_string(),
|
|
360
|
+
move |r| { let _ = tx.send(r); },
|
|
361
|
+
);
|
|
362
|
+
})
|
|
363
|
+
.map_err(|e| e.to_string())?;
|
|
364
|
+
rx.recv_timeout(Duration::from_secs(5)).map_err(|e| e.to_string())?
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
#[cfg(target_os = "macos")]
|
|
369
|
+
#[tauri::command]
|
|
370
|
+
pub fn browser_tab_create(
|
|
371
|
+
app: tauri::AppHandle,
|
|
372
|
+
id: String,
|
|
373
|
+
url: String,
|
|
374
|
+
profile_id: String,
|
|
375
|
+
rect: imp::PaneRect,
|
|
376
|
+
) -> Result<String, String> {
|
|
377
|
+
imp::create(app, id.clone(), url, profile_id, rect).map(|_| id)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
#[cfg(target_os = "macos")]
|
|
381
|
+
#[tauri::command]
|
|
382
|
+
pub fn browser_tab_set_mount(app: tauri::AppHandle, id: String, mode: String) -> Result<(), String> {
|
|
383
|
+
imp::set_mount(app, id, mode)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
#[cfg(target_os = "macos")]
|
|
387
|
+
#[tauri::command]
|
|
388
|
+
pub fn browser_tab_navigate(app: tauri::AppHandle, id: String, url: String) -> Result<(), String> {
|
|
389
|
+
imp::navigate(app, id, url)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
#[cfg(target_os = "macos")]
|
|
393
|
+
#[tauri::command]
|
|
394
|
+
pub fn browser_tab_close(app: tauri::AppHandle, id: String) -> Result<(), String> {
|
|
395
|
+
imp::close(app, id)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
#[cfg(target_os = "macos")]
|
|
399
|
+
#[tauri::command]
|
|
400
|
+
pub fn browser_subwindow_open(app: tauri::AppHandle, profile_id: Option<String>) -> Result<String, String> {
|
|
401
|
+
imp::open(app, profile_id.unwrap_or_else(|| "default".to_string()))
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
#[cfg(target_os = "macos")]
|
|
405
|
+
#[tauri::command]
|
|
406
|
+
pub fn browser_subwindow_switch_profile(app: tauri::AppHandle, id: String, profile_id: String) -> Result<(), String> {
|
|
407
|
+
imp::switch_profile(app, id, profile_id)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
#[cfg(target_os = "macos")]
|
|
411
|
+
#[tauri::command]
|
|
412
|
+
pub fn browser_tab_back(app: tauri::AppHandle, id: String) -> Result<(), String> {
|
|
413
|
+
imp::nav_op(app, id, "back")
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
#[cfg(target_os = "macos")]
|
|
417
|
+
#[tauri::command]
|
|
418
|
+
pub fn browser_tab_forward(app: tauri::AppHandle, id: String) -> Result<(), String> {
|
|
419
|
+
imp::nav_op(app, id, "forward")
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
#[cfg(target_os = "macos")]
|
|
423
|
+
#[tauri::command]
|
|
424
|
+
pub fn browser_tab_reload(app: tauri::AppHandle, id: String) -> Result<(), String> {
|
|
425
|
+
imp::nav_op(app, id, "reload")
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
#[cfg(target_os = "macos")]
|
|
429
|
+
#[tauri::command]
|
|
430
|
+
pub fn browser_tab_current_url(app: tauri::AppHandle, id: String) -> Result<String, String> {
|
|
431
|
+
imp::current_url(app, id)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
#[cfg(target_os = "macos")]
|
|
435
|
+
#[tauri::command]
|
|
436
|
+
pub fn browser_list_profiles() -> Result<Vec<String>, String> {
|
|
437
|
+
Ok(webview_control::list_profiles())
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/// Nudge every open browser sub-window to re-post its `register` beacon (called by the bridge control
|
|
441
|
+
/// channel when a shim (re)connects, so pre-existing sub-windows self-heal into agent-exposure).
|
|
442
|
+
#[cfg(target_os = "macos")]
|
|
443
|
+
pub fn reregister_subwindows(app: &tauri::AppHandle) {
|
|
444
|
+
imp::reregister_all(app);
|
|
445
|
+
}
|
|
@@ -17,6 +17,7 @@ use tauri::Manager;
|
|
|
17
17
|
mod biometrics_mac;
|
|
18
18
|
#[cfg(target_os = "macos")]
|
|
19
19
|
mod bridge_mac;
|
|
20
|
+
mod browser_tab;
|
|
20
21
|
#[cfg(target_os = "macos")]
|
|
21
22
|
mod notifications_mac;
|
|
22
23
|
#[cfg(target_os = "macos")]
|
|
@@ -688,7 +689,20 @@ fn main() {
|
|
|
688
689
|
builder = builder.plugin(tauri_plugin_window_state::Builder::default().build());
|
|
689
690
|
}
|
|
690
691
|
builder
|
|
691
|
-
.invoke_handler(tauri::generate_handler![
|
|
692
|
+
.invoke_handler(tauri::generate_handler![
|
|
693
|
+
appwrap_invoke,
|
|
694
|
+
browser_tab::browser_tab_create,
|
|
695
|
+
browser_tab::browser_tab_set_mount,
|
|
696
|
+
browser_tab::browser_tab_navigate,
|
|
697
|
+
browser_tab::browser_tab_close,
|
|
698
|
+
browser_tab::browser_subwindow_open,
|
|
699
|
+
browser_tab::browser_subwindow_switch_profile,
|
|
700
|
+
browser_tab::browser_tab_back,
|
|
701
|
+
browser_tab::browser_tab_forward,
|
|
702
|
+
browser_tab::browser_tab_reload,
|
|
703
|
+
browser_tab::browser_tab_current_url,
|
|
704
|
+
browser_tab::browser_list_profiles,
|
|
705
|
+
])
|
|
692
706
|
// Popups are plain NSWindows tauri doesn't know about; with the main (only tauri) window
|
|
693
707
|
// closed, tauri exits the app — but tear popups down explicitly so no dangling windows or
|
|
694
708
|
// live child webviews outlive the shell during teardown (or if the exit is ever prevented).
|
|
@@ -707,6 +721,75 @@ fn main() {
|
|
|
707
721
|
.setup(|app| {
|
|
708
722
|
// Stash the app handle so off-main-thread handlers can reach plugin state (notifications).
|
|
709
723
|
let _ = APP.set(app.handle().clone());
|
|
724
|
+
// Native "Browser ▸ New Browser Window" menu — every appwrap desktop app gets the
|
|
725
|
+
// app-owned browser sub-window affordance. We APPEND to the DEFAULT macOS menu (built via
|
|
726
|
+
// Menu::default) so Edit's Cut/Copy/Paste/Select-All stay intact; suppressing them would
|
|
727
|
+
// break Cmd+C/V in inputs. The menu event opens a sub-window for the "default" profile.
|
|
728
|
+
{
|
|
729
|
+
use tauri::menu::{Menu, MenuItem, Submenu};
|
|
730
|
+
let handle = app.handle();
|
|
731
|
+
match Menu::default(handle).and_then(|menu| {
|
|
732
|
+
let new_win = MenuItem::with_id(
|
|
733
|
+
handle, "browser_new_window", "New Browser Window", true,
|
|
734
|
+
Some("CmdOrCtrl+Shift+N"),
|
|
735
|
+
)?;
|
|
736
|
+
let browser = Submenu::with_id_and_items(
|
|
737
|
+
handle, "browser_menu", "Browser", true, &[&new_win],
|
|
738
|
+
)?;
|
|
739
|
+
menu.append(&browser)?;
|
|
740
|
+
// View ▸ Toggle DevTools (⌥⌘I). Opens/closes Tauri's devtools on the focused
|
|
741
|
+
// window; the handler no-ops in release unless APPWRAP_DEVTOOLS is set
|
|
742
|
+
// (devtools_enabled), matching the Safari-inspectable gate.
|
|
743
|
+
let toggle_devtools = MenuItem::with_id(
|
|
744
|
+
handle, "toggle_devtools", "Toggle DevTools", true,
|
|
745
|
+
Some("Alt+CmdOrCtrl+I"),
|
|
746
|
+
)?;
|
|
747
|
+
let view = Submenu::with_id_and_items(
|
|
748
|
+
handle, "view_menu", "View", true, &[&toggle_devtools],
|
|
749
|
+
)?;
|
|
750
|
+
menu.append(&view)?;
|
|
751
|
+
Ok(menu)
|
|
752
|
+
}) {
|
|
753
|
+
Ok(menu) => {
|
|
754
|
+
if let Err(e) = app.set_menu(menu) {
|
|
755
|
+
eprintln!("[browser] set_menu failed: {e}");
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
Err(e) => eprintln!("[browser] build Browser menu failed: {e}"),
|
|
759
|
+
}
|
|
760
|
+
app.on_menu_event(|app, event| {
|
|
761
|
+
match event.id().as_ref() {
|
|
762
|
+
"browser_new_window" => {
|
|
763
|
+
if let Err(e) = browser_tab::browser_subwindow_open(app.clone(), Some("default".to_string())) {
|
|
764
|
+
eprintln!("[browser] open sub-window (menu) failed: {e}");
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
"toggle_devtools" => {
|
|
768
|
+
// Gate at runtime: inert in a release build unless APPWRAP_DEVTOOLS is set
|
|
769
|
+
// (same policy as Safari-inspectable); always on in debug.
|
|
770
|
+
#[cfg(target_os = "macos")]
|
|
771
|
+
if !webview_control::devtools_enabled() {
|
|
772
|
+
eprintln!("[devtools] toggle ignored (set APPWRAP_DEVTOOLS=1 to enable in release)");
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
// Toggle on the focused webview window (sub-windows are their own
|
|
776
|
+
// WebviewWindows), falling back to main.
|
|
777
|
+
let win = app.webview_windows()
|
|
778
|
+
.into_values()
|
|
779
|
+
.find(|w| w.is_focused().unwrap_or(false))
|
|
780
|
+
.or_else(|| app.get_webview_window("main"));
|
|
781
|
+
if let Some(win) = win {
|
|
782
|
+
if win.is_devtools_open() {
|
|
783
|
+
win.close_devtools();
|
|
784
|
+
} else {
|
|
785
|
+
win.open_devtools();
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
_ => {}
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
}
|
|
710
793
|
// SPIKE: browser-bridge window-ops backend — inert unless APPWRAP_BRIDGE_JS is set.
|
|
711
794
|
#[cfg(target_os = "macos")]
|
|
712
795
|
bridge_mac::init(app.handle().clone());
|
|
@@ -748,7 +831,12 @@ fn main() {
|
|
|
748
831
|
#[cfg(target_os = "macos")]
|
|
749
832
|
if let Some(window) = app.get_webview_window("main") {
|
|
750
833
|
let picker = shell().google_account_picker;
|
|
751
|
-
window.with_webview(move |w|
|
|
834
|
+
window.with_webview(move |w| {
|
|
835
|
+
popup_mac::install(w.inner() as _, picker);
|
|
836
|
+
// Safari Web Inspector on the main Tauri window's WKWebView (debug always;
|
|
837
|
+
// release only under APPWRAP_DEVTOOLS). Runs on the main thread (with_webview).
|
|
838
|
+
unsafe { webview_control::set_inspectable(w.inner() as *mut _) };
|
|
839
|
+
})?;
|
|
752
840
|
}
|
|
753
841
|
// Reveal after the state plugin's on_window_ready restore has applied (built hidden above).
|
|
754
842
|
if restore {
|