pake-cli 3.16.3 → 3.17.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 +1 -1
- package/dist/cli.js +129 -6
- package/package.json +1 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/app/window.rs +40 -1
- package/src-tauri/src/lib.rs +140 -23
- package/src-tauri/src/util.rs +39 -0
- package/src-tauri/tauri.conf.json +1 -1
package/README.md
CHANGED
|
@@ -192,7 +192,7 @@ pake https://weekly.tw93.fun --name Weekly --icon https://cdn.tw93.fun/pake/week
|
|
|
192
192
|
|
|
193
193
|
First-time packaging requires environment setup and may be slower, subsequent builds are fast. For complete parameter documentation, see [CLI Usage Guide](docs/cli-usage.md). Don't want to use CLI? Try [GitHub Actions Online Building](docs/github-actions-usage.md).
|
|
194
194
|
|
|
195
|
-
Using Pake from a script or AI agent? Pass `--json` for machine-readable results, describe apps declaratively with `--config app.json` ([schema](schema/pake.schema.json)), and package local build output directly with `pake ./dist --name MyTool`. See [llms.txt](llms.txt) for the full agent contract.
|
|
195
|
+
Using Pake from a script or AI agent? Pass `--json` for machine-readable results, describe apps declaratively with `--config app.json` ([schema](schema/pake.schema.json)), and package local build output directly with `pake ./dist --name MyTool`. See [llms.txt](llms.txt) for the full agent contract. To install the official skill, run `/plugin marketplace add tw93/Pake` and `/plugin install pake@pake` in Claude Code, or `codex plugin marketplace add tw93/Pake` and `codex plugin add pake@pake` in Codex.
|
|
196
196
|
|
|
197
197
|
Copy this to your AI agent to get started:
|
|
198
198
|
|
package/dist/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ import * as psl from 'psl';
|
|
|
19
19
|
import { InvalidArgumentError, program as program$1, Option } from 'commander';
|
|
20
20
|
|
|
21
21
|
var name = "pake-cli";
|
|
22
|
-
var version = "3.
|
|
22
|
+
var version = "3.17.0";
|
|
23
23
|
var description = "🤱🏻 Turn any webpage into a desktop app with one command. 🤱🏻 一键打包网页生成轻量桌面应用。";
|
|
24
24
|
var engines = {
|
|
25
25
|
node: ">=20.9.0"
|
|
@@ -787,6 +787,49 @@ function checkRustInstalled() {
|
|
|
787
787
|
return false;
|
|
788
788
|
}
|
|
789
789
|
}
|
|
790
|
+
function getVsWherePath() {
|
|
791
|
+
const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
|
|
792
|
+
return path.join(programFilesX86, 'Microsoft Visual Studio', 'Installer', 'vswhere.exe');
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Detects Visual Studio Build Tools the way rustc itself does: via the VS
|
|
796
|
+
* installer's vswhere.exe, not PATH. cl.exe/link.exe are normally absent
|
|
797
|
+
* from PATH even on a fully working MSVC setup (rustc locates them through
|
|
798
|
+
* the same registry vswhere reads), so checking PATH directly would warn on
|
|
799
|
+
* most MSVC machines.
|
|
800
|
+
*/
|
|
801
|
+
function hasWindowsMsvcBuildTools() {
|
|
802
|
+
const vswhere = getVsWherePath();
|
|
803
|
+
if (!fsExtra.pathExistsSync(vswhere))
|
|
804
|
+
return false;
|
|
805
|
+
try {
|
|
806
|
+
const { stdout } = execaSync(vswhere, [
|
|
807
|
+
'-products',
|
|
808
|
+
'*',
|
|
809
|
+
'-requires',
|
|
810
|
+
'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
|
|
811
|
+
'-property',
|
|
812
|
+
'installationPath',
|
|
813
|
+
]);
|
|
814
|
+
return stdout.trim().length > 0;
|
|
815
|
+
}
|
|
816
|
+
catch {
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* MinGW/MSYS2's gcc drives the GNU Windows target directly off PATH (no
|
|
822
|
+
* registry lookup involved), so a PATH check is the correct signal here.
|
|
823
|
+
*/
|
|
824
|
+
function hasWindowsGnuToolchain() {
|
|
825
|
+
try {
|
|
826
|
+
execaSync('gcc', ['--version'], { stdio: 'ignore' });
|
|
827
|
+
return true;
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
790
833
|
|
|
791
834
|
async function combineFiles(files, output) {
|
|
792
835
|
const contents = await Promise.all(files.map(async (file) => {
|
|
@@ -1361,6 +1404,27 @@ function getBuildEnvironment() {
|
|
|
1361
1404
|
PATH: buildPath,
|
|
1362
1405
|
};
|
|
1363
1406
|
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Build scripts and proc-macros compile for the rustup *host* toolchain, not
|
|
1409
|
+
* the `--target` triple, even when cross-compiling. Pake's own
|
|
1410
|
+
* rust-toolchain.toml pins a bare channel (no host), which rustup resolves
|
|
1411
|
+
* against the machine's configured default host — msvc on most Windows
|
|
1412
|
+
* installs, regardless of whether MSVC is actually present. Without this,
|
|
1413
|
+
* a gnu `--target` build still shells out to the (possibly missing) MSVC
|
|
1414
|
+
* `link.exe` for every build script. RUSTUP_TOOLCHAIN is rustup's documented
|
|
1415
|
+
* per-invocation override (read by the cargo/rustc proxies it installs) and
|
|
1416
|
+
* only affects this build subprocess. Left alone if the user already set it.
|
|
1417
|
+
*/
|
|
1418
|
+
function getWindowsGnuBuildEnvironment() {
|
|
1419
|
+
const excludeAllSymbols = '-C link-args=-Wl,--exclude-all-symbols';
|
|
1420
|
+
const existingRustflags = process.env.RUSTFLAGS;
|
|
1421
|
+
return {
|
|
1422
|
+
RUSTFLAGS: existingRustflags
|
|
1423
|
+
? `${existingRustflags} ${excludeAllSymbols}`
|
|
1424
|
+
: excludeAllSymbols,
|
|
1425
|
+
RUSTUP_TOOLCHAIN: process.env.RUSTUP_TOOLCHAIN || 'stable-x86_64-pc-windows-gnu',
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1364
1428
|
/**
|
|
1365
1429
|
* Windows needs more time due to native compilation and antivirus scanning.
|
|
1366
1430
|
*/
|
|
@@ -1575,6 +1639,13 @@ class BaseBuilder {
|
|
|
1575
1639
|
logger.warn('✼ See more in https://tauri.app/start/prerequisites/.');
|
|
1576
1640
|
}
|
|
1577
1641
|
ensureRustEnv();
|
|
1642
|
+
if (IS_WIN &&
|
|
1643
|
+
this.options.windowsToolchain !== 'gnu' &&
|
|
1644
|
+
!hasWindowsMsvcBuildTools() &&
|
|
1645
|
+
hasWindowsGnuToolchain()) {
|
|
1646
|
+
logger.warn('✼ No Visual Studio Build Tools detected, but a MinGW/GNU toolchain (gcc) is available.');
|
|
1647
|
+
logger.warn('✼ If the build fails to link, retry with --windows-toolchain gnu.');
|
|
1648
|
+
}
|
|
1578
1649
|
if (!checkRustInstalled()) {
|
|
1579
1650
|
if (!isInteractive()) {
|
|
1580
1651
|
throw new PakeError('Rust required to package your webapp.', {
|
|
@@ -1675,8 +1746,10 @@ class BaseBuilder {
|
|
|
1675
1746
|
// entries feed the --json warnings array and this is a status line.
|
|
1676
1747
|
logger.info('✸ Building app...');
|
|
1677
1748
|
const baseEnv = getBuildEnvironment();
|
|
1749
|
+
const isWindowsGnuBuild = process.platform === 'win32' && this.options.windowsToolchain === 'gnu';
|
|
1678
1750
|
let buildEnv = {
|
|
1679
1751
|
...(baseEnv ?? {}),
|
|
1752
|
+
...(isWindowsGnuBuild ? getWindowsGnuBuildEnvironment() : {}),
|
|
1680
1753
|
...(process.env.NO_STRIP ? { NO_STRIP: process.env.NO_STRIP } : {}),
|
|
1681
1754
|
};
|
|
1682
1755
|
const resolveExecEnv = () => Object.keys(buildEnv).length > 0 ? buildEnv : undefined;
|
|
@@ -2038,11 +2111,18 @@ class WinBuilder extends BaseBuilder {
|
|
|
2038
2111
|
this.buildArch = validArchs.includes(options.targets || '')
|
|
2039
2112
|
? this.resolveTargetArch(options.targets)
|
|
2040
2113
|
: this.resolveTargetArch('auto');
|
|
2114
|
+
this.toolchain = options.windowsToolchain === 'gnu' ? 'gnu' : 'msvc';
|
|
2041
2115
|
this.options.targets = this.buildFormat;
|
|
2042
2116
|
}
|
|
2043
2117
|
getReportArch() {
|
|
2044
2118
|
return this.buildArch;
|
|
2045
2119
|
}
|
|
2120
|
+
getTauriTarget(arch, platform = 'win32') {
|
|
2121
|
+
if (this.toolchain === 'gnu') {
|
|
2122
|
+
return WinBuilder.GNU_ARCH_MAPPINGS[arch] || null;
|
|
2123
|
+
}
|
|
2124
|
+
return super.getTauriTarget(arch, platform);
|
|
2125
|
+
}
|
|
2046
2126
|
getFileName() {
|
|
2047
2127
|
const { name } = this.options;
|
|
2048
2128
|
const language = this.options.installerLanguage;
|
|
@@ -2082,6 +2162,12 @@ class WinBuilder extends BaseBuilder {
|
|
|
2082
2162
|
return `pake-${generateIdentifierSafeName(appName)}.exe`;
|
|
2083
2163
|
}
|
|
2084
2164
|
}
|
|
2165
|
+
// MSYS2/MinGW only ships an x86_64 GCC toolchain, so gnu is x64-only;
|
|
2166
|
+
// arm64 falls through to getTauriTarget returning null, which the
|
|
2167
|
+
// existing call sites already turn into "Unsupported architecture".
|
|
2168
|
+
WinBuilder.GNU_ARCH_MAPPINGS = {
|
|
2169
|
+
x64: 'x86_64-pc-windows-gnu',
|
|
2170
|
+
};
|
|
2085
2171
|
|
|
2086
2172
|
class LinuxBuilder extends BaseBuilder {
|
|
2087
2173
|
constructor(options) {
|
|
@@ -3537,6 +3623,7 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
3537
3623
|
.default(DEFAULT_PAKE_OPTIONS.userAgent)
|
|
3538
3624
|
.hideHelp())
|
|
3539
3625
|
.addOption(new Option('--targets <string>', 'Build target format for your system').default(DEFAULT_PAKE_OPTIONS.targets))
|
|
3626
|
+
.addOption(new Option('--windows-toolchain <toolchain>', 'Windows Rust toolchain: msvc (default, requires Visual Studio Build Tools) or gnu (MinGW/MSYS2, for machines without them)').choices(['msvc', 'gnu']))
|
|
3540
3627
|
.addOption(new Option('--app-version <string>', 'App version, the same as package.json version')
|
|
3541
3628
|
.default(DEFAULT_PAKE_OPTIONS.appVersion)
|
|
3542
3629
|
.hideHelp())
|
|
@@ -3570,7 +3657,7 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
3570
3657
|
return true;
|
|
3571
3658
|
if (value === 'false')
|
|
3572
3659
|
return false;
|
|
3573
|
-
throw new
|
|
3660
|
+
throw new InvalidArgumentError('--hide-on-close must be true or false');
|
|
3574
3661
|
})
|
|
3575
3662
|
.hideHelp())
|
|
3576
3663
|
.addOption(new Option('--title <string>', 'Window title').hideHelp())
|
|
@@ -3612,7 +3699,7 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
3612
3699
|
.argParser((value) => {
|
|
3613
3700
|
const zoom = Number(value);
|
|
3614
3701
|
if (!Number.isInteger(zoom) || zoom < 50 || zoom > 200) {
|
|
3615
|
-
throw new
|
|
3702
|
+
throw new InvalidArgumentError('--zoom must be an integer between 50 and 200');
|
|
3616
3703
|
}
|
|
3617
3704
|
return zoom;
|
|
3618
3705
|
})
|
|
@@ -3668,12 +3755,20 @@ const REJECTED_KEYS = new Set(['config', 'json', 'version']);
|
|
|
3668
3755
|
const EXTRA_STRING_KEYS = new Set(['name', 'title', 'identifier']);
|
|
3669
3756
|
// Numeric fields share the CLI flag ranges (see cli-program.ts validators),
|
|
3670
3757
|
// so a config file cannot smuggle a value the same flag would reject.
|
|
3758
|
+
// Like --zoom, zoom must be an integer: pake.json stores it as a Rust u32.
|
|
3671
3759
|
const NUMBER_RANGES = {
|
|
3672
3760
|
width: { min: 0 },
|
|
3673
3761
|
height: { min: 0 },
|
|
3674
3762
|
minWidth: { min: 0 },
|
|
3675
3763
|
minHeight: { min: 0 },
|
|
3676
|
-
zoom: { min: 50, max: 200 },
|
|
3764
|
+
zoom: { min: 50, max: 200, integer: true },
|
|
3765
|
+
};
|
|
3766
|
+
// Fields whose CLI flag restricts the value set (see the .choices() calls in
|
|
3767
|
+
// cli-program.ts), so a config file cannot smuggle a value the same flag
|
|
3768
|
+
// would reject. Without this, an unknown value falls through to the builder's
|
|
3769
|
+
// own fallback and silently produces the default behavior.
|
|
3770
|
+
const ENUM_VALUES = {
|
|
3771
|
+
windowsToolchain: ['msvc', 'gnu'],
|
|
3677
3772
|
};
|
|
3678
3773
|
function expectedTypeFor(key) {
|
|
3679
3774
|
if (key === 'inject')
|
|
@@ -3752,15 +3847,25 @@ async function loadConfigFile(configPath, validKeys) {
|
|
|
3752
3847
|
hint: 'See schema/pake.schema.json for field types.',
|
|
3753
3848
|
});
|
|
3754
3849
|
}
|
|
3850
|
+
const allowed = ENUM_VALUES[key];
|
|
3851
|
+
if (allowed && !allowed.includes(value)) {
|
|
3852
|
+
throw new PakeError(`Config field "${key}" must be one of: ${allowed.join(', ')}.`, {
|
|
3853
|
+
code: 'INVALID_INPUT',
|
|
3854
|
+
hint: 'See schema/pake.schema.json for allowed values.',
|
|
3855
|
+
});
|
|
3856
|
+
}
|
|
3755
3857
|
if (typeof value === 'number') {
|
|
3756
3858
|
const range = NUMBER_RANGES[key];
|
|
3757
3859
|
const min = range?.min ?? 0;
|
|
3758
3860
|
const max = range?.max;
|
|
3861
|
+
const integer = range?.integer === true;
|
|
3759
3862
|
if (!Number.isFinite(value) ||
|
|
3863
|
+
(integer && !Number.isInteger(value)) ||
|
|
3760
3864
|
value < min ||
|
|
3761
3865
|
(max !== undefined && value > max)) {
|
|
3762
3866
|
const bounds = max !== undefined ? `${min}-${max}` : `>= ${min}`;
|
|
3763
|
-
|
|
3867
|
+
const kind = integer ? 'an integer' : 'a finite number';
|
|
3868
|
+
throw new PakeError(`Config field "${key}" must be ${kind} (${bounds}).`, {
|
|
3764
3869
|
code: 'INVALID_INPUT',
|
|
3765
3870
|
hint: 'See schema/pake.schema.json for field ranges.',
|
|
3766
3871
|
});
|
|
@@ -3968,6 +4073,19 @@ program.parseAsync().catch((error) => {
|
|
|
3968
4073
|
// Parse errors (unknown option, invalid argument, missing value) are
|
|
3969
4074
|
// invalid input. Commander already printed the message to stderr; in
|
|
3970
4075
|
// json mode also emit the machine-readable result on stdout.
|
|
4076
|
+
//
|
|
4077
|
+
// Excess operands are almost always an unquoted value: `--name Google
|
|
4078
|
+
// Translate` leaves `Translate` as a second operand, and commander's
|
|
4079
|
+
// "too many arguments" names neither --name nor quoting, so the shape
|
|
4080
|
+
// reads as "names with spaces are unsupported" (#1378).
|
|
4081
|
+
const excessArguments = error.code === 'commander.excessArguments';
|
|
4082
|
+
// Name the operand rather than asserting why it is there. Unquoting is the
|
|
4083
|
+
// usual cause, but two bare URLs produce the same error and the quoting
|
|
4084
|
+
// advice would be wrong for them.
|
|
4085
|
+
const extraOperand = excessArguments ? program.args.slice(1)[0] : undefined;
|
|
4086
|
+
const hint = excessArguments
|
|
4087
|
+
? `Unexpected extra argument${extraOperand ? ` "${extraOperand}"` : ''}. A value containing spaces must be quoted, for example --name "Google Translate".`
|
|
4088
|
+
: 'Run pake --help for the accepted options.';
|
|
3971
4089
|
if (process.argv.includes('--json')) {
|
|
3972
4090
|
printJsonResult({
|
|
3973
4091
|
ok: false,
|
|
@@ -3979,10 +4097,15 @@ program.parseAsync().catch((error) => {
|
|
|
3979
4097
|
error: {
|
|
3980
4098
|
code: 'INVALID_INPUT',
|
|
3981
4099
|
message: error.message.trim(),
|
|
3982
|
-
hint
|
|
4100
|
+
hint,
|
|
3983
4101
|
},
|
|
3984
4102
|
});
|
|
3985
4103
|
}
|
|
4104
|
+
else if (excessArguments) {
|
|
4105
|
+
// Commander has already written its message and the full help, so this
|
|
4106
|
+
// lands last, which is where the eye goes after a wall of help text.
|
|
4107
|
+
console.error(chalk.red(`\u2715 ${hint}`));
|
|
4108
|
+
}
|
|
3986
4109
|
process.exitCode = ERROR_EXIT_CODES.INVALID_INPUT;
|
|
3987
4110
|
return;
|
|
3988
4111
|
}
|
package/package.json
CHANGED
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
|
@@ -405,6 +405,27 @@ fn build_window(
|
|
|
405
405
|
))
|
|
406
406
|
})?;
|
|
407
407
|
|
|
408
|
+
let restored_url = if label == "pake"
|
|
409
|
+
&& window_config.url_type == "web"
|
|
410
|
+
&& !window_config.incognito
|
|
411
|
+
{
|
|
412
|
+
match app.path().app_data_dir() {
|
|
413
|
+
Ok(directory) => match crate::util::read_last_url(&directory.join("last-url.txt")) {
|
|
414
|
+
Ok(url) => url,
|
|
415
|
+
Err(error) => {
|
|
416
|
+
eprintln!("[Pake] Could not read the last URL: {error}");
|
|
417
|
+
None
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
Err(error) => {
|
|
421
|
+
eprintln!("[Pake] Could not locate the last URL: {error}");
|
|
422
|
+
None
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
None
|
|
427
|
+
};
|
|
428
|
+
|
|
408
429
|
// On macOS both HTTP Basic auth and certificate bypass use the same
|
|
409
430
|
// navigation-delegate proxy. Start on a neutral page so the proxy is in
|
|
410
431
|
// place before the target can issue its first authentication challenge.
|
|
@@ -413,11 +434,15 @@ fn build_window(
|
|
|
413
434
|
&& window_config.url_type == "web"
|
|
414
435
|
&& (config.basic_auth || window_config.ignore_certificate_errors)
|
|
415
436
|
{
|
|
416
|
-
|
|
437
|
+
restored_url
|
|
438
|
+
.clone()
|
|
439
|
+
.or_else(|| Url::parse(&window_config.url).ok())
|
|
417
440
|
} else {
|
|
418
441
|
None
|
|
419
442
|
};
|
|
420
443
|
|
|
444
|
+
let url = restored_url.map(WebviewUrl::External).unwrap_or(url);
|
|
445
|
+
|
|
421
446
|
// The delegate must be installed before the first TLS challenge. Start on
|
|
422
447
|
// a neutral page, then navigate from the with_webview callback below.
|
|
423
448
|
#[cfg(target_os = "macos")]
|
|
@@ -821,6 +846,20 @@ fn build_window(
|
|
|
821
846
|
Ok(window)
|
|
822
847
|
}
|
|
823
848
|
|
|
849
|
+
pub fn save_last_url(app: &AppHandle) {
|
|
850
|
+
let Some(window) = app.get_webview_window("pake") else {
|
|
851
|
+
return;
|
|
852
|
+
};
|
|
853
|
+
let result = (|| -> Result<(), Box<dyn std::error::Error>> {
|
|
854
|
+
let path = app.path().app_data_dir()?.join("last-url.txt");
|
|
855
|
+
crate::util::write_last_url(&path, &window.url()?)?;
|
|
856
|
+
Ok(())
|
|
857
|
+
})();
|
|
858
|
+
if let Err(error) = result {
|
|
859
|
+
eprintln!("[Pake] Could not save the last URL: {error}");
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
824
863
|
#[cfg(all(test, target_os = "windows"))]
|
|
825
864
|
mod proxy_arg_tests {
|
|
826
865
|
use super::*;
|
package/src-tauri/src/lib.rs
CHANGED
|
@@ -32,8 +32,8 @@ use app::{
|
|
|
32
32
|
},
|
|
33
33
|
setup::{set_global_shortcut, set_system_tray},
|
|
34
34
|
window::{
|
|
35
|
-
open_additional_window_safe, reapply_window_icon, reveal_built_window,
|
|
36
|
-
MultiWindowState,
|
|
35
|
+
open_additional_window_safe, reapply_window_icon, reveal_built_window, save_last_url,
|
|
36
|
+
set_window, MultiWindowState,
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
39
|
use util::get_pake_config;
|
|
@@ -107,16 +107,51 @@ fn contains_niri(value: &str) -> bool {
|
|
|
107
107
|
.any(|part| part.eq_ignore_ascii_case("niri"))
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/// The two WebKit workarounds have different origins and therefore different
|
|
111
|
+
/// scopes, which one gate cannot express.
|
|
112
|
+
///
|
|
113
|
+
/// `WEBKIT_DISABLE_DMABUF_RENDERER` came from #1117 (96e57376) for Linux
|
|
114
|
+
/// stability in general, three months before anything Wayland-specific, and
|
|
115
|
+
/// upstream still reports the blank window it prevents on X11 with the NVIDIA
|
|
116
|
+
/// proprietary driver (tauri-apps/tauri#9394). It stays on for every session.
|
|
110
117
|
#[cfg(any(target_os = "linux", test))]
|
|
111
|
-
fn
|
|
118
|
+
fn should_disable_dmabuf_renderer(safe_mode: Option<&str>) -> bool {
|
|
119
|
+
match safe_mode.filter(|value| !value.trim().is_empty()) {
|
|
120
|
+
Some(value) => !is_disabled_env_value(value),
|
|
121
|
+
None => true,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// `WEBKIT_DISABLE_COMPOSITING_MODE` came from cb911ec7, for a blank screen on
|
|
126
|
+
/// Wayland without a GPU. X11 never had that failure, and disabling compositing
|
|
127
|
+
/// there segfaults WebKitGTK 2.52 as soon as a video decodes (#1374, Intel i915
|
|
128
|
+
/// under i3), so this one is Wayland-only. niri keeps its existing exception.
|
|
129
|
+
/// `PAKE_LINUX_WEBKIT_SAFE_MODE` still forces or suppresses both anywhere.
|
|
130
|
+
#[cfg(any(target_os = "linux", test))]
|
|
131
|
+
fn should_disable_compositing_mode(
|
|
112
132
|
safe_mode: Option<&str>,
|
|
113
133
|
niri_socket: Option<&str>,
|
|
134
|
+
wayland_display: Option<&str>,
|
|
135
|
+
gdk_backend: Option<&str>,
|
|
114
136
|
desktop_values: &[Option<&str>],
|
|
115
137
|
) -> bool {
|
|
116
138
|
if let Some(value) = safe_mode.filter(|value| !value.trim().is_empty()) {
|
|
117
139
|
return !is_disabled_env_value(value);
|
|
118
140
|
}
|
|
119
141
|
|
|
142
|
+
// WAYLAND_DISPLAY says a compositor is reachable, not that GTK will use it.
|
|
143
|
+
// An explicit GDK_BACKEND=x11 renders through XWayland, which is the X11
|
|
144
|
+
// path this flag crashes, and it is also the workaround recommended in
|
|
145
|
+
// #1117, so the two would otherwise collide. should_force_wayland_gdk_backend
|
|
146
|
+
// already treats an explicit backend as authoritative; so does this.
|
|
147
|
+
if gdk_backend.is_some_and(|value| value.trim().eq_ignore_ascii_case("x11")) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if !is_non_empty_env_value(wayland_display) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
120
155
|
let is_niri_session = is_non_empty_env_value(niri_socket)
|
|
121
156
|
|| desktop_values
|
|
122
157
|
.iter()
|
|
@@ -173,18 +208,20 @@ fn apply_linux_webkit_runtime_flags() {
|
|
|
173
208
|
.map(|value| value.as_deref())
|
|
174
209
|
.collect::<Vec<_>>();
|
|
175
210
|
|
|
176
|
-
if
|
|
211
|
+
if should_disable_dmabuf_renderer(safe_mode.as_deref())
|
|
212
|
+
&& std::env::var(WEBKIT_DISABLE_DMABUF_RENDERER).is_err()
|
|
213
|
+
{
|
|
214
|
+
std::env::set_var(WEBKIT_DISABLE_DMABUF_RENDERER, "1");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if should_disable_compositing_mode(
|
|
177
218
|
safe_mode.as_deref(),
|
|
178
219
|
std::env::var("NIRI_SOCKET").ok().as_deref(),
|
|
220
|
+
std::env::var("WAYLAND_DISPLAY").ok().as_deref(),
|
|
221
|
+
std::env::var(GDK_BACKEND).ok().as_deref(),
|
|
179
222
|
&desktop_refs,
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if std::env::var(WEBKIT_DISABLE_DMABUF_RENDERER).is_err() {
|
|
185
|
-
std::env::set_var(WEBKIT_DISABLE_DMABUF_RENDERER, "1");
|
|
186
|
-
}
|
|
187
|
-
if std::env::var(WEBKIT_DISABLE_COMPOSITING_MODE).is_err() {
|
|
223
|
+
) && std::env::var(WEBKIT_DISABLE_COMPOSITING_MODE).is_err()
|
|
224
|
+
{
|
|
188
225
|
std::env::set_var(WEBKIT_DISABLE_COMPOSITING_MODE, "1");
|
|
189
226
|
}
|
|
190
227
|
}
|
|
@@ -201,6 +238,8 @@ pub fn run_app() {
|
|
|
201
238
|
|
|
202
239
|
let show_system_tray = pake_config.show_system_tray();
|
|
203
240
|
let hide_on_close = pake_config.windows[0].hide_on_close;
|
|
241
|
+
let remember_url =
|
|
242
|
+
pake_config.windows[0].url_type == "web" && !pake_config.windows[0].incognito;
|
|
204
243
|
let activation_shortcut = pake_config.windows[0].activation_shortcut.clone();
|
|
205
244
|
let init_fullscreen = pake_config.windows[0].fullscreen;
|
|
206
245
|
let start_to_tray = pake_config.windows[0].start_to_tray && show_system_tray; // Only valid when tray is enabled
|
|
@@ -357,6 +396,9 @@ pub fn run_app() {
|
|
|
357
396
|
})
|
|
358
397
|
.on_window_event(move |_window, _event| {
|
|
359
398
|
if let tauri::WindowEvent::CloseRequested { api, .. } = _event {
|
|
399
|
+
if remember_url && _window.label() == "pake" {
|
|
400
|
+
save_last_url(_window.app_handle());
|
|
401
|
+
}
|
|
360
402
|
if hide_on_close && _window.label() == "pake" {
|
|
361
403
|
// User dismissed the window; do not let startup reveal reopen it.
|
|
362
404
|
cancel_startup_reveal(&close_revealed);
|
|
@@ -395,6 +437,9 @@ pub fn run_app() {
|
|
|
395
437
|
std::process::exit(1);
|
|
396
438
|
})
|
|
397
439
|
.run(move |_app, _event| {
|
|
440
|
+
if remember_url && matches!(&_event, tauri::RunEvent::Exit) {
|
|
441
|
+
save_last_url(_app);
|
|
442
|
+
}
|
|
398
443
|
// Handle macOS dock icon click to reopen hidden window
|
|
399
444
|
#[cfg(target_os = "macos")]
|
|
400
445
|
if let tauri::RunEvent::Reopen {
|
|
@@ -463,51 +508,123 @@ mod tests {
|
|
|
463
508
|
}
|
|
464
509
|
|
|
465
510
|
#[test]
|
|
466
|
-
fn
|
|
467
|
-
|
|
511
|
+
fn dmabuf_renderer_stays_disabled_on_every_session() {
|
|
512
|
+
// #1117 was never Wayland-scoped, and upstream still reports the blank
|
|
513
|
+
// window it prevents on X11 with the NVIDIA proprietary driver.
|
|
514
|
+
assert!(should_disable_dmabuf_renderer(None));
|
|
515
|
+
assert!(should_disable_dmabuf_renderer(Some("1")));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
#[test]
|
|
519
|
+
fn dmabuf_renderer_can_be_re_enabled_explicitly() {
|
|
520
|
+
for value in ["0", "false", "off", "no", "native", "disabled"] {
|
|
521
|
+
assert!(
|
|
522
|
+
!should_disable_dmabuf_renderer(Some(value)),
|
|
523
|
+
"expected {value} to restore the dmabuf renderer"
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
#[test]
|
|
529
|
+
fn x11_keeps_dmabuf_disabled_but_keeps_compositing() {
|
|
530
|
+
// The exact shape of #1374: an X11 session gets the stability flag it
|
|
531
|
+
// has had since #1117, and does not get the compositing flag that
|
|
532
|
+
// segfaults playback there.
|
|
533
|
+
let desktop = [Some("i3"), None, None];
|
|
534
|
+
assert!(should_disable_dmabuf_renderer(None));
|
|
535
|
+
assert!(!should_disable_compositing_mode(
|
|
536
|
+
None, None, None, None, &desktop
|
|
537
|
+
));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
#[test]
|
|
541
|
+
fn explicit_x11_gdk_backend_keeps_compositing_on_wayland() {
|
|
542
|
+
// XWayland renders through the X11 path this flag crashes, and
|
|
543
|
+
// GDK_BACKEND=x11 is the workaround recommended in #1117.
|
|
544
|
+
assert!(!should_disable_compositing_mode(
|
|
545
|
+
None,
|
|
546
|
+
None,
|
|
547
|
+
Some("wayland-0"),
|
|
548
|
+
Some("x11"),
|
|
549
|
+
&[None, None, None]
|
|
550
|
+
));
|
|
551
|
+
// An explicit wayland backend is still a Wayland session.
|
|
552
|
+
assert!(should_disable_compositing_mode(
|
|
553
|
+
None,
|
|
554
|
+
None,
|
|
555
|
+
Some("wayland-0"),
|
|
556
|
+
Some("wayland"),
|
|
557
|
+
&[None, None, None]
|
|
558
|
+
));
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
#[test]
|
|
562
|
+
fn compositing_mode_stays_disabled_by_default_on_wayland() {
|
|
563
|
+
assert!(should_disable_compositing_mode(
|
|
564
|
+
None,
|
|
468
565
|
None,
|
|
566
|
+
Some("wayland-0"),
|
|
469
567
|
None,
|
|
470
568
|
&[None, None, None]
|
|
471
569
|
));
|
|
472
570
|
}
|
|
473
571
|
|
|
474
572
|
#[test]
|
|
475
|
-
fn
|
|
476
|
-
assert!(
|
|
573
|
+
fn compositing_mode_can_be_forced_on_x11() {
|
|
574
|
+
assert!(should_disable_compositing_mode(
|
|
575
|
+
Some("1"),
|
|
576
|
+
None,
|
|
577
|
+
None,
|
|
578
|
+
None,
|
|
579
|
+
&[Some("i3"), None, None]
|
|
580
|
+
));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
#[test]
|
|
584
|
+
fn compositing_mode_is_kept_for_niri_socket() {
|
|
585
|
+
assert!(!should_disable_compositing_mode(
|
|
477
586
|
None,
|
|
478
587
|
Some("/run/user/501/niri.sock"),
|
|
588
|
+
Some("wayland-0"),
|
|
589
|
+
None,
|
|
479
590
|
&[None, None, None]
|
|
480
591
|
));
|
|
481
592
|
}
|
|
482
593
|
|
|
483
594
|
#[test]
|
|
484
|
-
fn
|
|
485
|
-
assert!(!
|
|
595
|
+
fn compositing_mode_is_kept_for_niri_desktop() {
|
|
596
|
+
assert!(!should_disable_compositing_mode(
|
|
486
597
|
None,
|
|
487
598
|
None,
|
|
599
|
+
Some("wayland-0"),
|
|
600
|
+
None,
|
|
488
601
|
&[Some("niri"), None, None]
|
|
489
602
|
));
|
|
490
603
|
}
|
|
491
604
|
|
|
492
605
|
#[test]
|
|
493
|
-
fn
|
|
494
|
-
assert!(
|
|
606
|
+
fn compositing_mode_can_be_forced_on_for_niri() {
|
|
607
|
+
assert!(should_disable_compositing_mode(
|
|
495
608
|
Some("1"),
|
|
496
609
|
Some("/run/user/501/niri.sock"),
|
|
610
|
+
Some("wayland-0"),
|
|
611
|
+
None,
|
|
497
612
|
&[Some("niri"), None, None]
|
|
498
613
|
));
|
|
499
614
|
}
|
|
500
615
|
|
|
501
616
|
#[test]
|
|
502
|
-
fn
|
|
617
|
+
fn compositing_mode_can_be_disabled_explicitly() {
|
|
503
618
|
for value in ["0", "false", "off", "no", "native", "disabled"] {
|
|
504
619
|
assert!(
|
|
505
|
-
!
|
|
620
|
+
!should_disable_compositing_mode(
|
|
506
621
|
Some(value),
|
|
507
622
|
None,
|
|
623
|
+
Some("wayland-0"),
|
|
624
|
+
None,
|
|
508
625
|
&[None, None, None]
|
|
509
626
|
),
|
|
510
|
-
"expected {value} to
|
|
627
|
+
"expected {value} to restore compositing"
|
|
511
628
|
);
|
|
512
629
|
}
|
|
513
630
|
}
|
package/src-tauri/src/util.rs
CHANGED
|
@@ -48,6 +48,26 @@ pub fn get_data_dir(app: &AppHandle, package_name: String) -> std::io::Result<Pa
|
|
|
48
48
|
Ok(data_dir)
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
pub fn read_last_url(path: &Path) -> std::io::Result<Option<tauri::Url>> {
|
|
52
|
+
match std::fs::read_to_string(path) {
|
|
53
|
+
Ok(value) => Ok(tauri::Url::parse(&value)
|
|
54
|
+
.ok()
|
|
55
|
+
.filter(|url| matches!(url.scheme(), "http" | "https"))),
|
|
56
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
|
57
|
+
Err(error) => Err(error),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pub fn write_last_url(path: &Path, url: &tauri::Url) -> std::io::Result<()> {
|
|
62
|
+
if matches!(url.scheme(), "http" | "https") {
|
|
63
|
+
if let Some(parent) = path.parent() {
|
|
64
|
+
std::fs::create_dir_all(parent)?;
|
|
65
|
+
}
|
|
66
|
+
std::fs::write(path, url.as_str())?;
|
|
67
|
+
}
|
|
68
|
+
Ok(())
|
|
69
|
+
}
|
|
70
|
+
|
|
51
71
|
/// Both native and IPC downloads use the trusted, packaged configuration.
|
|
52
72
|
pub fn get_download_dir(app: &AppHandle) -> Result<PathBuf, String> {
|
|
53
73
|
let state = app
|
|
@@ -257,6 +277,25 @@ mod tests {
|
|
|
257
277
|
dir
|
|
258
278
|
}
|
|
259
279
|
|
|
280
|
+
#[test]
|
|
281
|
+
fn last_url_round_trip_preserves_full_address() {
|
|
282
|
+
let path = temp_path("state/last-url.txt");
|
|
283
|
+
assert_eq!(read_last_url(&path).unwrap(), None);
|
|
284
|
+
for address in [
|
|
285
|
+
"https://acme.example/projects/42?view=board#activity",
|
|
286
|
+
"https://login.example/callback?code=example#result",
|
|
287
|
+
] {
|
|
288
|
+
let url = tauri::Url::parse(address).unwrap();
|
|
289
|
+
write_last_url(&path, &url).unwrap();
|
|
290
|
+
assert_eq!(read_last_url(&path).unwrap(), Some(url));
|
|
291
|
+
}
|
|
292
|
+
write_last_url(&path, &tauri::Url::parse("about:blank").unwrap()).unwrap();
|
|
293
|
+
assert!(read_last_url(&path).unwrap().is_some());
|
|
294
|
+
std::fs::write(&path, "invalid URL").unwrap();
|
|
295
|
+
assert_eq!(read_last_url(&path).unwrap(), None);
|
|
296
|
+
fs::remove_dir_all(path.parent().unwrap().parent().unwrap()).unwrap();
|
|
297
|
+
}
|
|
298
|
+
|
|
260
299
|
#[test]
|
|
261
300
|
fn expand_download_dir_preserves_absolute_paths_without_home() {
|
|
262
301
|
let absolute = temp_path("My Downloads");
|