pake-cli 3.15.2 โ 3.15.4
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/README.md +1 -1
- package/dist/cli.js +3 -2
- package/llms.txt +31 -0
- package/package.json +3 -2
- package/src-tauri/Cargo.lock +4 -1
- package/src-tauri/Cargo.toml +13 -2
- package/src-tauri/src/app/cert.rs +187 -0
- package/src-tauri/src/app/menu.rs +86 -18
- package/src-tauri/src/app/mod.rs +2 -0
- package/src-tauri/src/app/window.rs +123 -10
- package/src-tauri/src/inject/event.js +15 -9
- package/src-tauri/src/inject/find.js +5 -3
- package/src-tauri/src/lib.rs +1 -0
- package/src-tauri/src/util.rs +7 -2
- package/src-tauri/tauri.conf.json +1 -1
package/README.md
CHANGED
|
@@ -197,7 +197,7 @@ Using Pake from a script or AI agent? Pass `--json` for machine-readable results
|
|
|
197
197
|
Copy this to your AI agent to get started:
|
|
198
198
|
|
|
199
199
|
```text
|
|
200
|
-
Use Pake (npm i -g pake-cli) to package webpages as desktop apps. Read https://
|
|
200
|
+
Use Pake (npm i -g pake-cli) to package webpages as desktop apps. Read https://unpkg.com/pake-cli@latest/llms.txt first; always run pake with --json and parse stdout as a single JSON object. Package <url-or-local-dist> into an app named <AppName>.
|
|
201
201
|
```
|
|
202
202
|
|
|
203
203
|
## Development
|
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import * as psl from 'psl';
|
|
|
20
20
|
import { InvalidArgumentError, program as program$1, Option } from 'commander';
|
|
21
21
|
|
|
22
22
|
var name = "pake-cli";
|
|
23
|
-
var version = "3.15.
|
|
23
|
+
var version = "3.15.4";
|
|
24
24
|
var description = "๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ
็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ";
|
|
25
25
|
var engines = {
|
|
26
26
|
node: ">=18.0.0"
|
|
@@ -47,7 +47,8 @@ var keywords = [
|
|
|
47
47
|
];
|
|
48
48
|
var files = [
|
|
49
49
|
"LICENSE-EXCEPTION",
|
|
50
|
-
"
|
|
50
|
+
"llms.txt",
|
|
51
|
+
"dist/cli.js",
|
|
51
52
|
"src-tauri"
|
|
52
53
|
];
|
|
53
54
|
var scripts = {
|
package/llms.txt
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Pake
|
|
2
|
+
|
|
3
|
+
> Pake turns any webpage or local web build into a lightweight desktop app (~5MB) for macOS, Windows, and Linux, using Tauri (Rust) and the system webview. One CLI command packages a URL, a local HTML file, or a static directory into a native installer.
|
|
4
|
+
|
|
5
|
+
## Install and run
|
|
6
|
+
|
|
7
|
+
- Install: `npm install -g pake-cli` (requires Node 18+ and Rust; the CLI offers to install Rust when missing)
|
|
8
|
+
- Basic: `pake https://example.com --name MyApp`
|
|
9
|
+
- Local build output: `pake ./dist --name MyTool` (directory must contain index.html at its root; hash routing only, history-mode SPA routing unsupported)
|
|
10
|
+
|
|
11
|
+
## Agent contract
|
|
12
|
+
|
|
13
|
+
- Always pass `--json` in automation: stdout carries exactly one JSON object `{ok, name, platform, arch, outputs: [{path, sizeBytes, format}], warnings, error: {code, message, hint} | null}`; all logs go to stderr; interactive prompts are disabled.
|
|
14
|
+
- Exit codes: 0 success, 2 invalid input, 3 build failure, 4 missing environment or dependency setup failure, 1 unexpected.
|
|
15
|
+
- On Linux multi-target builds, ok:true can carry fewer outputs than requested formats; failed targets land in warnings. Check outputs[].format.
|
|
16
|
+
- Error codes: INVALID_INPUT, ENV_MISSING, BUILD_FAILED, UNEXPECTED, plus NETWORK (reserved; current versions report network failures under the phase code, e.g. ENV_MISSING or BUILD_FAILED). `error.hint` states how to fix.
|
|
17
|
+
- Declarative config: `pake --config app.json --json`; fields are camelCase CLI option names plus `url`. Explicit CLI flags win over config fields. Unknown fields fail fast. A relative `url` path resolves against the process working directory, not the config file's directory.
|
|
18
|
+
- Config schema: https://raw.githubusercontent.com/tw93/Pake/main/schema/pake.schema.json
|
|
19
|
+
|
|
20
|
+
## Docs
|
|
21
|
+
|
|
22
|
+
- [CLI usage, all options](https://github.com/tw93/Pake/blob/main/docs/cli-usage.md): every flag with defaults and examples
|
|
23
|
+
- [Advanced usage](https://github.com/tw93/Pake/blob/main/docs/advanced-usage.md): style injection, containers, customization
|
|
24
|
+
- [FAQ](https://github.com/tw93/Pake/blob/main/docs/faq.md): platform-specific troubleshooting
|
|
25
|
+
|
|
26
|
+
## Notes
|
|
27
|
+
|
|
28
|
+
- Output formats: dmg/app (macOS), msi (Windows), deb/AppImage/rpm/zst (Linux); pick with `--targets`.
|
|
29
|
+
- Icons auto-fetch from the site favicon when `--icon` is omitted.
|
|
30
|
+
- `--proxy-url` is baked into the app webview at build time; it does not proxy the icon download itself.
|
|
31
|
+
- Google OAuth and similar providers may reject embedded webviews regardless of flags; `--new-window` and `--safe-domain` help with most SSO flows.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pake-cli",
|
|
3
|
-
"version": "3.15.
|
|
3
|
+
"version": "3.15.4",
|
|
4
4
|
"description": "๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ
็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18.0.0"
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
],
|
|
28
28
|
"files": [
|
|
29
29
|
"LICENSE-EXCEPTION",
|
|
30
|
-
"
|
|
30
|
+
"llms.txt",
|
|
31
|
+
"dist/cli.js",
|
|
31
32
|
"src-tauri"
|
|
32
33
|
],
|
|
33
34
|
"scripts": {
|
package/src-tauri/Cargo.lock
CHANGED
|
@@ -2564,8 +2564,10 @@ dependencies = [
|
|
|
2564
2564
|
|
|
2565
2565
|
[[package]]
|
|
2566
2566
|
name = "pake"
|
|
2567
|
-
version = "3.15.
|
|
2567
|
+
version = "3.15.4"
|
|
2568
2568
|
dependencies = [
|
|
2569
|
+
"block2",
|
|
2570
|
+
"dispatch",
|
|
2569
2571
|
"objc2",
|
|
2570
2572
|
"objc2-app-kit",
|
|
2571
2573
|
"objc2-foundation",
|
|
@@ -2582,6 +2584,7 @@ dependencies = [
|
|
|
2582
2584
|
"tauri-plugin-single-instance",
|
|
2583
2585
|
"tauri-plugin-window-state",
|
|
2584
2586
|
"tokio",
|
|
2587
|
+
"windows-sys 0.61.2",
|
|
2585
2588
|
]
|
|
2586
2589
|
|
|
2587
2590
|
[[package]]
|
package/src-tauri/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "pake"
|
|
3
|
-
version = "3.15.
|
|
3
|
+
version = "3.15.4"
|
|
4
4
|
description = "๐คฑ๐ป Turn any webpage into a desktop app with Rust."
|
|
5
5
|
authors = ["Tw93"]
|
|
6
6
|
license = "GPL-3.0-or-later"
|
|
@@ -25,7 +25,6 @@ tauri = { version = "2.10.2", features = [
|
|
|
25
25
|
"tray-icon",
|
|
26
26
|
"image-ico",
|
|
27
27
|
"image-png",
|
|
28
|
-
"macos-proxy",
|
|
29
28
|
] }
|
|
30
29
|
tauri-plugin-window-state = "2.4.1"
|
|
31
30
|
tauri-plugin-oauth = "2.0.0"
|
|
@@ -37,13 +36,25 @@ tauri-plugin-single-instance = "2.4.0"
|
|
|
37
36
|
tauri-plugin-notification = "2.3.3"
|
|
38
37
|
|
|
39
38
|
[target.'cfg(target_os = "macos")'.dependencies]
|
|
39
|
+
block2 = "0.6"
|
|
40
|
+
dispatch = "0.2"
|
|
40
41
|
objc2 = "0.6"
|
|
41
42
|
objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSDockTile"] }
|
|
42
43
|
objc2-foundation = { version = "0.3", features = ["NSString"] }
|
|
43
44
|
|
|
45
|
+
[target.'cfg(windows)'.dependencies]
|
|
46
|
+
windows-sys = { version = "0.61.2", features = [
|
|
47
|
+
"Win32_Foundation",
|
|
48
|
+
"Win32_UI_Shell",
|
|
49
|
+
"Win32_UI_WindowsAndMessaging",
|
|
50
|
+
] }
|
|
51
|
+
|
|
44
52
|
[features]
|
|
45
53
|
# this feature is used for development builds from development cli
|
|
46
54
|
cli-build = []
|
|
55
|
+
# Tauri's macOS proxy APIs require macOS 14 or later. The CLI enables this
|
|
56
|
+
# feature only after checking the host Darwin version.
|
|
57
|
+
macos-proxy = ["tauri/macos-proxy"]
|
|
47
58
|
# by default Tauri runs in production mode
|
|
48
59
|
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
|
49
60
|
default = ["custom-protocol"]
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// macOS TLS certificate bypass for `--ignore-certificate-errors`.
|
|
2
|
+
//
|
|
3
|
+
// On macOS the webview is a WKWebView, which ignores the Chromium
|
|
4
|
+
// `--ignore-certificate-errors` flag entirely, so Pake's flag was a no-op here
|
|
5
|
+
// (see the Windows/Linux vs macOS split in window.rs). wry's own
|
|
6
|
+
// `WryNavigationDelegate` does not implement
|
|
7
|
+
// `webView:didReceiveAuthenticationChallenge:completionHandler:`, so WKWebView
|
|
8
|
+
// falls back to default validation and rejects self-signed certs.
|
|
9
|
+
//
|
|
10
|
+
// This installs a thin navigation-delegate proxy that accepts server trust only
|
|
11
|
+
// for the configured target host and forwards every other selector to wry's
|
|
12
|
+
// original delegate, so navigation policy, downloads, and page-load callbacks
|
|
13
|
+
// keep working untouched. It is installed only when the user opts in via
|
|
14
|
+
// `--ignore-certificate-errors`.
|
|
15
|
+
|
|
16
|
+
use objc2::rc::{Retained, Weak};
|
|
17
|
+
use objc2::runtime::{AnyObject, Bool, NSObject, NSObjectProtocol, Sel};
|
|
18
|
+
use objc2::{class, define_class, msg_send, DefinedClass, MainThreadMarker, MainThreadOnly};
|
|
19
|
+
use objc2_foundation::NSString;
|
|
20
|
+
use std::ffi::c_void;
|
|
21
|
+
|
|
22
|
+
// NSURLSessionAuthChallengeDisposition values.
|
|
23
|
+
const USE_CREDENTIAL: isize = 0;
|
|
24
|
+
const PERFORM_DEFAULT_HANDLING: isize = 1;
|
|
25
|
+
|
|
26
|
+
pub struct PakeCertDelegateIvars {
|
|
27
|
+
// wry's real navigation delegate; every non-challenge selector forwards here.
|
|
28
|
+
inner: Weak<AnyObject>,
|
|
29
|
+
allowed_host: String,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static CERT_BYPASS_ASSOCIATION_KEY: u8 = 0;
|
|
33
|
+
|
|
34
|
+
fn hosts_match(allowed_host: &str, challenge_host: &str) -> bool {
|
|
35
|
+
allowed_host
|
|
36
|
+
.trim_end_matches('.')
|
|
37
|
+
.eq_ignore_ascii_case(challenge_host.trim_end_matches('.'))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
define_class!(
|
|
41
|
+
#[unsafe(super(NSObject))]
|
|
42
|
+
#[name = "PakeCertBypassDelegate"]
|
|
43
|
+
#[thread_kind = MainThreadOnly]
|
|
44
|
+
#[ivars = PakeCertDelegateIvars]
|
|
45
|
+
struct PakeCertBypassDelegate;
|
|
46
|
+
|
|
47
|
+
unsafe impl NSObjectProtocol for PakeCertBypassDelegate {}
|
|
48
|
+
|
|
49
|
+
impl PakeCertBypassDelegate {
|
|
50
|
+
#[unsafe(method(webView:didReceiveAuthenticationChallenge:completionHandler:))]
|
|
51
|
+
fn did_receive_challenge(
|
|
52
|
+
&self,
|
|
53
|
+
_webview: &AnyObject,
|
|
54
|
+
challenge: &AnyObject,
|
|
55
|
+
handler: &block2::Block<dyn Fn(isize, *mut AnyObject)>,
|
|
56
|
+
) {
|
|
57
|
+
unsafe {
|
|
58
|
+
let space: *mut AnyObject = msg_send![challenge, protectionSpace];
|
|
59
|
+
if space.is_null() {
|
|
60
|
+
handler.call((PERFORM_DEFAULT_HANDLING, core::ptr::null_mut()));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
let host: *mut NSString = msg_send![space, host];
|
|
64
|
+
if host.is_null()
|
|
65
|
+
|| !hosts_match(&self.ivars().allowed_host, &(&*host).to_string())
|
|
66
|
+
{
|
|
67
|
+
handler.call((PERFORM_DEFAULT_HANDLING, core::ptr::null_mut()));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// Only server-trust challenges carry a non-null serverTrust; for
|
|
71
|
+
// anything else (e.g. HTTP basic auth) defer to default handling.
|
|
72
|
+
let server_trust: *mut AnyObject = msg_send![space, serverTrust];
|
|
73
|
+
if server_trust.is_null() {
|
|
74
|
+
handler.call((PERFORM_DEFAULT_HANDLING, core::ptr::null_mut()));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
let credential: *mut AnyObject =
|
|
78
|
+
msg_send![class!(NSURLCredential), credentialForTrust: server_trust];
|
|
79
|
+
handler.call((USE_CREDENTIAL, credential));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// WKWebView probes respondsToSelector: before calling optional delegate
|
|
84
|
+
// methods, so report our own methods plus everything wry implements.
|
|
85
|
+
#[unsafe(method(respondsToSelector:))]
|
|
86
|
+
fn responds_to_selector(&self, selector: Sel) -> Bool {
|
|
87
|
+
let responds: Bool = unsafe { msg_send![super(self), respondsToSelector: selector] };
|
|
88
|
+
if responds.as_bool() {
|
|
89
|
+
return Bool::YES;
|
|
90
|
+
}
|
|
91
|
+
self.ivars()
|
|
92
|
+
.inner
|
|
93
|
+
.load()
|
|
94
|
+
.map(|inner| unsafe { msg_send![&*inner, respondsToSelector: selector] })
|
|
95
|
+
.unwrap_or(Bool::NO)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Fast-forward any selector we do not implement to wry's delegate.
|
|
99
|
+
#[unsafe(method(forwardingTargetForSelector:))]
|
|
100
|
+
fn forwarding_target(&self, _selector: Sel) -> *mut AnyObject {
|
|
101
|
+
self.ivars()
|
|
102
|
+
.inner
|
|
103
|
+
.load()
|
|
104
|
+
.map(|inner| Retained::as_ptr(&inner) as *mut AnyObject)
|
|
105
|
+
.unwrap_or(core::ptr::null_mut())
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
impl PakeCertBypassDelegate {
|
|
111
|
+
fn new(
|
|
112
|
+
inner: &Retained<AnyObject>,
|
|
113
|
+
allowed_host: String,
|
|
114
|
+
mtm: MainThreadMarker,
|
|
115
|
+
) -> Retained<Self> {
|
|
116
|
+
let this = mtm
|
|
117
|
+
.alloc::<PakeCertBypassDelegate>()
|
|
118
|
+
.set_ivars(PakeCertDelegateIvars {
|
|
119
|
+
inner: Weak::from_retained(inner),
|
|
120
|
+
allowed_host,
|
|
121
|
+
});
|
|
122
|
+
unsafe { msg_send![super(this), init] }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// Replace the WKWebView's navigation delegate with a proxy that accepts
|
|
127
|
+
/// invalid TLS certificates for the configured target host, then navigate to
|
|
128
|
+
/// that target. `webview_ptr` is the raw `WKWebView` from
|
|
129
|
+
/// `PlatformWebview::inner()`. Returns false if setup cannot be completed.
|
|
130
|
+
pub fn install_cert_bypass_and_navigate(
|
|
131
|
+
webview_ptr: *mut c_void,
|
|
132
|
+
allowed_host: String,
|
|
133
|
+
target_url: String,
|
|
134
|
+
) -> bool {
|
|
135
|
+
if webview_ptr.is_null() || allowed_host.is_empty() || target_url.is_empty() {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
let Some(mtm) = MainThreadMarker::new() else {
|
|
139
|
+
return false;
|
|
140
|
+
};
|
|
141
|
+
unsafe {
|
|
142
|
+
let webview: &AnyObject = &*(webview_ptr as *const AnyObject);
|
|
143
|
+
let existing: *mut AnyObject = msg_send![webview, navigationDelegate];
|
|
144
|
+
if existing.is_null() {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
let Some(inner) = Retained::retain(existing) else {
|
|
148
|
+
return false;
|
|
149
|
+
};
|
|
150
|
+
let proxy = PakeCertBypassDelegate::new(&inner, allowed_host, mtm);
|
|
151
|
+
objc2::ffi::objc_setAssociatedObject(
|
|
152
|
+
webview as *const AnyObject as *mut AnyObject,
|
|
153
|
+
std::ptr::addr_of!(CERT_BYPASS_ASSOCIATION_KEY).cast(),
|
|
154
|
+
Retained::as_ptr(&proxy) as *mut AnyObject,
|
|
155
|
+
objc2::ffi::OBJC_ASSOCIATION_RETAIN_NONATOMIC,
|
|
156
|
+
);
|
|
157
|
+
let _: () = msg_send![webview, setNavigationDelegate: &*proxy];
|
|
158
|
+
|
|
159
|
+
let target_url = NSString::from_str(&target_url);
|
|
160
|
+
let ns_url: *mut AnyObject = msg_send![class!(NSURL), URLWithString: &*target_url];
|
|
161
|
+
if ns_url.is_null() {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
let request: *mut AnyObject = msg_send![class!(NSURLRequest), requestWithURL: ns_url];
|
|
165
|
+
if request.is_null() {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
let _: () = msg_send![webview, loadRequest: request];
|
|
169
|
+
true
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#[cfg(test)]
|
|
174
|
+
mod tests {
|
|
175
|
+
use super::hosts_match;
|
|
176
|
+
|
|
177
|
+
#[test]
|
|
178
|
+
fn accepts_the_configured_host_case_insensitively() {
|
|
179
|
+
assert!(hosts_match("INTERNAL.EXAMPLE.COM", "internal.example.com"));
|
|
180
|
+
assert!(hosts_match("internal.example.com.", "internal.example.com"));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#[test]
|
|
184
|
+
fn rejects_other_hosts() {
|
|
185
|
+
assert!(!hosts_match("internal.example.com", "login.example.com"));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Menu functionality is only used on macOS; the module is gated in app/mod.rs.
|
|
2
|
-
use crate::app::window::open_additional_window_safe;
|
|
2
|
+
use crate::app::window::{open_additional_window_safe, MultiWindowState};
|
|
3
3
|
use tauri::menu::{AboutMetadata, Menu, MenuItem, PredefinedMenuItem, Submenu};
|
|
4
|
-
use tauri::{AppHandle, Manager, Wry};
|
|
4
|
+
use tauri::{AppHandle, Manager, WebviewWindow, Wry};
|
|
5
5
|
use tauri_plugin_opener::OpenerExt;
|
|
6
6
|
|
|
7
7
|
pub fn set_app_menu(
|
|
@@ -224,6 +224,36 @@ fn help_menu(app: &AppHandle<Wry>, title: &str) -> tauri::Result<Submenu<Wry>> {
|
|
|
224
224
|
Ok(help_menu)
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
// Resolve the app's real home URL from its window config. Split out from
|
|
228
|
+
// `home_url` so the mapping can be unit-tested without an AppHandle.
|
|
229
|
+
fn resolve_home_url(url_type: &str, url: &str) -> Option<tauri::Url> {
|
|
230
|
+
match url_type {
|
|
231
|
+
// A web app's configured url is already absolute.
|
|
232
|
+
"web" => tauri::Url::parse(url).ok(),
|
|
233
|
+
// A local-file app's url is only a basename; Tauri serves bundled assets
|
|
234
|
+
// from tauri://localhost on macOS (this menu is macOS-only). Resolving it
|
|
235
|
+
// against the currently loaded remote origin (as the old eval path did)
|
|
236
|
+
// would point at the wrong server.
|
|
237
|
+
"local" => tauri::Url::parse(&format!("tauri://localhost/{url}")).ok(),
|
|
238
|
+
_ => None,
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
fn home_url(app: &AppHandle) -> Option<tauri::Url> {
|
|
243
|
+
let state = app.try_state::<MultiWindowState>()?;
|
|
244
|
+
let window_config = state.pake_config.windows.first()?;
|
|
245
|
+
resolve_home_url(&window_config.url_type, &window_config.url)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
fn focused_webview_window(app_handle: &AppHandle) -> Option<WebviewWindow> {
|
|
249
|
+
let windows = app_handle.webview_windows();
|
|
250
|
+
windows
|
|
251
|
+
.values()
|
|
252
|
+
.find(|window| window.is_focused().unwrap_or(false))
|
|
253
|
+
.cloned()
|
|
254
|
+
.or_else(|| windows.get("pake").cloned())
|
|
255
|
+
}
|
|
256
|
+
|
|
227
257
|
pub fn handle_menu_click(app_handle: &AppHandle, id: &str) {
|
|
228
258
|
match id {
|
|
229
259
|
"new_window" => {
|
|
@@ -235,13 +265,13 @@ pub fn handle_menu_click(app_handle: &AppHandle, id: &str) {
|
|
|
235
265
|
.open_url("https://github.com/tw93/Pake", None::<&str>);
|
|
236
266
|
}
|
|
237
267
|
"reload" => {
|
|
238
|
-
if let Some(window) = app_handle
|
|
268
|
+
if let Some(window) = focused_webview_window(app_handle) {
|
|
239
269
|
let _ = window.eval("window.location.reload()");
|
|
240
270
|
}
|
|
241
271
|
}
|
|
242
272
|
"toggle_devtools" => {
|
|
243
273
|
#[cfg(debug_assertions)] // Only allow in debug builds
|
|
244
|
-
if let Some(window) = app_handle
|
|
274
|
+
if let Some(window) = focused_webview_window(app_handle) {
|
|
245
275
|
if window.is_devtools_open() {
|
|
246
276
|
window.close_devtools();
|
|
247
277
|
} else {
|
|
@@ -250,69 +280,79 @@ pub fn handle_menu_click(app_handle: &AppHandle, id: &str) {
|
|
|
250
280
|
}
|
|
251
281
|
}
|
|
252
282
|
"zoom_in" => {
|
|
253
|
-
if let Some(window) = app_handle
|
|
283
|
+
if let Some(window) = focused_webview_window(app_handle) {
|
|
254
284
|
let _ = window.eval("zoomIn()");
|
|
255
285
|
}
|
|
256
286
|
}
|
|
257
287
|
"zoom_out" => {
|
|
258
|
-
if let Some(window) = app_handle
|
|
288
|
+
if let Some(window) = focused_webview_window(app_handle) {
|
|
259
289
|
let _ = window.eval("zoomOut()");
|
|
260
290
|
}
|
|
261
291
|
}
|
|
262
292
|
"zoom_reset" => {
|
|
263
|
-
if let Some(window) = app_handle
|
|
293
|
+
if let Some(window) = focused_webview_window(app_handle) {
|
|
264
294
|
let _ = window.eval("setZoom('100%')");
|
|
265
295
|
}
|
|
266
296
|
}
|
|
267
297
|
"go_back" => {
|
|
268
|
-
if let Some(window) = app_handle
|
|
298
|
+
if let Some(window) = focused_webview_window(app_handle) {
|
|
269
299
|
let _ = window.eval("window.history.back()");
|
|
270
300
|
}
|
|
271
301
|
}
|
|
272
302
|
"go_forward" => {
|
|
273
|
-
if let Some(window) = app_handle
|
|
303
|
+
if let Some(window) = focused_webview_window(app_handle) {
|
|
274
304
|
let _ = window.eval("window.history.forward()");
|
|
275
305
|
}
|
|
276
306
|
}
|
|
277
307
|
"go_home" => {
|
|
278
|
-
if let Some(window) = app_handle
|
|
279
|
-
|
|
308
|
+
if let Some(window) = focused_webview_window(app_handle) {
|
|
309
|
+
// Native navigation works even from a blank error page (where
|
|
310
|
+
// eval cannot run) and resolves local-file apps to the correct
|
|
311
|
+
// bundled asset instead of a path on the current origin.
|
|
312
|
+
match home_url(app_handle) {
|
|
313
|
+
Some(url) => {
|
|
314
|
+
let _ = window.navigate(url);
|
|
315
|
+
}
|
|
316
|
+
None => {
|
|
317
|
+
let _ = window.eval("window.location.href = window.pakeConfig.url");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
280
320
|
}
|
|
281
321
|
}
|
|
282
322
|
"copy_url" => {
|
|
283
|
-
if let Some(window) = app_handle
|
|
323
|
+
if let Some(window) = focused_webview_window(app_handle) {
|
|
284
324
|
let _ = window.eval("navigator.clipboard.writeText(window.location.href)");
|
|
285
325
|
}
|
|
286
326
|
}
|
|
287
327
|
"paste_and_match_style" => {
|
|
288
|
-
if let Some(window) = app_handle
|
|
328
|
+
if let Some(window) = focused_webview_window(app_handle) {
|
|
289
329
|
let _ = window.eval("triggerPasteAsPlainText()");
|
|
290
330
|
}
|
|
291
331
|
}
|
|
292
332
|
"find" => {
|
|
293
|
-
if let Some(window) = app_handle
|
|
333
|
+
if let Some(window) = focused_webview_window(app_handle) {
|
|
294
334
|
let _ = window.eval("window.pakeFind?.open()");
|
|
295
335
|
}
|
|
296
336
|
}
|
|
297
337
|
"find_next" => {
|
|
298
|
-
if let Some(window) = app_handle
|
|
338
|
+
if let Some(window) = focused_webview_window(app_handle) {
|
|
299
339
|
let _ = window.eval("window.pakeFind?.next()");
|
|
300
340
|
}
|
|
301
341
|
}
|
|
302
342
|
"find_previous" => {
|
|
303
|
-
if let Some(window) = app_handle
|
|
343
|
+
if let Some(window) = focused_webview_window(app_handle) {
|
|
304
344
|
let _ = window.eval("window.pakeFind?.previous()");
|
|
305
345
|
}
|
|
306
346
|
}
|
|
307
347
|
"clear_cache_restart" => {
|
|
308
|
-
if let Some(window) = app_handle
|
|
348
|
+
if let Some(window) = focused_webview_window(app_handle) {
|
|
309
349
|
if window.clear_all_browsing_data().is_ok() {
|
|
310
350
|
app_handle.restart();
|
|
311
351
|
}
|
|
312
352
|
}
|
|
313
353
|
}
|
|
314
354
|
"always_on_top" => {
|
|
315
|
-
if let Some(window) = app_handle
|
|
355
|
+
if let Some(window) = focused_webview_window(app_handle) {
|
|
316
356
|
let is_on_top = window.is_always_on_top().unwrap_or(false);
|
|
317
357
|
let _ = window.set_always_on_top(!is_on_top);
|
|
318
358
|
}
|
|
@@ -320,3 +360,31 @@ pub fn handle_menu_click(app_handle: &AppHandle, id: &str) {
|
|
|
320
360
|
_ => {}
|
|
321
361
|
}
|
|
322
362
|
}
|
|
363
|
+
|
|
364
|
+
#[cfg(test)]
|
|
365
|
+
mod tests {
|
|
366
|
+
use super::resolve_home_url;
|
|
367
|
+
|
|
368
|
+
#[test]
|
|
369
|
+
fn web_url_passes_through_unchanged() {
|
|
370
|
+
assert_eq!(
|
|
371
|
+
resolve_home_url("web", "https://github.com").map(|u| u.to_string()),
|
|
372
|
+
Some("https://github.com/".to_string())
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
#[test]
|
|
377
|
+
fn local_basename_resolves_to_bundled_asset_url() {
|
|
378
|
+
// The fix: a local app's basename must become the bundled asset URL,
|
|
379
|
+
// not a path resolved against whatever origin is currently loaded.
|
|
380
|
+
assert_eq!(
|
|
381
|
+
resolve_home_url("local", "launcher.html").map(|u| u.to_string()),
|
|
382
|
+
Some("tauri://localhost/launcher.html".to_string())
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
#[test]
|
|
387
|
+
fn unknown_url_type_is_none() {
|
|
388
|
+
assert!(resolve_home_url("bogus", "whatever").is_none());
|
|
389
|
+
}
|
|
390
|
+
}
|
package/src-tauri/src/app/mod.rs
CHANGED
|
@@ -3,6 +3,10 @@ use crate::util::{
|
|
|
3
3
|
check_file_or_append, get_data_dir, get_download_message_with_lang, sanitize_download_filename,
|
|
4
4
|
show_toast, MessageType,
|
|
5
5
|
};
|
|
6
|
+
#[cfg(target_os = "macos")]
|
|
7
|
+
use dispatch::Queue;
|
|
8
|
+
#[cfg(target_os = "windows")]
|
|
9
|
+
use std::{os::windows::ffi::OsStrExt, ptr, sync::OnceLock};
|
|
6
10
|
use std::{
|
|
7
11
|
path::PathBuf,
|
|
8
12
|
str::FromStr,
|
|
@@ -13,6 +17,12 @@ use tauri::{
|
|
|
13
17
|
AppHandle, Config, Manager, Url, WebviewUrl, WebviewWindow, WebviewWindowBuilder,
|
|
14
18
|
};
|
|
15
19
|
|
|
20
|
+
#[cfg(target_os = "windows")]
|
|
21
|
+
use windows_sys::Win32::UI::{
|
|
22
|
+
Shell::ExtractIconExW,
|
|
23
|
+
WindowsAndMessaging::{SendMessageW, ICON_BIG, WM_SETICON},
|
|
24
|
+
};
|
|
25
|
+
|
|
16
26
|
use tauri::Theme;
|
|
17
27
|
|
|
18
28
|
#[cfg(target_os = "macos")]
|
|
@@ -69,14 +79,70 @@ pub fn open_additional_window(app: &AppHandle) -> tauri::Result<WebviewWindow> {
|
|
|
69
79
|
build_window_with_label(app, &state.pake_config, &state.tauri_config, &label)
|
|
70
80
|
}
|
|
71
81
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
#[cfg(target_os = "windows")]
|
|
83
|
+
fn taskbar_icon_handle() -> Option<isize> {
|
|
84
|
+
static TASKBAR_ICON: OnceLock<Option<isize>> = OnceLock::new();
|
|
85
|
+
|
|
86
|
+
*TASKBAR_ICON.get_or_init(|| {
|
|
87
|
+
let executable = match std::env::current_exe() {
|
|
88
|
+
Ok(path) => path,
|
|
89
|
+
Err(error) => {
|
|
90
|
+
eprintln!(
|
|
91
|
+
"[Pake] Failed to resolve the app executable for its taskbar icon: {error}"
|
|
92
|
+
);
|
|
93
|
+
return None;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
let executable_wide: Vec<u16> = executable
|
|
97
|
+
.as_os_str()
|
|
98
|
+
.encode_wide()
|
|
99
|
+
.chain(std::iter::once(0))
|
|
100
|
+
.collect();
|
|
101
|
+
let mut large_icon = ptr::null_mut();
|
|
102
|
+
let extracted = unsafe {
|
|
103
|
+
ExtractIconExW(
|
|
104
|
+
executable_wide.as_ptr(),
|
|
105
|
+
0,
|
|
106
|
+
&mut large_icon,
|
|
107
|
+
ptr::null_mut(),
|
|
108
|
+
1,
|
|
109
|
+
)
|
|
110
|
+
};
|
|
111
|
+
if extracted == 0 || large_icon.is_null() {
|
|
112
|
+
eprintln!(
|
|
113
|
+
"[Pake] Failed to extract the taskbar icon from {}.",
|
|
114
|
+
executable.display()
|
|
115
|
+
);
|
|
116
|
+
return None;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// WM_SETICON keeps this handle rather than copying it. Cache the single
|
|
120
|
+
// extracted icon for the process lifetime so repeated tray restores do
|
|
121
|
+
// not leak a new HICON or leave the window with a dangling handle.
|
|
122
|
+
Some(large_icon as isize)
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Apps autostarted at Windows logon can register their icons before Explorer's
|
|
127
|
+
// icon cache is ready (#1323). Re-assert both the small/title-bar icon and the
|
|
128
|
+
// large taskbar icon whenever a window becomes visible.
|
|
75
129
|
#[cfg(target_os = "windows")]
|
|
76
130
|
pub fn reapply_window_icon(window: &WebviewWindow) {
|
|
77
131
|
if let Some(icon) = window.app_handle().default_window_icon().cloned() {
|
|
78
132
|
if let Err(error) = window.set_icon(icon) {
|
|
79
|
-
eprintln!("[Pake] Failed to re-apply window icon: {error}");
|
|
133
|
+
eprintln!("[Pake] Failed to re-apply the window icon: {error}");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let Some(taskbar_icon) = taskbar_icon_handle() else {
|
|
138
|
+
return;
|
|
139
|
+
};
|
|
140
|
+
match window.hwnd() {
|
|
141
|
+
Ok(hwnd) => unsafe {
|
|
142
|
+
SendMessageW(hwnd.0, WM_SETICON, ICON_BIG as usize, taskbar_icon);
|
|
143
|
+
},
|
|
144
|
+
Err(error) => {
|
|
145
|
+
eprintln!("[Pake] Failed to resolve the window handle for its taskbar icon: {error}");
|
|
80
146
|
}
|
|
81
147
|
}
|
|
82
148
|
}
|
|
@@ -114,6 +180,7 @@ fn open_requested_window(
|
|
|
114
180
|
|
|
115
181
|
let title = target_url.host_str().unwrap_or(target_url.as_str());
|
|
116
182
|
let _ = window.set_title(title);
|
|
183
|
+
reapply_window_icon(&window);
|
|
117
184
|
let _ = window.set_focus();
|
|
118
185
|
|
|
119
186
|
Ok(window)
|
|
@@ -126,6 +193,7 @@ pub fn open_additional_window_safe(app: &AppHandle) {
|
|
|
126
193
|
std::thread::spawn(move || {
|
|
127
194
|
if let Ok(window) = open_additional_window(&app_handle) {
|
|
128
195
|
let _ = window.show();
|
|
196
|
+
reapply_window_icon(&window);
|
|
129
197
|
let _ = window.set_focus();
|
|
130
198
|
}
|
|
131
199
|
});
|
|
@@ -135,6 +203,7 @@ pub fn open_additional_window_safe(app: &AppHandle) {
|
|
|
135
203
|
{
|
|
136
204
|
if let Ok(window) = open_additional_window(app) {
|
|
137
205
|
let _ = window.show();
|
|
206
|
+
reapply_window_icon(&window);
|
|
138
207
|
let _ = window.set_focus();
|
|
139
208
|
}
|
|
140
209
|
}
|
|
@@ -212,6 +281,27 @@ fn build_window(
|
|
|
212
281
|
))
|
|
213
282
|
})?;
|
|
214
283
|
|
|
284
|
+
#[cfg(target_os = "macos")]
|
|
285
|
+
let cert_bypass_target = if label == "pake"
|
|
286
|
+
&& window_config.ignore_certificate_errors
|
|
287
|
+
&& window_config.url_type == "web"
|
|
288
|
+
{
|
|
289
|
+
Url::parse(&window_config.url).ok()
|
|
290
|
+
} else {
|
|
291
|
+
None
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// The delegate must be installed before the first TLS challenge. Start on
|
|
295
|
+
// a neutral page, then navigate from the with_webview callback below.
|
|
296
|
+
#[cfg(target_os = "macos")]
|
|
297
|
+
let url = if cert_bypass_target.is_some() {
|
|
298
|
+
WebviewUrl::CustomProtocol(
|
|
299
|
+
Url::parse("about:blank").expect("about:blank must be a valid URL"),
|
|
300
|
+
)
|
|
301
|
+
} else {
|
|
302
|
+
url
|
|
303
|
+
};
|
|
304
|
+
|
|
215
305
|
let user_agent = config.user_agent.get();
|
|
216
306
|
|
|
217
307
|
let config_script = format!(
|
|
@@ -340,11 +430,6 @@ fn build_window(
|
|
|
340
430
|
{
|
|
341
431
|
linux_browser_args.push_str(" --ignore-certificate-errors");
|
|
342
432
|
}
|
|
343
|
-
|
|
344
|
-
#[cfg(target_os = "macos")]
|
|
345
|
-
{
|
|
346
|
-
window_builder = window_builder.additional_browser_args("--ignore-certificate-errors");
|
|
347
|
-
}
|
|
348
433
|
}
|
|
349
434
|
|
|
350
435
|
if window_config.enable_wasm {
|
|
@@ -516,7 +601,35 @@ fn build_window(
|
|
|
516
601
|
|
|
517
602
|
window_builder = window_builder.on_navigation(|_| true);
|
|
518
603
|
|
|
519
|
-
window_builder.build()
|
|
604
|
+
let window = window_builder.build()?;
|
|
605
|
+
|
|
606
|
+
// macOS WKWebView ignores the Chromium --ignore-certificate-errors flag, so
|
|
607
|
+
// install a host-scoped delegate on the process-lifetime main window only.
|
|
608
|
+
// Queue setup after construction so wry cannot replace the proxy while it
|
|
609
|
+
// finishes initializing its own navigation delegate.
|
|
610
|
+
#[cfg(target_os = "macos")]
|
|
611
|
+
if let Some(target_url) = cert_bypass_target {
|
|
612
|
+
let allowed_host = target_url
|
|
613
|
+
.host_str()
|
|
614
|
+
.expect("web URLs must have a host")
|
|
615
|
+
.to_owned();
|
|
616
|
+
let cert_window = window.clone();
|
|
617
|
+
Queue::main().exec_async(move || {
|
|
618
|
+
if let Err(error) = cert_window.with_webview(move |webview| {
|
|
619
|
+
if !crate::app::cert::install_cert_bypass_and_navigate(
|
|
620
|
+
webview.inner(),
|
|
621
|
+
allowed_host,
|
|
622
|
+
target_url.to_string(),
|
|
623
|
+
) {
|
|
624
|
+
eprintln!("[Pake] Failed to configure macOS certificate bypass.");
|
|
625
|
+
}
|
|
626
|
+
}) {
|
|
627
|
+
eprintln!("[Pake] Failed to access the macOS webview: {error}");
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
Ok(window)
|
|
520
633
|
}
|
|
521
634
|
|
|
522
635
|
#[cfg(all(test, target_os = "windows"))]
|
|
@@ -60,6 +60,20 @@ function handleShortcut(event) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function handleWebShortcut(event) {
|
|
64
|
+
if (isNonMacDesktop() && event.ctrlKey) {
|
|
65
|
+
handleShortcut(event);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const isMac = /mac/i.test(getDesktopPlatform());
|
|
70
|
+
const isMacScrollShortcut =
|
|
71
|
+
event.key === "ArrowUp" || event.key === "ArrowDown";
|
|
72
|
+
if (isMac && event.metaKey && isMacScrollShortcut) {
|
|
73
|
+
handleShortcut(event);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
function toggleNativeFullscreen(appWindow) {
|
|
64
78
|
appWindow
|
|
65
79
|
.isFullscreen()
|
|
@@ -437,7 +451,6 @@ const DOWNLOAD_PATH_PATTERNS = [
|
|
|
437
451
|
"/files/",
|
|
438
452
|
"/attachments/",
|
|
439
453
|
"/assets/",
|
|
440
|
-
"/releases/",
|
|
441
454
|
"/dist/",
|
|
442
455
|
];
|
|
443
456
|
|
|
@@ -622,14 +635,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
622
635
|
|
|
623
636
|
if (window["pakeConfig"]?.disabled_web_shortcuts !== true) {
|
|
624
637
|
document.addEventListener("keydown", handleWindowFullscreenShortcut, true);
|
|
625
|
-
document.addEventListener("keyup",
|
|
626
|
-
if (/windows|linux/i.test(navigator.userAgent) && event.ctrlKey) {
|
|
627
|
-
handleShortcut(event);
|
|
628
|
-
}
|
|
629
|
-
if (/macintosh|mac os x/i.test(navigator.userAgent) && event.metaKey) {
|
|
630
|
-
handleShortcut(event);
|
|
631
|
-
}
|
|
632
|
-
});
|
|
638
|
+
document.addEventListener("keyup", handleWebShortcut);
|
|
633
639
|
}
|
|
634
640
|
|
|
635
641
|
document.addEventListener("keydown", handleClipboardShortcut, true);
|
|
@@ -663,9 +663,11 @@
|
|
|
663
663
|
function getFindShortcutAction(event) {
|
|
664
664
|
const userAgent = navigator.userAgent || "";
|
|
665
665
|
const isMac = /macintosh|mac os x/i.test(userAgent);
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
666
|
+
if (isMac) {
|
|
667
|
+
return "";
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const hasModifier = event.ctrlKey && !event.metaKey;
|
|
669
671
|
|
|
670
672
|
if (!hasModifier || event.altKey) {
|
|
671
673
|
return "";
|
package/src-tauri/src/lib.rs
CHANGED
package/src-tauri/src/util.rs
CHANGED
|
@@ -183,16 +183,21 @@ mod tests {
|
|
|
183
183
|
use std::env;
|
|
184
184
|
use std::fs;
|
|
185
185
|
use std::path::PathBuf;
|
|
186
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
187
|
+
|
|
188
|
+
static TEMP_PATH_COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
186
189
|
|
|
187
190
|
fn temp_path(name: &str) -> PathBuf {
|
|
188
191
|
let mut dir = env::temp_dir();
|
|
192
|
+
let unique = TEMP_PATH_COUNTER.fetch_add(1, Ordering::Relaxed);
|
|
189
193
|
dir.push(format!(
|
|
190
|
-
"pake-util-test-{}-{}",
|
|
194
|
+
"pake-util-test-{}-{}-{}",
|
|
191
195
|
std::process::id(),
|
|
192
196
|
std::time::SystemTime::now()
|
|
193
197
|
.duration_since(std::time::UNIX_EPOCH)
|
|
194
198
|
.map(|d| d.as_nanos())
|
|
195
|
-
.unwrap_or(0)
|
|
199
|
+
.unwrap_or(0),
|
|
200
|
+
unique
|
|
196
201
|
));
|
|
197
202
|
fs::create_dir_all(&dir).unwrap();
|
|
198
203
|
dir.push(name);
|