pake-cli 3.12.0 โ 3.13.0
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 +2 -2
- package/dist/cli.js +134 -18
- package/package.json +1 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/app/invoke.rs +16 -33
- package/src-tauri/src/app/menu.rs +2 -4
- package/src-tauri/src/app/setup.rs +8 -4
- package/src-tauri/src/app/window.rs +12 -9
- package/src-tauri/src/inject/event.js +7 -13
- package/src-tauri/src/lib.rs +81 -5
- package/src-tauri/tauri.conf.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<div align="center">
|
|
8
8
|
<a href="https://twitter.com/HiTw93" target="_blank">
|
|
9
9
|
<img alt="twitter" src="https://img.shields.io/badge/follow-Tw93-red?style=flat-square&logo=Twitter"></a>
|
|
10
|
-
<a href="https://t.me/+
|
|
10
|
+
<a href="https://t.me/+9f9gf4ZrFSQ2OWVl" target="_blank">
|
|
11
11
|
<img alt="telegram" src="https://img.shields.io/badge/chat-telegram-blueviolet?style=flat-square&logo=Telegram"></a>
|
|
12
12
|
<a href="https://github.com/tw93/Pake/releases" target="_blank">
|
|
13
13
|
<img alt="GitHub downloads" src="https://img.shields.io/github/downloads/tw93/Pake/total.svg?style=flat-square"></a>
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
## Features
|
|
21
21
|
|
|
22
|
-
- ๐ **Lightweight**:
|
|
22
|
+
- ๐ **Lightweight**: Installer is nearly 20 times smaller than Electron packages, typically under 10M on disk
|
|
23
23
|
- ๐ **Fast**: Built with Rust Tauri, much faster than traditional JS frameworks with lower memory usage
|
|
24
24
|
- โก **Easy to use**: One-command packaging via CLI or online building, no complex configuration needed
|
|
25
25
|
- ๐ฆ **Feature-rich**: Supports shortcuts, immersive windows, drag & drop, style customization, ad removal
|
package/dist/cli.js
CHANGED
|
@@ -10,17 +10,17 @@ import os from 'os';
|
|
|
10
10
|
import { execa, execaSync } from 'execa';
|
|
11
11
|
import crypto from 'crypto';
|
|
12
12
|
import ora from 'ora';
|
|
13
|
-
import fs from 'fs
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import fs$1 from 'fs/promises';
|
|
14
15
|
import { dir } from 'tmp-promise';
|
|
15
16
|
import { fileTypeFromBuffer } from 'file-type';
|
|
16
17
|
import icongen from 'icon-gen';
|
|
17
18
|
import sharp from 'sharp';
|
|
18
19
|
import * as psl from 'psl';
|
|
19
20
|
import { InvalidArgumentError, program as program$1, Option } from 'commander';
|
|
20
|
-
import fs$1 from 'fs';
|
|
21
21
|
|
|
22
22
|
var name = "pake-cli";
|
|
23
|
-
var version = "3.
|
|
23
|
+
var version = "3.13.0";
|
|
24
24
|
var description = "๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ
็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ";
|
|
25
25
|
var engines = {
|
|
26
26
|
node: ">=18.0.0"
|
|
@@ -230,6 +230,93 @@ const { platform: platform$1 } = process;
|
|
|
230
230
|
const IS_MAC = platform$1 === 'darwin';
|
|
231
231
|
const IS_WIN = platform$1 === 'win32';
|
|
232
232
|
const IS_LINUX = platform$1 === 'linux';
|
|
233
|
+
// Distro IDs / ID_LIKE families that ship an RPM-based package manager.
|
|
234
|
+
const RPM_FAMILY_IDS = new Set([
|
|
235
|
+
'rhel',
|
|
236
|
+
'fedora',
|
|
237
|
+
'centos',
|
|
238
|
+
'rocky',
|
|
239
|
+
'almalinux',
|
|
240
|
+
'ol', // Oracle Linux
|
|
241
|
+
'oracle',
|
|
242
|
+
'amzn', // Amazon Linux
|
|
243
|
+
'mariner',
|
|
244
|
+
'azurelinux',
|
|
245
|
+
'suse',
|
|
246
|
+
'opensuse',
|
|
247
|
+
'opensuse-leap',
|
|
248
|
+
'opensuse-tumbleweed',
|
|
249
|
+
'sles',
|
|
250
|
+
]);
|
|
251
|
+
// Distro IDs / ID_LIKE families that ship a DEB-based package manager.
|
|
252
|
+
const DEB_FAMILY_IDS = new Set([
|
|
253
|
+
'debian',
|
|
254
|
+
'ubuntu',
|
|
255
|
+
'linuxmint',
|
|
256
|
+
'pop',
|
|
257
|
+
'elementary',
|
|
258
|
+
'kali',
|
|
259
|
+
'raspbian',
|
|
260
|
+
'devuan',
|
|
261
|
+
]);
|
|
262
|
+
// Parse the shell-style key=value pairs of an /etc/os-release file, stripping
|
|
263
|
+
// the optional surrounding quotes around values.
|
|
264
|
+
function parseOsRelease(content) {
|
|
265
|
+
const fields = {};
|
|
266
|
+
for (const rawLine of content.split('\n')) {
|
|
267
|
+
const line = rawLine.trim();
|
|
268
|
+
if (!line || line.startsWith('#'))
|
|
269
|
+
continue;
|
|
270
|
+
const separator = line.indexOf('=');
|
|
271
|
+
if (separator === -1)
|
|
272
|
+
continue;
|
|
273
|
+
const key = line.slice(0, separator).trim();
|
|
274
|
+
let value = line.slice(separator + 1).trim();
|
|
275
|
+
if (value.length >= 2 &&
|
|
276
|
+
((value.startsWith('"') && value.endsWith('"')) ||
|
|
277
|
+
(value.startsWith("'") && value.endsWith("'")))) {
|
|
278
|
+
value = value.slice(1, -1);
|
|
279
|
+
}
|
|
280
|
+
if (key)
|
|
281
|
+
fields[key] = value;
|
|
282
|
+
}
|
|
283
|
+
return fields;
|
|
284
|
+
}
|
|
285
|
+
// Detect the package family from /etc/os-release. The distro's own ID wins over
|
|
286
|
+
// ID_LIKE hints, and an unknown distro falls back to 'deb' to preserve Pake's
|
|
287
|
+
// historical default. Accepts content directly so the decision is unit-testable
|
|
288
|
+
// without a real /etc/os-release.
|
|
289
|
+
function detectLinuxPackageFamily(osReleaseContent) {
|
|
290
|
+
let content = osReleaseContent;
|
|
291
|
+
if (content === undefined) {
|
|
292
|
+
try {
|
|
293
|
+
content = fs.readFileSync('/etc/os-release', 'utf-8');
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return 'deb';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const fields = parseOsRelease(content);
|
|
300
|
+
const id = (fields.ID ?? '').toLowerCase().trim();
|
|
301
|
+
const idLike = (fields.ID_LIKE ?? '')
|
|
302
|
+
.toLowerCase()
|
|
303
|
+
.split(/\s+/)
|
|
304
|
+
.filter(Boolean);
|
|
305
|
+
for (const token of [id, ...idLike]) {
|
|
306
|
+
if (DEB_FAMILY_IDS.has(token))
|
|
307
|
+
return 'deb';
|
|
308
|
+
if (RPM_FAMILY_IDS.has(token))
|
|
309
|
+
return 'rpm';
|
|
310
|
+
}
|
|
311
|
+
return 'deb';
|
|
312
|
+
}
|
|
313
|
+
// Default Linux bundle targets, chosen by the host distro's package family so
|
|
314
|
+
// RPM-based distros (Fedora/RHEL/Oracle/Rocky/Alma/openSUSE) get a native .rpm
|
|
315
|
+
// instead of a .deb their package manager cannot install. AppImage stays as a
|
|
316
|
+
// universal fallback in both cases.
|
|
317
|
+
function getDefaultLinuxTargets() {
|
|
318
|
+
return detectLinuxPackageFamily() === 'rpm' ? 'rpm,appimage' : 'deb,appimage';
|
|
319
|
+
}
|
|
233
320
|
|
|
234
321
|
async function shellExec(command, timeout = 300000, env) {
|
|
235
322
|
try {
|
|
@@ -339,7 +426,7 @@ function checkRustInstalled() {
|
|
|
339
426
|
async function combineFiles(files, output) {
|
|
340
427
|
const contents = await Promise.all(files.map(async (file) => {
|
|
341
428
|
if (file.endsWith('.css')) {
|
|
342
|
-
const fileContent = await fs.readFile(file, 'utf-8');
|
|
429
|
+
const fileContent = await fs$1.readFile(file, 'utf-8');
|
|
343
430
|
return `window.addEventListener('DOMContentLoaded', (_event) => {
|
|
344
431
|
const css = ${JSON.stringify(fileContent)};
|
|
345
432
|
const style = document.createElement('style');
|
|
@@ -347,12 +434,12 @@ async function combineFiles(files, output) {
|
|
|
347
434
|
document.head.appendChild(style);
|
|
348
435
|
});`;
|
|
349
436
|
}
|
|
350
|
-
const fileContent = await fs.readFile(file);
|
|
437
|
+
const fileContent = await fs$1.readFile(file);
|
|
351
438
|
return ("window.addEventListener('DOMContentLoaded', (_event) => { " +
|
|
352
439
|
fileContent +
|
|
353
440
|
' });');
|
|
354
441
|
}));
|
|
355
|
-
await fs.writeFile(output, contents.join('\n'));
|
|
442
|
+
await fs$1.writeFile(output, contents.join('\n'));
|
|
356
443
|
return files;
|
|
357
444
|
}
|
|
358
445
|
|
|
@@ -1272,7 +1359,9 @@ class MacBuilder extends BaseBuilder {
|
|
|
1272
1359
|
this.buildArch = validArchs.includes(options.targets || '')
|
|
1273
1360
|
? options.targets
|
|
1274
1361
|
: 'auto';
|
|
1275
|
-
|
|
1362
|
+
// `app` is a valid macOS bundle target (see merge.ts); honour it explicitly.
|
|
1363
|
+
if (options.targets === 'app' ||
|
|
1364
|
+
options.iterativeBuild ||
|
|
1276
1365
|
options.install ||
|
|
1277
1366
|
process.env.PAKE_CREATE_APP === '1') {
|
|
1278
1367
|
this.buildFormat = 'app';
|
|
@@ -1442,20 +1531,47 @@ class LinuxBuilder extends BaseBuilder {
|
|
|
1442
1531
|
throw new Error(`No valid Linux target in "${this.options.targets}". Valid targets: ${LINUX_TARGET_TYPES.join(', ')}.`);
|
|
1443
1532
|
}
|
|
1444
1533
|
const useTemporaryDebForZst = needsTemporaryDebForZst(targets);
|
|
1534
|
+
// With a single explicit target, fail fast. With multiple targets (the
|
|
1535
|
+
// distro-aware default, or an explicit comma list) keep building the rest
|
|
1536
|
+
// when one fails, so a usable installer is still produced, e.g. AppImage
|
|
1537
|
+
// survives a .deb bundler abort on RPM-based distros.
|
|
1538
|
+
const isolateFailures = targets.length > 1;
|
|
1539
|
+
const failed = [];
|
|
1540
|
+
let firstError = null;
|
|
1445
1541
|
for (const target of targets) {
|
|
1446
1542
|
this.currentBuildType = target;
|
|
1447
|
-
|
|
1448
|
-
if (
|
|
1449
|
-
|
|
1543
|
+
try {
|
|
1544
|
+
if (target === 'zst') {
|
|
1545
|
+
if (useTemporaryDebForZst) {
|
|
1546
|
+
await this.buildAndCopy(url, 'deb', false);
|
|
1547
|
+
}
|
|
1548
|
+
await this.createArchPackageFromDeb({
|
|
1549
|
+
removeSourceDeb: useTemporaryDebForZst,
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
else {
|
|
1553
|
+
await this.buildAndCopy(url, target);
|
|
1450
1554
|
}
|
|
1451
|
-
await this.createArchPackageFromDeb({
|
|
1452
|
-
removeSourceDeb: useTemporaryDebForZst,
|
|
1453
|
-
});
|
|
1454
1555
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1556
|
+
catch (error) {
|
|
1557
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1558
|
+
if (!isolateFailures) {
|
|
1559
|
+
throw err;
|
|
1560
|
+
}
|
|
1561
|
+
if (!firstError) {
|
|
1562
|
+
firstError = err;
|
|
1563
|
+
}
|
|
1564
|
+
failed.push(target);
|
|
1565
|
+
logger.warn(`โผ Failed to build "${target}" target: ${err.message.split('\n')[0]}`);
|
|
1457
1566
|
}
|
|
1458
1567
|
}
|
|
1568
|
+
// Every requested target failed: surface the first real error.
|
|
1569
|
+
if (firstError && failed.length === targets.length) {
|
|
1570
|
+
throw firstError;
|
|
1571
|
+
}
|
|
1572
|
+
if (failed.length > 0) {
|
|
1573
|
+
logger.warn(`โผ Skipped failed Linux targets: ${failed.join(', ')}. Other formats built successfully.`);
|
|
1574
|
+
}
|
|
1459
1575
|
}
|
|
1460
1576
|
async ensureArchPackagingTools() {
|
|
1461
1577
|
const requiredTools = [
|
|
@@ -2567,7 +2683,7 @@ const DEFAULT_PAKE_OPTIONS = {
|
|
|
2567
2683
|
targets: (() => {
|
|
2568
2684
|
switch (process.platform) {
|
|
2569
2685
|
case 'linux':
|
|
2570
|
-
return
|
|
2686
|
+
return getDefaultLinuxTargets();
|
|
2571
2687
|
case 'darwin':
|
|
2572
2688
|
return 'dmg';
|
|
2573
2689
|
case 'win32':
|
|
@@ -2619,7 +2735,7 @@ function validateNumberInput(value) {
|
|
|
2619
2735
|
return parsedValue;
|
|
2620
2736
|
}
|
|
2621
2737
|
function validateUrlInput(url) {
|
|
2622
|
-
const isFile = fs
|
|
2738
|
+
const isFile = fs.existsSync(url);
|
|
2623
2739
|
if (!isFile) {
|
|
2624
2740
|
try {
|
|
2625
2741
|
return normalizeUrl(url);
|
|
@@ -2685,7 +2801,7 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2685
2801
|
.addOption(new Option('--maximize', 'Start window maximized')
|
|
2686
2802
|
.default(DEFAULT_PAKE_OPTIONS.maximize)
|
|
2687
2803
|
.hideHelp())
|
|
2688
|
-
.addOption(new Option('--dark-mode', 'Force
|
|
2804
|
+
.addOption(new Option('--dark-mode', 'Force app to use dark mode (supports macOS, Windows, and Linux)')
|
|
2689
2805
|
.default(DEFAULT_PAKE_OPTIONS.darkMode)
|
|
2690
2806
|
.hideHelp())
|
|
2691
2807
|
.addOption(new Option('--disabled-web-shortcuts', 'Disabled webPage shortcuts')
|
package/package.json
CHANGED
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
|
@@ -7,7 +7,6 @@ use tauri::http::Method;
|
|
|
7
7
|
use tauri::{command, AppHandle, Manager, Url, WebviewWindow};
|
|
8
8
|
use tauri_plugin_http::reqwest::{ClientBuilder, Request};
|
|
9
9
|
|
|
10
|
-
#[cfg(target_os = "macos")]
|
|
11
10
|
use tauri::Theme;
|
|
12
11
|
|
|
13
12
|
static BADGE_COUNT: AtomicI64 = AtomicI64::new(0);
|
|
@@ -183,40 +182,24 @@ pub fn set_dock_badge_label(app: AppHandle, label: Option<String>) -> Result<(),
|
|
|
183
182
|
|
|
184
183
|
#[command]
|
|
185
184
|
pub async fn update_theme_mode(app: AppHandle, mode: String) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
};
|
|
194
|
-
let _ = window.set_theme(Some(theme));
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
#[cfg(not(target_os = "macos"))]
|
|
198
|
-
{
|
|
199
|
-
let _ = app;
|
|
200
|
-
let _ = mode;
|
|
185
|
+
if let Some(window) = app.get_webview_window("pake") {
|
|
186
|
+
let theme = if mode == "dark" {
|
|
187
|
+
Theme::Dark
|
|
188
|
+
} else {
|
|
189
|
+
Theme::Light
|
|
190
|
+
};
|
|
191
|
+
let _ = window.set_theme(Some(theme));
|
|
201
192
|
}
|
|
202
193
|
}
|
|
203
194
|
|
|
195
|
+
// Apply native WebView zoom (WKWebView pageZoom / WebView2 ZoomFactor / WebKitGTK
|
|
196
|
+
// zoom level) instead of CSS hacks. CSS `transform: scale` and `html.style.zoom`
|
|
197
|
+
// break complex SPAs like ChatGPT (fixed positioning shifts, unrepainted layers);
|
|
198
|
+
// native zoom recalculates layout the same way a browser does for Cmd/Ctrl +/-.
|
|
204
199
|
#[command]
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// Clear all browsing data successfully
|
|
211
|
-
app.restart();
|
|
212
|
-
Ok(())
|
|
213
|
-
}
|
|
214
|
-
Err(e) => {
|
|
215
|
-
eprintln!("Failed to clear browsing data: {}", e);
|
|
216
|
-
Err(format!("Failed to clear browsing data: {}", e))
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
} else {
|
|
220
|
-
Err("Main window not found".to_string())
|
|
221
|
-
}
|
|
200
|
+
pub fn set_zoom(window: WebviewWindow, percent: f64) -> Result<(), String> {
|
|
201
|
+
let factor = (percent / 100.0).clamp(0.3, 2.0);
|
|
202
|
+
window
|
|
203
|
+
.set_zoom(factor)
|
|
204
|
+
.map_err(|e| format!("Failed to set zoom: {}", e))
|
|
222
205
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
// Menu functionality is only used on macOS
|
|
2
|
-
#![cfg(target_os = "macos")]
|
|
3
|
-
|
|
1
|
+
// Menu functionality is only used on macOS; the module is gated in app/mod.rs.
|
|
4
2
|
use crate::app::window::open_additional_window_safe;
|
|
5
3
|
use tauri::menu::{AboutMetadata, Menu, MenuItem, PredefinedMenuItem, Submenu};
|
|
6
4
|
use tauri::{AppHandle, Manager, Wry};
|
|
@@ -308,7 +306,7 @@ pub fn handle_menu_click(app_handle: &AppHandle, id: &str) {
|
|
|
308
306
|
}
|
|
309
307
|
"clear_cache_restart" => {
|
|
310
308
|
if let Some(window) = app_handle.get_webview_window("pake") {
|
|
311
|
-
if
|
|
309
|
+
if window.clear_all_browsing_data().is_ok() {
|
|
312
310
|
app_handle.restart();
|
|
313
311
|
}
|
|
314
312
|
}
|
|
@@ -61,13 +61,18 @@ pub fn set_system_tray(
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
"quit" => {
|
|
64
|
-
let
|
|
64
|
+
let flags = if _init_fullscreen {
|
|
65
|
+
StateFlags::all()
|
|
66
|
+
} else {
|
|
67
|
+
StateFlags::all() & !StateFlags::FULLSCREEN
|
|
68
|
+
};
|
|
69
|
+
let _ = app.save_window_state(flags);
|
|
65
70
|
app.exit(0);
|
|
66
71
|
}
|
|
67
72
|
_ => (),
|
|
68
73
|
})
|
|
69
|
-
.on_tray_icon_event(move |tray, event|
|
|
70
|
-
TrayIconEvent::Click { button, .. }
|
|
74
|
+
.on_tray_icon_event(move |tray, event| {
|
|
75
|
+
if let TrayIconEvent::Click { button, .. } = event {
|
|
71
76
|
if button == tauri::tray::MouseButton::Left {
|
|
72
77
|
if let Some(window) = tray.app_handle().get_webview_window("pake") {
|
|
73
78
|
let is_visible = window.is_visible().unwrap_or(false);
|
|
@@ -84,7 +89,6 @@ pub fn set_system_tray(
|
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
|
-
_ => {}
|
|
88
92
|
});
|
|
89
93
|
|
|
90
94
|
let resolved_icon = if tray_icon_path.is_empty() {
|
|
@@ -12,8 +12,10 @@ use tauri::{
|
|
|
12
12
|
AppHandle, Config, Manager, Url, WebviewUrl, WebviewWindow, WebviewWindowBuilder,
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
use tauri::Theme;
|
|
16
|
+
|
|
15
17
|
#[cfg(target_os = "macos")]
|
|
16
|
-
use tauri::
|
|
18
|
+
use tauri::TitleBarStyle;
|
|
17
19
|
|
|
18
20
|
#[cfg(target_os = "windows")]
|
|
19
21
|
fn build_proxy_browser_arg(url: &Url) -> Option<String> {
|
|
@@ -344,6 +346,14 @@ fn build_window(
|
|
|
344
346
|
|
|
345
347
|
let mut parsed_proxy_url: Option<Url> = None;
|
|
346
348
|
|
|
349
|
+
// Default to following the system theme (None), only force dark when explicitly set.
|
|
350
|
+
// Computed once; the matching platform block below is the sole consumer.
|
|
351
|
+
let theme = if window_config.dark_mode {
|
|
352
|
+
Some(Theme::Dark)
|
|
353
|
+
} else {
|
|
354
|
+
None // Follow system theme
|
|
355
|
+
};
|
|
356
|
+
|
|
347
357
|
// Platform-specific configuration must be set before proxy on Windows/Linux
|
|
348
358
|
#[cfg(target_os = "macos")]
|
|
349
359
|
{
|
|
@@ -353,20 +363,13 @@ fn build_window(
|
|
|
353
363
|
TitleBarStyle::Visible
|
|
354
364
|
};
|
|
355
365
|
window_builder = window_builder.title_bar_style(title_bar_style);
|
|
356
|
-
|
|
357
|
-
// Default to following system theme (None), only force dark when explicitly set
|
|
358
|
-
let theme = if window_config.dark_mode {
|
|
359
|
-
Some(Theme::Dark)
|
|
360
|
-
} else {
|
|
361
|
-
None // Follow system theme
|
|
362
|
-
};
|
|
363
366
|
window_builder = window_builder.theme(theme);
|
|
364
367
|
}
|
|
365
368
|
|
|
366
369
|
// Windows and Linux: set data_directory before proxy_url
|
|
367
370
|
#[cfg(not(target_os = "macos"))]
|
|
368
371
|
{
|
|
369
|
-
window_builder = window_builder.data_directory(_data_dir).theme(
|
|
372
|
+
window_builder = window_builder.data_directory(_data_dir).theme(theme);
|
|
370
373
|
|
|
371
374
|
if !config.proxy_url.is_empty() {
|
|
372
375
|
if let Ok(proxy_url) = Url::from_str(&config.proxy_url) {
|
|
@@ -11,19 +11,13 @@ const shortcuts = {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
function setZoom(zoom) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
body.style.transformOrigin = "top left";
|
|
22
|
-
body.style.width = `${100 / zoomValue}%`;
|
|
23
|
-
body.style.height = `${100 / zoomValue}%`;
|
|
24
|
-
} else {
|
|
25
|
-
html.style.zoom = zoom;
|
|
26
|
-
window.dispatchEvent(new Event("resize"));
|
|
14
|
+
// Use native WebView zoom (WKWebView pageZoom / WebView2 ZoomFactor) instead of
|
|
15
|
+
// CSS hacks. `transform: scale` and `html.style.zoom` break complex SPAs like
|
|
16
|
+
// ChatGPT: the page shifts right on Windows and parts of the UI stop repainting
|
|
17
|
+
// on macOS. Native zoom recalculates layout exactly like a browser does.
|
|
18
|
+
const invoke = window.__TAURI__?.core?.invoke;
|
|
19
|
+
if (invoke) {
|
|
20
|
+
invoke("set_zoom", { percent: parseFloat(zoom) }).catch(() => {});
|
|
27
21
|
}
|
|
28
22
|
|
|
29
23
|
window.localStorage.setItem("htmlZoom", zoom);
|
package/src-tauri/src/lib.rs
CHANGED
|
@@ -16,11 +16,13 @@ const PAKE_LINUX_WEBKIT_SAFE_MODE: &str = "PAKE_LINUX_WEBKIT_SAFE_MODE";
|
|
|
16
16
|
const WEBKIT_DISABLE_DMABUF_RENDERER: &str = "WEBKIT_DISABLE_DMABUF_RENDERER";
|
|
17
17
|
#[cfg(target_os = "linux")]
|
|
18
18
|
const WEBKIT_DISABLE_COMPOSITING_MODE: &str = "WEBKIT_DISABLE_COMPOSITING_MODE";
|
|
19
|
+
#[cfg(target_os = "linux")]
|
|
20
|
+
const GDK_BACKEND: &str = "GDK_BACKEND";
|
|
19
21
|
|
|
20
22
|
use app::{
|
|
21
23
|
invoke::{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
clear_dock_badge, download_file, increment_dock_badge, send_notification, set_dock_badge,
|
|
25
|
+
set_dock_badge_label, set_zoom, update_theme_mode,
|
|
24
26
|
},
|
|
25
27
|
setup::{set_global_shortcut, set_system_tray},
|
|
26
28
|
window::{open_additional_window_safe, set_window, MultiWindowState},
|
|
@@ -66,6 +68,34 @@ fn should_enable_linux_webkit_safe_mode_from_values(
|
|
|
66
68
|
!is_niri_session
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
#[cfg(any(target_os = "linux", test))]
|
|
72
|
+
fn should_force_wayland_gdk_backend(
|
|
73
|
+
gdk_backend: Option<&str>,
|
|
74
|
+
wayland_display: Option<&str>,
|
|
75
|
+
display: Option<&str>,
|
|
76
|
+
) -> bool {
|
|
77
|
+
// Respect an explicit user choice.
|
|
78
|
+
if is_non_empty_env_value(gdk_backend) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// On pure Wayland compositors without XWayland (e.g. Niri), $DISPLAY is unset
|
|
83
|
+
// and GTK defaults to the X11 backend, which aborts with "Failed to initialize
|
|
84
|
+
// GTK". Wayland is then the only viable backend, so forcing it is safe.
|
|
85
|
+
is_non_empty_env_value(wayland_display) && !is_non_empty_env_value(display)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#[cfg(target_os = "linux")]
|
|
89
|
+
fn apply_linux_gdk_backend() {
|
|
90
|
+
if should_force_wayland_gdk_backend(
|
|
91
|
+
std::env::var(GDK_BACKEND).ok().as_deref(),
|
|
92
|
+
std::env::var("WAYLAND_DISPLAY").ok().as_deref(),
|
|
93
|
+
std::env::var("DISPLAY").ok().as_deref(),
|
|
94
|
+
) {
|
|
95
|
+
std::env::set_var(GDK_BACKEND, "wayland");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
69
99
|
#[cfg(target_os = "linux")]
|
|
70
100
|
fn apply_linux_webkit_runtime_flags() {
|
|
71
101
|
let safe_mode = std::env::var(PAKE_LINUX_WEBKIT_SAFE_MODE).ok();
|
|
@@ -103,7 +133,10 @@ fn apply_linux_webkit_runtime_flags() {
|
|
|
103
133
|
|
|
104
134
|
pub fn run_app() {
|
|
105
135
|
#[cfg(target_os = "linux")]
|
|
106
|
-
|
|
136
|
+
{
|
|
137
|
+
apply_linux_gdk_backend();
|
|
138
|
+
apply_linux_webkit_runtime_flags();
|
|
139
|
+
}
|
|
107
140
|
|
|
108
141
|
let (pake_config, tauri_config) = get_pake_config();
|
|
109
142
|
let tauri_app = tauri::Builder::default();
|
|
@@ -122,7 +155,9 @@ pub fn run_app() {
|
|
|
122
155
|
StateFlags::FULLSCREEN
|
|
123
156
|
} else {
|
|
124
157
|
// Prevent flickering on the first open.
|
|
125
|
-
|
|
158
|
+
// Exclude FULLSCREEN so a prior --fullscreen build's persisted state
|
|
159
|
+
// doesn't force fullscreen on a rebuild without --fullscreen.
|
|
160
|
+
StateFlags::all() & !StateFlags::VISIBLE & !StateFlags::FULLSCREEN
|
|
126
161
|
})
|
|
127
162
|
.build();
|
|
128
163
|
|
|
@@ -159,7 +194,7 @@ pub fn run_app() {
|
|
|
159
194
|
set_dock_badge_label,
|
|
160
195
|
clear_dock_badge,
|
|
161
196
|
update_theme_mode,
|
|
162
|
-
|
|
197
|
+
set_zoom,
|
|
163
198
|
])
|
|
164
199
|
.setup(move |app| {
|
|
165
200
|
app.manage(MultiWindowState::new(
|
|
@@ -329,4 +364,45 @@ mod tests {
|
|
|
329
364
|
);
|
|
330
365
|
}
|
|
331
366
|
}
|
|
367
|
+
|
|
368
|
+
#[test]
|
|
369
|
+
fn forces_wayland_backend_on_pure_wayland() {
|
|
370
|
+
assert!(should_force_wayland_gdk_backend(
|
|
371
|
+
None,
|
|
372
|
+
Some("wayland-0"),
|
|
373
|
+
None
|
|
374
|
+
));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#[test]
|
|
378
|
+
fn forces_wayland_backend_when_display_is_blank() {
|
|
379
|
+
assert!(should_force_wayland_gdk_backend(
|
|
380
|
+
None,
|
|
381
|
+
Some("wayland-0"),
|
|
382
|
+
Some(" ")
|
|
383
|
+
));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
#[test]
|
|
387
|
+
fn keeps_default_backend_when_x11_display_present() {
|
|
388
|
+
assert!(!should_force_wayland_gdk_backend(
|
|
389
|
+
None,
|
|
390
|
+
Some("wayland-0"),
|
|
391
|
+
Some(":0")
|
|
392
|
+
));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
#[test]
|
|
396
|
+
fn keeps_default_backend_without_wayland_display() {
|
|
397
|
+
assert!(!should_force_wayland_gdk_backend(None, None, None));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
#[test]
|
|
401
|
+
fn respects_explicit_gdk_backend_override() {
|
|
402
|
+
assert!(!should_force_wayland_gdk_backend(
|
|
403
|
+
Some("x11"),
|
|
404
|
+
Some("wayland-0"),
|
|
405
|
+
None
|
|
406
|
+
));
|
|
407
|
+
}
|
|
332
408
|
}
|