diff --git a/src/main/index.ts b/src/main/index.ts index 89048a5..857aec7 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -2,6 +2,7 @@ import { app, BrowserWindow, shell } from 'electron' import { join } from 'path' import { registerIpc } from './ipc' import { killAllPtys } from './pty' +import { applyStartupSystemSettings, loadSettings } from './settingsStore' function createWindow(): void { const win = new BrowserWindow({ @@ -42,6 +43,9 @@ function createWindow(): void { app.whenReady().then(() => { registerIpc() + // OS-level effects (login item, sleep blocker) must apply even if the + // settings dialog is never opened this run. + applyStartupSystemSettings(loadSettings()) createWindow() app.on('activate', () => { diff --git a/src/main/settingsStore.ts b/src/main/settingsStore.ts index afa05e6..13bde89 100644 --- a/src/main/settingsStore.ts +++ b/src/main/settingsStore.ts @@ -1,4 +1,4 @@ -import { app, ipcMain } from 'electron' +import { app, ipcMain, powerSaveBlocker } from 'electron' import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs' import { join } from 'path' import { Ipc } from '../shared/ipc' @@ -7,6 +7,7 @@ import { DEFAULT_SETTINGS, type AppSettings, type HighlightRule, + type SystemSettings, type TerminalSettings } from '../shared/settings' import type { TerminalTheme } from '../shared/theme' @@ -14,6 +15,8 @@ import { broadcast } from './broadcast' const settingsPath = (): string => join(app.getPath('userData'), 'settings.json') +const DEFAULT_SYSTEM: SystemSettings = { launchAtLogin: false, preventSleep: false } + const TERMINAL_KEYS = new Set(Object.keys(DEFAULT_SETTINGS.terminal) as (keyof TerminalSettings)[]) /** Light structural check for a persisted highlight rule. */ @@ -66,9 +69,53 @@ function deepMerge(raw: unknown): { settings: AppSettings; errors: string[] } { } } + let system: unknown = DEFAULT_SYSTEM + if (raw !== null && typeof raw === 'object') { + const sys = (raw as { system?: unknown }).system + if (sys !== null && typeof sys === 'object') { + const candidate = sys as Record + system = { + launchAtLogin: candidate.launchAtLogin === true, + preventSleep: candidate.preventSleep === true + } + } + } + const themes: TerminalTheme[] = Array.isArray(customThemes) ? (customThemes as TerminalTheme[]) : [] - return { settings: { terminal, customThemes: themes, highlightRules: sanitizeRules(highlightRules) }, errors } + return { + settings: { + terminal, + customThemes: themes, + highlightRules: sanitizeRules(highlightRules), + system: system as SystemSettings + }, + errors + } +} + +// ---- real system side effects ------------------------------------------------ + +let sleepBlockerId: number | undefined + +/** Apply OS-level effects of the system settings (login item, sleep blocker). */ +function applySystemSettings(system: SystemSettings): void { + try { + app.setLoginItemSettings({ openAtLogin: system.launchAtLogin }) + } catch (err) { + console.error('[settings] setLoginItemSettings failed', err) + } + if (system.preventSleep && sleepBlockerId === undefined) { + sleepBlockerId = powerSaveBlocker.start('prevent-app-suspension') + } else if (!system.preventSleep && sleepBlockerId !== undefined) { + powerSaveBlocker.stop(sleepBlockerId) + sleepBlockerId = undefined + } +} + +/** Apply system side effects for the settings loaded at startup. */ +export function applyStartupSystemSettings(settings: AppSettings): void { + applySystemSettings(settings.system) } export function loadSettings(): AppSettings { @@ -80,7 +127,8 @@ export function loadSettings(): AppSettings { return { terminal: { ...DEFAULT_SETTINGS.terminal }, customThemes: [...DEFAULT_SETTINGS.customThemes], - highlightRules: DEFAULT_HIGHLIGHT_RULES.map((rule) => ({ ...rule })) + highlightRules: DEFAULT_HIGHLIGHT_RULES.map((rule) => ({ ...rule })), + system: { ...DEFAULT_SYSTEM } } } } @@ -89,8 +137,10 @@ export function saveSettings(next: AppSettings): AppSettings { const merged = deepMerge({ terminal: next.terminal, customThemes: next.customThemes, - highlightRules: next.highlightRules + highlightRules: next.highlightRules, + system: next.system ?? DEFAULT_SYSTEM }).settings + applySystemSettings(merged.system) const pretty = JSON.stringify(merged, null, 2) mkdirSync(app.getPath('userData'), { recursive: true }) diff --git a/src/renderer/src/settings/SettingsDialog.tsx b/src/renderer/src/settings/SettingsDialog.tsx index 84a77b7..0266d47 100644 --- a/src/renderer/src/settings/SettingsDialog.tsx +++ b/src/renderer/src/settings/SettingsDialog.tsx @@ -3,7 +3,7 @@ import { Button, Modal, Tabs } from 'antd' import type { TabsProps } from 'antd' import { DEFAULT_SETTINGS } from '@shared/settings' import { useSettingsStore } from './store' -import { CursorSettingsTab, FontSettingsTab, RenderSettingsTab } from './SettingsTabs' +import { CursorSettingsTab, FontSettingsTab, RenderSettingsTab, SystemSettingsTab } from './SettingsTabs' import { ThemeSettingsTab } from './ThemeSettingsTab' import { HighlightTab } from './HighlightTab' import { ThemeEditor } from '../theme/editor/ThemeEditor' @@ -34,7 +34,8 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps): React.JS onCreateTheme={() => setEditorState({ open: true, themeId: undefined })} /> ) - } + }, + { key: 'system', label: '系统', children: } ] const closeEditor = (): void => setEditorState((prev) => (prev.open ? { ...prev, open: false } : prev)) diff --git a/src/renderer/src/settings/SettingsTabs.tsx b/src/renderer/src/settings/SettingsTabs.tsx index d266fa7..ce79a76 100644 --- a/src/renderer/src/settings/SettingsTabs.tsx +++ b/src/renderer/src/settings/SettingsTabs.tsx @@ -282,6 +282,35 @@ export function RenderSettingsTab(): React.JSX.Element { ) } +export function SystemSettingsTab(): React.JSX.Element { + const settings = useSettingsStore((s) => s.settings) + const updateSystem = useSettingsStore((s) => s.updateSystem) + return ( +
+ void updateSystem({ launchAtLogin: checked })} + /> + } + /> + void updateSystem({ preventSleep: checked })} + /> + } + /> +
+ ) +} + /** * Live preview of the current font settings, rendered as an `ls -la`-style * terminal screen with ANSI-colored tokens on the active theme background. diff --git a/src/renderer/src/settings/store.ts b/src/renderer/src/settings/store.ts index e3f9d4d..2ee13f2 100644 --- a/src/renderer/src/settings/store.ts +++ b/src/renderer/src/settings/store.ts @@ -1,4 +1,4 @@ -import type { AppSettings, TerminalSettings } from '@shared/settings' +import type { AppSettings, SystemSettings, TerminalSettings } from '@shared/settings' import { DEFAULT_SETTINGS } from '@shared/settings' import type { TerminalTheme } from '@shared/theme' import { getThemeById } from '@shared/theme' @@ -15,6 +15,8 @@ export interface SettingsState { setCustomThemes: (themes: AppSettings['customThemes']) => Promise /** replace highlightRules array and persist */ setHighlightRules: (rules: AppSettings['highlightRules']) => Promise + /** shallow-merge into settings.system and persist */ + updateSystem: (partial: Partial) => Promise } /** unsubscriber for the cross-window SETTINGS_CHANGED listener; held module-level so hydrate() is idempotent */ @@ -86,6 +88,19 @@ export const useSettingsStore = create((set, get) => ({ console.error('[settings] setHighlightRules failed, rolling back', err) set({ settings: prev }) } + }, + + updateSystem: async (partial) => { + const prev = get().settings + const next: AppSettings = { ...prev, system: { ...prev.system, ...partial } } + // optimistic update; the SETTINGS_CHANGED echo will re-set the same value (idempotent) + set({ settings: next }) + try { + await window.api.saveSettings(next) + } catch (err) { + console.error('[settings] updateSystem failed, rolling back', err) + set({ settings: prev }) + } } })) diff --git a/src/shared/settings.ts b/src/shared/settings.ts index c93b09e..1728945 100644 --- a/src/shared/settings.ts +++ b/src/shared/settings.ts @@ -36,12 +36,20 @@ export interface TerminalSettings { pasteRiskConfirm: boolean } +export interface SystemSettings { + /** register the app to launch at OS login */ + launchAtLogin: boolean + /** hold a powerSaveBlocker so the OS never auto-suspends while running */ + preventSleep: boolean +} + export interface AppSettings { terminal: TerminalSettings /** user-created themes; builtin themes are immutable and live in shared/theme.ts */ customThemes: TerminalTheme[] /** terminal keyword highlight rules, applied at write time in priority order */ highlightRules: HighlightRule[] + system: SystemSettings } const RULE = ( @@ -86,5 +94,6 @@ export const DEFAULT_SETTINGS: AppSettings = { pasteRiskConfirm: true }, customThemes: [], - highlightRules: DEFAULT_HIGHLIGHT_RULES + highlightRules: DEFAULT_HIGHLIGHT_RULES, + system: { launchAtLogin: false, preventSleep: false } }