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 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. Claude Code users can install the official skill with `/plugin marketplace add tw93/Pake` and `/plugin install pake@pake`.
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.16.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 Error('--hide-on-close must be true or false');
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 Error('--zoom must be an integer between 50 and 200');
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
- throw new PakeError(`Config field "${key}" must be a finite number (${bounds}).`, {
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: 'Run pake --help for the accepted options.',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pake-cli",
3
- "version": "3.16.3",
3
+ "version": "3.17.0",
4
4
  "description": "🤱🏻 Turn any webpage into a desktop app with one command. 🤱🏻 一键打包网页生成轻量桌面应用。",
5
5
  "engines": {
6
6
  "node": ">=20.9.0"
@@ -2564,7 +2564,7 @@ dependencies = [
2564
2564
 
2565
2565
  [[package]]
2566
2566
  name = "pake"
2567
- version = "3.16.3"
2567
+ version = "3.17.0"
2568
2568
  dependencies = [
2569
2569
  "block2",
2570
2570
  "dispatch",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "pake"
3
- version = "3.16.3"
3
+ version = "3.17.0"
4
4
  description = "🤱🏻 Turn any webpage into a desktop app with Rust."
5
5
  authors = ["Tw93"]
6
6
  license = "GPL-3.0-or-later"
@@ -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
- Url::parse(&window_config.url).ok()
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::*;
@@ -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, set_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 should_enable_linux_webkit_safe_mode_from_values(
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 !should_enable_linux_webkit_safe_mode_from_values(
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
- return;
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 linux_webkit_safe_mode_stays_on_by_default() {
467
- assert!(should_enable_linux_webkit_safe_mode_from_values(
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 linux_webkit_safe_mode_is_disabled_for_niri_socket() {
476
- assert!(!should_enable_linux_webkit_safe_mode_from_values(
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 linux_webkit_safe_mode_is_disabled_for_niri_desktop() {
485
- assert!(!should_enable_linux_webkit_safe_mode_from_values(
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 linux_webkit_safe_mode_can_be_forced_on_for_niri() {
494
- assert!(should_enable_linux_webkit_safe_mode_from_values(
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 linux_webkit_safe_mode_can_be_disabled_explicitly() {
617
+ fn compositing_mode_can_be_disabled_explicitly() {
503
618
  for value in ["0", "false", "off", "no", "native", "disabled"] {
504
619
  assert!(
505
- !should_enable_linux_webkit_safe_mode_from_values(
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 disable safe mode"
627
+ "expected {value} to restore compositing"
511
628
  );
512
629
  }
513
630
  }
@@ -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");
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "productName": "Weekly",
3
3
  "identifier": "com.pake.weekly",
4
- "version": "3.16.3",
4
+ "version": "3.17.0",
5
5
  "app": {
6
6
  "withGlobalTauri": true,
7
7
  "trayIcon": {