@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.
- package/package.json +1 -1
- package/runtime/App_Resources/Android/src/main/AndroidManifest.xml +1 -0
- package/runtime-desktop/bridge-shim/build.sh +9 -0
- package/runtime-desktop/bridge-shim/content-entry.ts +87 -0
- package/runtime-desktop/bridge-shim/shim.ts +132 -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 +245 -0
- package/runtime-desktop/crates/webview-control/src/embedded.rs +100 -0
- package/runtime-desktop/crates/webview-control/src/lib.rs +95 -0
- package/runtime-desktop/crates/webview-control/src/popup.rs +519 -0
- package/runtime-desktop/crates/webview-control/src/profiles.rs +45 -0
- package/runtime-desktop/crates/webview-control/src/wkwebview_backend.rs +497 -0
- package/runtime-desktop/src-tauri/Cargo.lock +15 -0
- package/runtime-desktop/src-tauri/Cargo.toml +5 -0
- package/runtime-desktop/src-tauri/src/bridge_mac.rs +306 -0
- package/runtime-desktop/src-tauri/src/browser_tab.rs +193 -0
- package/runtime-desktop/src-tauri/src/main.rs +206 -3
- package/runtime-desktop/src-tauri/src/popup_mac.rs +5 -466
- package/runtime-desktop/src-tauri/src/server.rs +156 -0
- package/runtime-desktop/src-tauri/tauri.conf.json +1 -0
- package/src/cli.ts +86 -13
- package/src/config.ts +46 -1
- package/src/desktop.ts +48 -1
|
@@ -16,6 +16,9 @@ use tauri::Manager;
|
|
|
16
16
|
#[cfg(target_os = "macos")]
|
|
17
17
|
mod biometrics_mac;
|
|
18
18
|
#[cfg(target_os = "macos")]
|
|
19
|
+
mod bridge_mac;
|
|
20
|
+
mod browser_tab;
|
|
21
|
+
#[cfg(target_os = "macos")]
|
|
19
22
|
mod notifications_mac;
|
|
20
23
|
#[cfg(target_os = "macos")]
|
|
21
24
|
mod oauth_mac;
|
|
@@ -24,6 +27,8 @@ mod popup_mac;
|
|
|
24
27
|
#[cfg(target_os = "macos")]
|
|
25
28
|
mod push_mac;
|
|
26
29
|
#[cfg(target_os = "macos")]
|
|
30
|
+
mod server;
|
|
31
|
+
#[cfg(target_os = "macos")]
|
|
27
32
|
mod sidecar;
|
|
28
33
|
|
|
29
34
|
static STORAGE: Mutex<Option<HashMap<String, String>>> = Mutex::new(None);
|
|
@@ -46,6 +51,26 @@ static OAUTH_IN_FLIGHT: AtomicBool = AtomicBool::new(false);
|
|
|
46
51
|
struct DeepLinkState { handshaken: bool, pending: Option<String> }
|
|
47
52
|
static DEEPLINK: Mutex<DeepLinkState> = Mutex::new(DeepLinkState { handshaken: false, pending: None });
|
|
48
53
|
|
|
54
|
+
/// Server-mode (`desktop.server`) deep-link delivery. The page is a plain web app (not a native-kit
|
|
55
|
+
/// consumer), so instead of the native `deeplink.open` handshake we deliver deep links to it directly:
|
|
56
|
+
/// COLD (link arrived before the page loaded) → appended to the navigation URL as `?__appwrap_deeplink=`
|
|
57
|
+
/// so the page reads it on first load; WARM (already loaded) → a `window` CustomEvent `appwrap:deeplink`.
|
|
58
|
+
/// `loaded` flips true when the boot thread navigates to the server URL (drains `pending` atomically).
|
|
59
|
+
struct ServerNav { loaded: bool, pending: Option<String> }
|
|
60
|
+
static SERVER_NAV: Mutex<ServerNav> = Mutex::new(ServerNav { loaded: false, pending: None });
|
|
61
|
+
|
|
62
|
+
/// Percent-encode a string for use as a URL query-param value (RFC3986 unreserved set kept verbatim).
|
|
63
|
+
fn percent_encode(s: &str) -> String {
|
|
64
|
+
let mut out = String::new();
|
|
65
|
+
for b in s.bytes() {
|
|
66
|
+
match b {
|
|
67
|
+
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => out.push(b as char),
|
|
68
|
+
_ => out.push_str(&format!("%{b:02X}")),
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
out
|
|
72
|
+
}
|
|
73
|
+
|
|
49
74
|
/// App identity + window shape, stamped by `appwrap dev|build desktop` from the appwrap config into
|
|
50
75
|
/// `shell_config.json` (embedded at compile time). De-hardcodes what the spike baked in. Mirrors
|
|
51
76
|
/// `DesktopShellConfig` in packages/appwrap-cli/src/desktop.ts.
|
|
@@ -87,6 +112,40 @@ struct ShellConfig {
|
|
|
87
112
|
/// actually succeed. Gates push.register and the handshake's push capability (adhoc → 'none').
|
|
88
113
|
#[serde(default)]
|
|
89
114
|
push_signed: bool,
|
|
115
|
+
/// Local-server mode: when present the shell boots this command, waits for its port, then
|
|
116
|
+
/// navigates the window to `http://localhost:<port>` (instead of loading the bundled dist), and
|
|
117
|
+
/// SIGTERMs its process group on close. Absent → bundled-PWA load. See server.rs.
|
|
118
|
+
#[serde(default)]
|
|
119
|
+
server: Option<ServerConfig>,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// Resolved local-server config (mirrors `DesktopShellConfig.server` in desktop.ts; `command`/`cwd`/
|
|
123
|
+
/// `path` are absolute, stamped by the CLI).
|
|
124
|
+
#[derive(serde::Deserialize, Clone)]
|
|
125
|
+
#[serde(rename_all = "camelCase")]
|
|
126
|
+
struct ServerConfig {
|
|
127
|
+
command: String,
|
|
128
|
+
#[serde(default)]
|
|
129
|
+
args: Vec<String>,
|
|
130
|
+
#[serde(default)]
|
|
131
|
+
cwd: String,
|
|
132
|
+
/// Non-empty → capture the URL the server prints on the first stdout line containing this marker
|
|
133
|
+
/// (tracks a dynamic port). Empty → use the fixed `port` below.
|
|
134
|
+
#[serde(default)]
|
|
135
|
+
url_marker: String,
|
|
136
|
+
#[serde(default)]
|
|
137
|
+
port: u16,
|
|
138
|
+
/// Load over https (vs http) in fixed-`port` mode. True when the server serves TLS (e.g. vite + mkcert).
|
|
139
|
+
#[serde(default)]
|
|
140
|
+
https: bool,
|
|
141
|
+
/// Reserved: readiness currently uses a TCP-accept probe (sufficient for vite/preview), so the
|
|
142
|
+
/// path isn't hit yet — kept in the wire config for a future HTTP health probe.
|
|
143
|
+
#[serde(default)]
|
|
144
|
+
#[allow(dead_code)]
|
|
145
|
+
health_path: String,
|
|
146
|
+
ready_timeout_ms: u64,
|
|
147
|
+
#[serde(default)]
|
|
148
|
+
path: String,
|
|
90
149
|
}
|
|
91
150
|
|
|
92
151
|
fn default_true() -> bool { true }
|
|
@@ -108,6 +167,20 @@ const INIT_JS: &str = r#"
|
|
|
108
167
|
})();
|
|
109
168
|
"#;
|
|
110
169
|
|
|
170
|
+
/// Build a JS snippet that swaps the boot splash into an error state with `msg` (server mode). Escapes
|
|
171
|
+
/// the message for a single-quoted JS string literal.
|
|
172
|
+
fn splash_error_js(msg: &str) -> String {
|
|
173
|
+
let esc = msg
|
|
174
|
+
.replace('\\', "\\\\")
|
|
175
|
+
.replace('\'', "\\'")
|
|
176
|
+
.replace(['\n', '\r'], " ");
|
|
177
|
+
format!(
|
|
178
|
+
"(function(){{var s=document.getElementById('spin');if(s)s.remove();\
|
|
179
|
+
var m=document.getElementById('m');if(m)m.textContent='Server didn\\'t start';\
|
|
180
|
+
var e=document.getElementById('e');if(e)e.textContent='{esc}';}})()"
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
111
184
|
type HandlerResult = Result<Value, (&'static str, String)>;
|
|
112
185
|
|
|
113
186
|
/// spawn() without wait() leaks a zombie per call; reap in a detached thread.
|
|
@@ -548,6 +621,28 @@ fn deliver_error(window: &tauri::WebviewWindow, id: &str, code: &str, message: &
|
|
|
548
621
|
/// response so the PWA routes before first paint). Reaches the main window via the stashed AppHandle,
|
|
549
622
|
/// so it's safe to call from any thread. DRY single source for both entry points.
|
|
550
623
|
pub fn route_deeplink(url: &str) {
|
|
624
|
+
// Server-mode apps get direct web delivery (see ServerNav), not the native-kit handshake path.
|
|
625
|
+
#[cfg(target_os = "macos")]
|
|
626
|
+
if shell().server.is_some() {
|
|
627
|
+
let deliver_now = {
|
|
628
|
+
let mut nav = SERVER_NAV.lock().unwrap();
|
|
629
|
+
if nav.loaded {
|
|
630
|
+
true
|
|
631
|
+
} else {
|
|
632
|
+
nav.pending = Some(url.to_string()); // cold → boot thread appends to the nav URL
|
|
633
|
+
false
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
if deliver_now {
|
|
637
|
+
if let Some(window) = APP.get().and_then(|app| app.get_webview_window("main")) {
|
|
638
|
+
let esc = url.replace('\\', "\\\\").replace('\'', "\\'");
|
|
639
|
+
let _ = window.eval(&format!(
|
|
640
|
+
"window.dispatchEvent(new CustomEvent('appwrap:deeplink',{{detail:'{esc}'}}))"
|
|
641
|
+
));
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
551
646
|
let warm = {
|
|
552
647
|
let mut dl = DEEPLINK.lock().unwrap();
|
|
553
648
|
if !dl.handshaken && dl.pending.is_none() {
|
|
@@ -594,7 +689,13 @@ fn main() {
|
|
|
594
689
|
builder = builder.plugin(tauri_plugin_window_state::Builder::default().build());
|
|
595
690
|
}
|
|
596
691
|
builder
|
|
597
|
-
.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
|
+
])
|
|
598
699
|
// Popups are plain NSWindows tauri doesn't know about; with the main (only tauri) window
|
|
599
700
|
// closed, tauri exits the app — but tear popups down explicitly so no dangling windows or
|
|
600
701
|
// live child webviews outlive the shell during teardown (or if the exit is ever prevented).
|
|
@@ -606,11 +707,16 @@ fn main() {
|
|
|
606
707
|
popup_mac::close_all();
|
|
607
708
|
// Kill the handlers sidecar so the Bun process never outlives the shell.
|
|
608
709
|
sidecar::shutdown();
|
|
710
|
+
// NB: server teardown runs on RunEvent::Exit (below), NOT here — a blocking kill in
|
|
711
|
+
// this close handler would starve tauri-plugin-window-state's own save-on-close.
|
|
609
712
|
}
|
|
610
713
|
})
|
|
611
714
|
.setup(|app| {
|
|
612
715
|
// Stash the app handle so off-main-thread handlers can reach plugin state (notifications).
|
|
613
716
|
let _ = APP.set(app.handle().clone());
|
|
717
|
+
// SPIKE: browser-bridge window-ops backend — inert unless APPWRAP_BRIDGE_JS is set.
|
|
718
|
+
#[cfg(target_os = "macos")]
|
|
719
|
+
bridge_mac::init(app.handle().clone());
|
|
614
720
|
// Per-app custom handlers: spawn the Bun sidecar when configured (absolute path stamped by
|
|
615
721
|
// the CLI). No-op / degrades to inert when empty, on spawn failure, or on a missing `ready`.
|
|
616
722
|
// NOTE: init blocks setup for up to READY_TIMEOUT (5s, bounded) — intentional: sidecar
|
|
@@ -657,6 +763,64 @@ fn main() {
|
|
|
657
763
|
let _ = window.show();
|
|
658
764
|
}
|
|
659
765
|
}
|
|
766
|
+
// Local-server mode: boot the server child now (the window shows the bundled splash), then a
|
|
767
|
+
// background thread polls its port and navigates the window to it — or, on timeout/spawn
|
|
768
|
+
// failure, writes the reason into the splash. Absent `server` → the bundled dist just loads.
|
|
769
|
+
#[cfg(target_os = "macos")]
|
|
770
|
+
if let Some(sc) = shell().server.clone() {
|
|
771
|
+
match server::spawn(&server::ServerCfg {
|
|
772
|
+
command: &sc.command,
|
|
773
|
+
args: &sc.args,
|
|
774
|
+
cwd: &sc.cwd,
|
|
775
|
+
path: &sc.path,
|
|
776
|
+
url_marker: &sc.url_marker,
|
|
777
|
+
}) {
|
|
778
|
+
Ok(url_rx) => {
|
|
779
|
+
let app_handle = app.handle().clone();
|
|
780
|
+
std::thread::spawn(move || {
|
|
781
|
+
let timeout = std::time::Duration::from_millis(sc.ready_timeout_ms);
|
|
782
|
+
// urlMarker mode → the captured URL (dynamic port); else TCP-poll the fixed port.
|
|
783
|
+
let url = match url_rx {
|
|
784
|
+
Some(rx) => rx.recv_timeout(timeout).ok(),
|
|
785
|
+
None => server::wait_for_port(sc.port, sc.ready_timeout_ms)
|
|
786
|
+
.then(|| format!("{}://localhost:{}", if sc.https { "https" } else { "http" }, sc.port)),
|
|
787
|
+
};
|
|
788
|
+
let Some(win) = app_handle.get_webview_window("main") else { return };
|
|
789
|
+
match url {
|
|
790
|
+
Some(u) => {
|
|
791
|
+
// Flip to loaded + drain any cold deep link together, so a link
|
|
792
|
+
// arriving now either lands in this URL or takes the warm path.
|
|
793
|
+
let cold = {
|
|
794
|
+
let mut nav = SERVER_NAV.lock().unwrap();
|
|
795
|
+
nav.loaded = true;
|
|
796
|
+
nav.pending.take()
|
|
797
|
+
};
|
|
798
|
+
let target = match cold {
|
|
799
|
+
Some(dl) => {
|
|
800
|
+
let sep = if u.contains('?') { '&' } else { '?' };
|
|
801
|
+
format!("{u}{sep}__appwrap_deeplink={}", percent_encode(&dl))
|
|
802
|
+
}
|
|
803
|
+
None => u,
|
|
804
|
+
};
|
|
805
|
+
let _ = win.eval(&format!("location.replace('{target}')"));
|
|
806
|
+
}
|
|
807
|
+
None => {
|
|
808
|
+
let _ = win.eval(&splash_error_js(&format!(
|
|
809
|
+
"Server didn't come up within {}s.",
|
|
810
|
+
sc.ready_timeout_ms / 1000
|
|
811
|
+
)));
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
Err(e) => {
|
|
817
|
+
eprintln!("[server] spawn `{}` failed: {e} — server mode inert", sc.command);
|
|
818
|
+
if let Some(win) = app.get_webview_window("main") {
|
|
819
|
+
let _ = win.eval(&splash_error_js(&format!("Failed to start server: {e}")));
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
660
824
|
// OS deep links (<urlScheme>://…, registered via the .app's CFBundleURLTypes). Warm →
|
|
661
825
|
// emit `deeplink.open` (same envelope as the internal loopback in appwrap_invoke);
|
|
662
826
|
// pre-handshake → stash the FIRST url, carried back in the handshake response.
|
|
@@ -670,6 +834,16 @@ fn main() {
|
|
|
670
834
|
route_deeplink(&url);
|
|
671
835
|
}
|
|
672
836
|
});
|
|
837
|
+
// Cold launch: the URL that LAUNCHED the app (open para://…) can be missed by on_open_url on
|
|
838
|
+
// macOS — retrieve it explicitly so a link that starts the app is honored too. Stashed as a
|
|
839
|
+
// cold deep link (server mode → drained into the boot navigation; see route_deeplink/ServerNav).
|
|
840
|
+
if let Ok(Some(urls)) = app.deep_link().get_current() {
|
|
841
|
+
for url in urls {
|
|
842
|
+
let url = url.to_string();
|
|
843
|
+
eprintln!("[bridge] deeplink.open (launch) ← {url}");
|
|
844
|
+
route_deeplink(&url);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
673
847
|
// Install the UNUserNotificationCenter delegate now (main thread) so a notification tapped
|
|
674
848
|
// right after a cold launch is captured. Idempotent with the schedule-time call.
|
|
675
849
|
#[cfg(target_os = "macos")]
|
|
@@ -678,6 +852,35 @@ fn main() {
|
|
|
678
852
|
}
|
|
679
853
|
Ok(())
|
|
680
854
|
})
|
|
681
|
-
.
|
|
682
|
-
.expect("error while
|
|
855
|
+
.build(tauri::generate_context!())
|
|
856
|
+
.expect("error while building appwrap desktop shell")
|
|
857
|
+
.run(|app_handle, event| {
|
|
858
|
+
#[cfg(target_os = "macos")]
|
|
859
|
+
match &event {
|
|
860
|
+
// ExitRequested fires while the window still EXISTS (⌘Q / Dock-quit / last-window-close),
|
|
861
|
+
// so persist geometry here — the window-state plugin's own save only fires on window
|
|
862
|
+
// CLOSE events, which those quit paths skip, so position/size wouldn't restore next run.
|
|
863
|
+
tauri::RunEvent::ExitRequested { .. } => {
|
|
864
|
+
if shell().restore_window_state {
|
|
865
|
+
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
|
866
|
+
let _ = app_handle.save_window_state(StateFlags::all());
|
|
867
|
+
}
|
|
868
|
+
server::shutdown();
|
|
869
|
+
}
|
|
870
|
+
// Belt-and-suspenders: also tear down on Exit (idempotent — shutdown() takes the child
|
|
871
|
+
// out of its slot, so this is a no-op after ExitRequested already ran).
|
|
872
|
+
tauri::RunEvent::Exit => server::shutdown(),
|
|
873
|
+
// macOS URL-scheme delivery for BOTH cold launch (app opened BY the link) and warm
|
|
874
|
+
// re-open — the canonical path; `on_open_url`/`get_current` miss the launch URL. Routed
|
|
875
|
+
// through the same handler (server mode → drained into the boot nav or dispatched warm).
|
|
876
|
+
tauri::RunEvent::Opened { urls } => {
|
|
877
|
+
for url in urls {
|
|
878
|
+
let u = url.to_string();
|
|
879
|
+
eprintln!("[bridge] deeplink.open (opened) ← {u}");
|
|
880
|
+
route_deeplink(&u);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
_ => {}
|
|
884
|
+
}
|
|
885
|
+
});
|
|
683
886
|
}
|