diff --git a/bin/defaults.ts b/bin/defaults.ts index e919f5da6..9f35fd351 100644 --- a/bin/defaults.ts +++ b/bin/defaults.ts @@ -49,6 +49,7 @@ export const DEFAULT_PAKE_OPTIONS: PakeCliOptions = { minWidth: 0, minHeight: 0, ignoreCertificateErrors: false, + refreshInterval: 0, newWindow: false, install: false, }; diff --git a/bin/helpers/cli-program.ts b/bin/helpers/cli-program.ts index ac1a1a63e..4a1c0d01d 100644 --- a/bin/helpers/cli-program.ts +++ b/bin/helpers/cli-program.ts @@ -221,6 +221,20 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with .argParser(validateNumberInput) .hideHelp(), ) + .addOption( + new Option( + '--refresh-interval ', + 'Auto-refresh page interval in seconds (0 disables)', + ) + .default(DEFAULT.refreshInterval) + .argParser((value) => { + const interval = parseInt(value); + if (isNaN(interval) || interval < 0) { + throw new Error('--refresh-interval must be a number >= 0'); + } + return interval; + }), + ) .addOption( new Option( '--ignore-certificate-errors', diff --git a/bin/helpers/merge.ts b/bin/helpers/merge.ts index 54712ed2c..1736dc497 100644 --- a/bin/helpers/merge.ts +++ b/bin/helpers/merge.ts @@ -78,6 +78,7 @@ export async function mergeConfig( minWidth, minHeight, ignoreCertificateErrors, + refreshInterval, newWindow, } = options; @@ -108,6 +109,7 @@ export async function mergeConfig( min_width: minWidth, min_height: minHeight, ignore_certificate_errors: ignoreCertificateErrors, + refresh_interval: refreshInterval, new_window: newWindow, }; Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions }); diff --git a/bin/types.ts b/bin/types.ts index b2cd86f38..8c1d70d8f 100644 --- a/bin/types.ts +++ b/bin/types.ts @@ -118,6 +118,9 @@ export interface PakeCliOptions { // Ignore certificate errors (for self-signed certs), default false ignoreCertificateErrors: boolean; + // Auto-refresh page interval in seconds, default 0 (disabled) + refreshInterval: number; + // Turn on rapid build mode (app only, no dmg/deb/msi), good for debugging iterativeBuild: boolean; @@ -163,6 +166,7 @@ export interface WindowConfig { min_width: number; min_height: number; ignore_certificate_errors: boolean; + refresh_interval: number; new_window: boolean; } diff --git a/dist/cli.js b/dist/cli.js index b2bf0fd2c..2a96737fd 100644 --- a/dist/cli.js +++ b/dist/cli.js @@ -484,7 +484,7 @@ async function mergeConfig(url, options, tauriConf) { await fsExtra.copy(sourcePath, destPath); } })); - const { width, height, fullscreen, maximize, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name = 'pake-app', resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, incognito, title, wasm, enableDragDrop, multiInstance, multiWindow, startToTray, forceInternalNavigation, internalUrlRegex, zoom, minWidth, minHeight, ignoreCertificateErrors, newWindow, } = options; + const { width, height, fullscreen, maximize, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name = 'pake-app', resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, incognito, title, wasm, enableDragDrop, multiInstance, multiWindow, startToTray, forceInternalNavigation, internalUrlRegex, zoom, minWidth, minHeight, ignoreCertificateErrors, refreshInterval, newWindow, } = options; const { platform } = process; const platformHideOnClose = hideOnClose ?? platform === 'darwin'; const tauriConfWindowOptions = { @@ -510,6 +510,7 @@ async function mergeConfig(url, options, tauriConf) { min_width: minWidth, min_height: minHeight, ignore_certificate_errors: ignoreCertificateErrors, + refresh_interval: refreshInterval, new_window: newWindow, }; Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions }); @@ -2073,6 +2074,7 @@ const DEFAULT_PAKE_OPTIONS = { minWidth: 0, minHeight: 0, ignoreCertificateErrors: false, + refreshInterval: 0, newWindow: false, install: false, }; @@ -2225,6 +2227,15 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with .default(DEFAULT_PAKE_OPTIONS.minHeight) .argParser(validateNumberInput) .hideHelp()) + .addOption(new Option('--refresh-interval ', 'Auto-refresh page interval in seconds (0 disables)') + .default(DEFAULT_PAKE_OPTIONS.refreshInterval) + .argParser((value) => { + const interval = parseInt(value); + if (isNaN(interval) || interval < 0) { + throw new Error('--refresh-interval must be a number >= 0'); + } + return interval; + })) .addOption(new Option('--ignore-certificate-errors', 'Ignore certificate errors (for self-signed certificates)') .default(DEFAULT_PAKE_OPTIONS.ignoreCertificateErrors) .hideHelp()) diff --git a/docs/cli-usage.md b/docs/cli-usage.md index 0360a75fe..3f24475aa 100644 --- a/docs/cli-usage.md +++ b/docs/cli-usage.md @@ -370,6 +370,19 @@ Set the window title bar text. macOS shows no title if not specified; Windows/Li --title "Google Translate" ``` +#### [refresh-interval] + +Auto-refresh the current page at a fixed interval in seconds. Default is `0` (disabled). + +When enabled, Pake defers the refresh while the page is hidden or while the user is actively focused in an input, textarea, select, or contenteditable field. + +```shell +--refresh-interval + +# Example: refresh every 5 minutes +pake https://news.ycombinator.com --name HackerNews --refresh-interval 300 +``` + #### [incognito] Launch the application in incognito/private browsing mode. Default is `false`. When enabled, the webview will run in private mode, which means it won't store cookies, local storage, or browsing history. This is useful for privacy-sensitive applications. diff --git a/src-tauri/pake.json b/src-tauri/pake.json index 1fd46d7a2..78e0009ff 100644 --- a/src-tauri/pake.json +++ b/src-tauri/pake.json @@ -20,6 +20,7 @@ "start_to_tray": false, "force_internal_navigation": false, "internal_url_regex": "", + "refresh_interval": 0, "new_window": false } ], diff --git a/src-tauri/src/app/config.rs b/src-tauri/src/app/config.rs index a40550f3e..9a927287f 100644 --- a/src-tauri/src/app/config.rs +++ b/src-tauri/src/app/config.rs @@ -34,6 +34,8 @@ pub struct WindowConfig { pub min_height: f64, #[serde(default)] pub ignore_certificate_errors: bool, + #[serde(default)] + pub refresh_interval: u32, } fn default_zoom() -> u32 { diff --git a/src-tauri/src/inject/event.js b/src-tauri/src/inject/event.js index b67a6f04b..81da21780 100644 --- a/src-tauri/src/inject/event.js +++ b/src-tauri/src/inject/event.js @@ -263,6 +263,8 @@ document.addEventListener("DOMContentLoaded", () => { const pakeConfig = window["pakeConfig"] || {}; const forceInternalNavigation = pakeConfig.force_internal_navigation === true; const internalUrlRegex = pakeConfig.internal_url_regex || ""; + const refreshIntervalSeconds = Number(pakeConfig.refresh_interval || 0); + let autoRefreshTimer = null; let internalUrlPattern = null; if (internalUrlRegex) { try { @@ -308,6 +310,60 @@ document.addEventListener("DOMContentLoaded", () => { }); } + function shouldDeferAutoRefresh() { + if (document.hidden) { + return true; + } + + const activeElement = document.activeElement; + if (!activeElement) { + return false; + } + + const tagName = activeElement.tagName; + return ( + activeElement.isContentEditable || + tagName === "INPUT" || + tagName === "TEXTAREA" || + tagName === "SELECT" + ); + } + + function scheduleAutoRefresh(delayMs = refreshIntervalSeconds * 1000) { + if (refreshIntervalSeconds <= 0) { + return; + } + + if (autoRefreshTimer) { + clearTimeout(autoRefreshTimer); + } + + autoRefreshTimer = window.setTimeout(() => { + if (shouldDeferAutoRefresh()) { + scheduleAutoRefresh(5000); + return; + } + + window.location.reload(); + }, delayMs); + } + + if (refreshIntervalSeconds > 0) { + scheduleAutoRefresh(); + + document.addEventListener("visibilitychange", () => { + if (!document.hidden) { + scheduleAutoRefresh(); + } + }); + + window.addEventListener("beforeunload", () => { + if (autoRefreshTimer) { + clearTimeout(autoRefreshTimer); + } + }); + } + document.addEventListener( "paste", (event) => { diff --git a/tests/unit/cli-options.test.ts b/tests/unit/cli-options.test.ts index 756d2a4a5..6ad345b8f 100644 --- a/tests/unit/cli-options.test.ts +++ b/tests/unit/cli-options.test.ts @@ -29,4 +29,13 @@ describe('CLI options', () => { expect(option?.defaultValue).toBe(false); expect(option?.hidden).toBe(false); }); + + it('registers --refresh-interval option', () => { + const option = program.options.find( + (item) => item.long === '--refresh-interval', + ); + + expect(option).toBeDefined(); + expect(option?.defaultValue).toBe(0); + }); });