@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,121 @@
|
|
|
1
|
+
//! Driving adapter (a): the in-process Rust API for EMBEDDED hosts (para-chat / unclaw-app).
|
|
2
|
+
//!
|
|
3
|
+
//! Same command surface as the control-channel adapter (bridge_mac.rs, driving adapter (b)) but
|
|
4
|
+
//! with NO socket, NO JSON framing — a plain Rust object over the one `WebviewControl` contract.
|
|
5
|
+
//! An embedded host that links this crate creates an in-app browser tab as `ChildOf(paneView)` and
|
|
6
|
+
//! tears it out / docks it back with `set_mount` — the same live WKWebView, session + bridge intact.
|
|
7
|
+
//!
|
|
8
|
+
//! Threading: every method takes a `MainThreadMarker` and runs synchronously on the AppKit main
|
|
9
|
+
//! thread. The facade deliberately has NO built-in `run_on_main_thread` — the host owns marshalling
|
|
10
|
+
//! (Tauri's `run_on_main_thread`, or being called from the main runloop). WKWebView/NSView are not
|
|
11
|
+
//! thread-safe, and the crate must not assume a Tauri `AppHandle`.
|
|
12
|
+
|
|
13
|
+
use objc2::rc::Retained;
|
|
14
|
+
use objc2::MainThreadMarker;
|
|
15
|
+
use objc2_web_kit::WKWebView;
|
|
16
|
+
|
|
17
|
+
use crate::wkwebview_backend as backend;
|
|
18
|
+
use crate::{CreateResult, CreateSpec, MessageSink, Mount, WebviewInfo};
|
|
19
|
+
|
|
20
|
+
/// In-process facade over the webview primitive. Stateless — the webview registry is a crate-level
|
|
21
|
+
/// static — so it is a zero-cost handle the host can construct freely.
|
|
22
|
+
#[derive(Clone, Copy, Default)]
|
|
23
|
+
pub struct Controller;
|
|
24
|
+
|
|
25
|
+
impl Controller {
|
|
26
|
+
pub fn new() -> Self {
|
|
27
|
+
Controller
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Create a webview mounted per `spec.mount` (`ChildOf(pane)` for an embedded tab). `on_message`
|
|
31
|
+
/// receives RAW page->host payloads (`webkit.messageHandlers.bridgeShim.postMessage`).
|
|
32
|
+
pub fn create_webview(
|
|
33
|
+
&self,
|
|
34
|
+
mtm: MainThreadMarker,
|
|
35
|
+
spec: CreateSpec,
|
|
36
|
+
on_message: MessageSink,
|
|
37
|
+
) -> Result<CreateResult, String> {
|
|
38
|
+
backend::create_webview(mtm, spec, on_message)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Tear-off / dock: move the live webview between `TopLevel` and `ChildOf(pane)`. False if unknown.
|
|
42
|
+
pub fn set_mount(&self, mtm: MainThreadMarker, id: &str, mount: Mount) -> bool {
|
|
43
|
+
backend::set_mount(mtm, id, mount)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Navigate the webview `id` to `url`. Err if `id` is unknown/closed.
|
|
47
|
+
pub fn navigate(&self, _mtm: MainThreadMarker, id: &str, url: &str) -> Result<(), String> {
|
|
48
|
+
let wk = self.webview(id)?;
|
|
49
|
+
unsafe { backend::navigate(Retained::as_ptr(&wk) as *mut WKWebView, url) }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Navigate back in webview `id`'s history. Err if `id` is unknown/closed.
|
|
53
|
+
pub fn go_back(&self, _mtm: MainThreadMarker, id: &str) -> Result<(), String> {
|
|
54
|
+
let wk = self.webview(id)?;
|
|
55
|
+
unsafe { backend::go_back(Retained::as_ptr(&wk) as *mut WKWebView) };
|
|
56
|
+
Ok(())
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Navigate forward in webview `id`'s history. Err if `id` is unknown/closed.
|
|
60
|
+
pub fn go_forward(&self, _mtm: MainThreadMarker, id: &str) -> Result<(), String> {
|
|
61
|
+
let wk = self.webview(id)?;
|
|
62
|
+
unsafe { backend::go_forward(Retained::as_ptr(&wk) as *mut WKWebView) };
|
|
63
|
+
Ok(())
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Reload webview `id`. Err if `id` is unknown/closed.
|
|
67
|
+
pub fn reload(&self, _mtm: MainThreadMarker, id: &str) -> Result<(), String> {
|
|
68
|
+
let wk = self.webview(id)?;
|
|
69
|
+
unsafe { backend::reload(Retained::as_ptr(&wk) as *mut WKWebView) };
|
|
70
|
+
Ok(())
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Fire-and-forget JS injection into webview `id`. Err if `id` is unknown/closed.
|
|
74
|
+
pub fn inject_script(&self, _mtm: MainThreadMarker, id: &str, js: &str) -> Result<(), String> {
|
|
75
|
+
let wk = self.webview(id)?;
|
|
76
|
+
unsafe { backend::inject_script(Retained::as_ptr(&wk) as *mut WKWebView, js) };
|
|
77
|
+
Ok(())
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// CSP-immune eval (callAsyncJavaScript, `js` is a function body). Delivers the RAW return
|
|
81
|
+
/// string via `on_done` on the main thread. Err (via `on_done`) if `id` is unknown/closed.
|
|
82
|
+
pub fn eval_async(
|
|
83
|
+
&self,
|
|
84
|
+
_mtm: MainThreadMarker,
|
|
85
|
+
id: &str,
|
|
86
|
+
js: String,
|
|
87
|
+
on_done: impl FnOnce(Result<String, String>) + 'static,
|
|
88
|
+
) {
|
|
89
|
+
match self.webview(id) {
|
|
90
|
+
Ok(wk) => unsafe { backend::eval_async(Retained::as_ptr(&wk) as *mut WKWebView, js, on_done) },
|
|
91
|
+
Err(e) => on_done(Err(e)),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Snapshot webview `id` to PNG bytes via `on_done` on the main thread.
|
|
96
|
+
pub fn snapshot(
|
|
97
|
+
&self,
|
|
98
|
+
_mtm: MainThreadMarker,
|
|
99
|
+
id: &str,
|
|
100
|
+
on_done: impl FnOnce(Result<Vec<u8>, String>) + 'static,
|
|
101
|
+
) {
|
|
102
|
+
match self.webview(id) {
|
|
103
|
+
Ok(wk) => unsafe { backend::snapshot(Retained::as_ptr(&wk) as *mut WKWebView, on_done) },
|
|
104
|
+
Err(e) => on_done(Err(e)),
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Close + drop the webview `id` (releases its window + view). False if unknown.
|
|
109
|
+
pub fn close(&self, _mtm: MainThreadMarker, id: &str) -> bool {
|
|
110
|
+
backend::close(id)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Live (non-closed) webviews.
|
|
114
|
+
pub fn list_webviews(&self) -> Vec<WebviewInfo> {
|
|
115
|
+
backend::list_webviews()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fn webview(&self, id: &str) -> Result<Retained<WKWebView>, String> {
|
|
119
|
+
backend::profile_webview(id).ok_or_else(|| format!("no webview {id}"))
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//! webview-control — appwrap's generalized, automation-agnostic native webview primitive.
|
|
2
|
+
//!
|
|
3
|
+
//! Carved out of `src-tauri/src/bridge_mac.rs` (Phase 1). It owns the parts that MUST be native +
|
|
4
|
+
//! in-process + appwrap-owned because `WKWebsiteDataStore` is immutable post-webview-creation:
|
|
5
|
+
//! per-profile persistent+isolated stores, manual NSWindow+WKWebView creation, CSP-immune eval,
|
|
6
|
+
//! snapshot, the `bridgeShim` WKScriptMessageHandler, and the SSO-popup delegate.
|
|
7
|
+
//!
|
|
8
|
+
//! Seam purity: the primitive emits RAW page payloads (`MessageSink`) and RAW eval/snapshot results
|
|
9
|
+
//! (per-call `on_done` callbacks). It holds NO bod-browser protocol, transport `Sender`, or
|
|
10
|
+
//! `{type:"pageMsg"}` / `{id,result}` framing — those live in the control-channel adapter
|
|
11
|
+
//! (`bridge_mac.rs`). The `WkWebViewBackend` is macOS-only; a `WebKitGtkBackend` is a future slot.
|
|
12
|
+
//!
|
|
13
|
+
//! Two driving adapters over ONE contract:
|
|
14
|
+
//! - (a) [`embedded::Controller`] — in-process Rust API for EMBEDDED hosts (para-chat / unclaw-app);
|
|
15
|
+
//! creates tabs as [`Mount::ChildOf`] a host pane and tears off / docks with [`set_mount`].
|
|
16
|
+
//! - (b) the control-channel socket (`bridge_mac.rs`) — standalone / MCP; only ever [`Mount::TopLevel`].
|
|
17
|
+
//!
|
|
18
|
+
//! Cross-repo consumption (R1, resolved Phase 2): external hosts pin this crate as a **git dependency
|
|
19
|
+
//! on a tagged appwrap rev** — `webview-control = { git = ".../appwrap", tag = "webview-control-vX" }`
|
|
20
|
+
//! (no crates.io publish). appwrap-generated apps (unclaw-app) already get it via the whole-tree
|
|
21
|
+
//! `native-desktop` copy; para-chat's hand-maintained `src-tauri` adds the git dep directly. objc2*
|
|
22
|
+
//! versions come pinned by the tagged rev so the `*mut WKWebView`/`*mut NSView` pointers that cross
|
|
23
|
+
//! this boundary never hit an ABI mismatch.
|
|
24
|
+
|
|
25
|
+
#[cfg(target_os = "macos")]
|
|
26
|
+
pub mod embedded;
|
|
27
|
+
#[cfg(target_os = "macos")]
|
|
28
|
+
pub mod popup;
|
|
29
|
+
#[cfg(target_os = "macos")]
|
|
30
|
+
mod profiles;
|
|
31
|
+
#[cfg(target_os = "macos")]
|
|
32
|
+
mod wkwebview_backend;
|
|
33
|
+
|
|
34
|
+
use std::ffi::c_void;
|
|
35
|
+
|
|
36
|
+
/// Opaque handle to a host-owned `NSView` that a webview can be mounted inside (Phase 2).
|
|
37
|
+
///
|
|
38
|
+
/// # Safety contract
|
|
39
|
+
/// The wrapped pointer MUST be a valid `NSView*` (or subclass) for the ENTIRE time a webview is
|
|
40
|
+
/// mounted in it. The caller guarantees, and the crate relies on, all of:
|
|
41
|
+
/// - **Validity:** a live `NSView` created and retained by the *host* (e.g. a Tauri pane's
|
|
42
|
+
/// `ns_view()` / an AppKit container). Never a dangling, freed, or non-`NSView` pointer.
|
|
43
|
+
/// - **Main-thread only:** the handle is created, passed, and dereferenced ONLY on the AppKit main
|
|
44
|
+
/// thread. `ViewHandle` is intentionally `!Send`/`!Sync` — it must not cross threads.
|
|
45
|
+
/// - **Host-owned lifetime:** the host owns the pane and MUST NOT drop/free it while a webview is
|
|
46
|
+
/// mounted in it. Reparent the webview out (`set_mount(TopLevel)`) or `close` it BEFORE tearing
|
|
47
|
+
/// down the pane. The crate holds only a borrow-strength AppKit subview link, not ownership of
|
|
48
|
+
/// the host pane.
|
|
49
|
+
/// - **Laid-out bounds (Phase-3 landmine a):** fetch the `NSView*` and mount `ChildOf` it only
|
|
50
|
+
/// AFTER the pane has been laid out (non-zero `bounds`). The webview is framed to the pane's
|
|
51
|
+
/// bounds at mount; a zero-bounds pane (handle taken before layout) renders BLANK until a resize.
|
|
52
|
+
/// Take the handle post-layout, on the main thread.
|
|
53
|
+
/// - **objc2 ABI match (Phase-3 landmine b):** a host with its OWN `wry`/`objc2` tree (e.g.
|
|
54
|
+
/// para-chat's `src-tauri`) MUST resolve `objc2*` to the SAME `0.6.x` this crate is built against
|
|
55
|
+
/// (pinned by the tagged rev). A divergent objc2 makes the `*mut NSView` pointer cast UB in the
|
|
56
|
+
/// host's tree — pin/dedupe objc2 across the workspace, don't just add the git dep.
|
|
57
|
+
#[derive(Clone, Copy)]
|
|
58
|
+
pub struct ViewHandle(*mut c_void);
|
|
59
|
+
|
|
60
|
+
impl ViewHandle {
|
|
61
|
+
/// Wrap a raw host `NSView*`. See the type-level safety contract — the caller upholds validity,
|
|
62
|
+
/// main-thread affinity, and host-owned lifetime.
|
|
63
|
+
///
|
|
64
|
+
/// # Safety
|
|
65
|
+
/// `ptr` must be a valid `NSView*` per the [`ViewHandle`] contract.
|
|
66
|
+
pub unsafe fn from_ptr(ptr: *mut c_void) -> Self {
|
|
67
|
+
ViewHandle(ptr)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// The raw `NSView*`. Main-thread only.
|
|
71
|
+
pub fn as_ptr(self) -> *mut c_void {
|
|
72
|
+
self.0
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Where a webview lives in the AppKit view tree (Phase 2 tear-off / dock).
|
|
77
|
+
/// - `TopLevel` — the webview owns/fills its own `NSWindow` content view (the standalone/MCP shape;
|
|
78
|
+
/// the only mount the control-channel adapter ever requests, so its behavior is unchanged).
|
|
79
|
+
/// - `ChildOf(handle)` — the webview is a subview of a host-owned pane `NSView`; the same live
|
|
80
|
+
/// `WKWebView` (page + `WKWebsiteDataStore` session + bridge) survives moving between the two.
|
|
81
|
+
#[derive(Clone, Copy)]
|
|
82
|
+
pub enum Mount {
|
|
83
|
+
TopLevel,
|
|
84
|
+
ChildOf(ViewHandle),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The Phase-1 primitive surface — a flat, transport-free command set the control-channel adapter
|
|
88
|
+
// drives — plus the Phase-2 reparent op (`set_mount`) and the embedded facade (`embedded::Controller`).
|
|
89
|
+
// See ARCHITECTURE.md for the full contract.
|
|
90
|
+
#[cfg(target_os = "macos")]
|
|
91
|
+
pub use wkwebview_backend::{
|
|
92
|
+
close, create_profile_window, create_webview, devtools_enabled, eval_async, go_back, go_forward,
|
|
93
|
+
inject_script, install_message_handler, list_webviews, navigate, profile_webview, reload,
|
|
94
|
+
set_inspectable, set_mount, snapshot, user_close, CreateResult, CreateSpec, MessageSink,
|
|
95
|
+
WebviewInfo,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
#[cfg(target_os = "macos")]
|
|
99
|
+
pub use profiles::list as list_profiles;
|