From a4e4c6186adbeeeeebe1d4e34da5929812597b1d Mon Sep 17 00:00:00 2001 From: Memory <134070804+Memory2314@users.noreply.github.com> Date: Fri, 19 Jun 2026 23:52:54 +0800 Subject: [PATCH] feat: add lightweight mode behavior toggle --- src/main/core/manager.ts | 53 +++++++++------- src/main/lifecycle.ts | 4 ++ src/main/utils/template.ts | 3 + src/main/window.ts | 62 ++++++++++++++++--- .../components/settings/general-config.tsx | 61 ++++++++++++------ src/renderer/src/locales/en-US.json | 3 + src/renderer/src/locales/fa-IR.json | 3 + src/renderer/src/locales/ru-RU.json | 3 + src/renderer/src/locales/zh-CN.json | 3 + src/renderer/src/locales/zh-TW.json | 3 + src/shared/types.d.ts | 1 + 11 files changed, 148 insertions(+), 51 deletions(-) diff --git a/src/main/core/manager.ts b/src/main/core/manager.ts index a4975987..3e964dbf 100644 --- a/src/main/core/manager.ts +++ b/src/main/core/manager.ts @@ -90,6 +90,31 @@ function hasCoreProcess(): boolean { return Boolean(child && !child.killed && child.exitCode === null && child.signalCode === null) } +async function stopPidFileCore(): Promise { + const pidPath = path.join(dataDir(), 'core.pid') + if (!existsSync(pidPath)) return + + const pidString = await readFile(pidPath, 'utf-8').catch(() => '') + const pid = parseInt(pidString.trim()) + if (!isNaN(pid)) { + try { + process.kill(pid, 0) + process.kill(pid, 'SIGINT') + await new Promise((resolve) => setTimeout(resolve, 1000)) + try { + process.kill(pid, 0) + process.kill(pid, 'SIGKILL') + } catch { + // ignore + } + } catch { + // ignore + } + } + + await rm(pidPath).catch(() => {}) +} + // 初始化核心文件监听 export function initCoreWatcher(): void { if (coreWatcher) return @@ -183,18 +208,8 @@ async function prepareCore(detached: boolean, skipStop = false): Promise { managerLogger.warn('Failed to refresh axios instance:', error) } + await stopPidFileCore() await cleanupSocketFile() } @@ -477,18 +493,7 @@ export async function keepCoreAlive(): Promise { // 退出但保持核心运行 export async function quitWithoutCore(): Promise { managerLogger.info(`Starting lightweight mode on platform: ${process.platform}`) - - try { - await startCore(true) - if (child?.pid) { - await writeFile(path.join(dataDir(), 'core.pid'), child.pid.toString()) - managerLogger.info(`Core started in lightweight mode with PID: ${child.pid}`) - } - } catch (e) { - managerLogger.error('Failed to start core in lightweight mode:', e) - safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`) - } - + await keepCoreAlive() await startMonitor(true) managerLogger.info('Exiting main process, core will continue running in background') app.exit() diff --git a/src/main/lifecycle.ts b/src/main/lifecycle.ts index 2039cd2b..9a3bc6bc 100644 --- a/src/main/lifecycle.ts +++ b/src/main/lifecycle.ts @@ -116,6 +116,10 @@ export function setupAppLifecycle(): void { ) } + app.on('window-all-closed', () => { + // Keep the app and tray alive when lightweight tray mode destroys the renderer window. + }) + app.on('before-quit', async (e) => { e.preventDefault() await cleanupBeforeExit() diff --git a/src/main/utils/template.ts b/src/main/utils/template.ts index 79d2be85..52339e0b 100644 --- a/src/main/utils/template.ts +++ b/src/main/utils/template.ts @@ -39,6 +39,9 @@ export const defaultConfig: IAppConfig = { connectionDirection: 'asc', connectionOrderBy: 'time', useSubStore: DEFAULT_USE_SUB_STORE, + autoQuitWithoutCore: false, + autoQuitWithoutCoreDelay: 60, + autoQuitWithoutCoreMode: 'core', proxyDisplayMode: 'simple', proxyDisplayOrder: 'default', autoCheckUpdate: true, diff --git a/src/main/window.ts b/src/main/window.ts index 3ac8d383..63aeafbe 100644 --- a/src/main/window.ts +++ b/src/main/window.ts @@ -52,13 +52,26 @@ function ensureVisibleOnScreen(state: WindowState): WindowState { export let mainWindow: BrowserWindow | null = null let quitTimeout: NodeJS.Timeout | null = null +let createWindowPromise: Promise | null = null +type AutoQuitWithoutCoreMode = NonNullable export async function createWindow(): Promise { + if (mainWindow && !mainWindow.isDestroyed()) return + if (createWindowPromise) return createWindowPromise + + createWindowPromise = createWindowInternal().finally(() => { + createWindowPromise = null + }) + return createWindowPromise +} + +async function createWindowInternal(): Promise { const { useWindowFrame = false, silentStart = false, autoQuitWithoutCore = false, - autoQuitWithoutCoreDelay = 60 + autoQuitWithoutCoreDelay = 60, + autoQuitWithoutCoreMode = 'core' } = await getAppConfig() const savedState = ensureVisibleOnScreen(loadWindowState()) @@ -97,7 +110,8 @@ export async function createWindow(): Promise { setupWindowEvents(mainWindow, { silentStart, autoQuitWithoutCore, - autoQuitWithoutCoreDelay + autoQuitWithoutCoreDelay, + autoQuitWithoutCoreMode }) if (is.dev) { @@ -115,14 +129,16 @@ interface WindowConfig { silentStart: boolean autoQuitWithoutCore: boolean autoQuitWithoutCoreDelay: number + autoQuitWithoutCoreMode: AutoQuitWithoutCoreMode } function setupWindowEvents(window: BrowserWindow, config: WindowConfig): void { - const { silentStart, autoQuitWithoutCore, autoQuitWithoutCoreDelay } = config + const { silentStart, autoQuitWithoutCore, autoQuitWithoutCoreDelay, autoQuitWithoutCoreMode } = + config window.on('ready-to-show', () => { if (autoQuitWithoutCore && !window.isVisible()) { - scheduleQuitWithoutCore(autoQuitWithoutCoreDelay) + scheduleQuitWithoutCore(autoQuitWithoutCoreDelay, autoQuitWithoutCoreMode) } // 开发模式下始终显示窗口 @@ -148,6 +164,7 @@ function setupWindowEvents(window: BrowserWindow, config: WindowConfig): void { const { autoQuitWithoutCore = false, autoQuitWithoutCoreDelay = 60, + autoQuitWithoutCoreMode = 'core', useDockIcon = true } = await getAppConfig() @@ -156,7 +173,13 @@ function setupWindowEvents(window: BrowserWindow, config: WindowConfig): void { } if (autoQuitWithoutCore) { - scheduleQuitWithoutCore(autoQuitWithoutCoreDelay) + scheduleQuitWithoutCore(autoQuitWithoutCoreDelay, autoQuitWithoutCoreMode) + } + }) + + window.on('closed', () => { + if (mainWindow === window) { + mainWindow = null } }) @@ -176,9 +199,20 @@ function setupWindowEvents(window: BrowserWindow, config: WindowConfig): void { }) } -function scheduleQuitWithoutCore(delaySeconds: number): void { +function scheduleQuitWithoutCore( + delaySeconds: number, + mode: AutoQuitWithoutCoreMode = 'core' +): void { clearQuitTimeout() quitTimeout = setTimeout(async () => { + if (mode === 'tray') { + if (mainWindow && !mainWindow.isVisible()) { + mainWindow.destroy() + hideDockIcon() + } + return + } + await quitWithoutCore() }, delaySeconds * 1000) } @@ -191,7 +225,10 @@ export function clearQuitTimeout(): void { } export function triggerMainWindow(force?: boolean): void { - if (!mainWindow) return + if (!mainWindow || mainWindow.isDestroyed()) { + showMainWindow() + return + } getAppConfig() .then(({ triggerMainWindowBehavior = 'toggle' }) => { @@ -209,11 +246,20 @@ export function triggerMainWindow(force?: boolean): void { } export function showMainWindow(): void { - if (mainWindow) { + clearQuitTimeout() + + if (mainWindow && !mainWindow.isDestroyed()) { clearQuitTimeout() mainWindow.show() mainWindow.focusOnWebView() + return } + + void createWindow().then(() => { + clearQuitTimeout() + mainWindow?.show() + mainWindow?.focusOnWebView() + }) } export function closeMainWindow(): void { diff --git a/src/renderer/src/components/settings/general-config.tsx b/src/renderer/src/components/settings/general-config.tsx index 98bc7be4..ac32ba41 100644 --- a/src/renderer/src/components/settings/general-config.tsx +++ b/src/renderer/src/components/settings/general-config.tsx @@ -69,6 +69,7 @@ const GeneralConfig: React.FC = () => { useWindowFrame = false, autoQuitWithoutCore = false, autoQuitWithoutCoreDelay = 60, + autoQuitWithoutCoreMode = 'core', customTheme = 'default.css', envType = [platform === 'win32' ? 'powershell' : 'bash'], autoCheckUpdate, @@ -237,27 +238,49 @@ const GeneralConfig: React.FC = () => { /> {autoQuitWithoutCore && ( - -
- + + { - const num = parseInt(v) - await patchAppConfig({ autoQuitWithoutCoreDelay: num }) + color="primary" + selectedKey={autoQuitWithoutCoreMode} + onSelectionChange={async (key) => { + const mode = key as 'core' | 'tray' + await patchAppConfig({ autoQuitWithoutCoreMode: mode }) + if (mode === 'core' && autoQuitWithoutCoreDelay < 5) { + await patchAppConfig({ autoQuitWithoutCoreDelay: 5 }) + } }} - onBlur={async (e) => { - let num = parseInt(e.target.value) - if (isNaN(num)) num = 5 - if (num < 5) num = 5 - await patchAppConfig({ autoQuitWithoutCoreDelay: num }) - }} - /> - {t('common.seconds')} -
-
+ > + + + + + +
+ { + const num = parseInt(v) + if (!isNaN(num)) { + await patchAppConfig({ autoQuitWithoutCoreDelay: num }) + } + }} + onBlur={async (e) => { + const minDelay = autoQuitWithoutCoreMode === 'core' ? 5 : 0 + let num = parseInt(e.target.value) + if (isNaN(num)) num = minDelay + if (num < minDelay) num = minDelay + await patchAppConfig({ autoQuitWithoutCoreDelay: num }) + }} + /> + {t('common.seconds')} +
+
+ )}