feat: system settings tab — launch at login and prevent-sleep toggles

AppSettings.system persisted through the existing settings IPC; the main
process applies real side effects at startup and on every save via
app.setLoginItemSettings and powerSaveBlocker.
This commit is contained in:
954801926@qq.com committed 2026-09-06 11:43:40 +08:00
1 parent db0eae5efd
commit 18506c926a
6 files changed
+116 -8

No files matched your search

+4
View File
@@ -2,6 +2,7 @@ import { app, BrowserWindow, shell } from 'electron'
import { join } from 'path' import { join } from 'path'
import { registerIpc } from './ipc' import { registerIpc } from './ipc'
import { killAllPtys } from './pty' import { killAllPtys } from './pty'
import { applyStartupSystemSettings, loadSettings } from './settingsStore'
function createWindow(): void { function createWindow(): void {
const win = new BrowserWindow({ const win = new BrowserWindow({
@@ -42,6 +43,9 @@ function createWindow(): void {
app.whenReady().then(() => { app.whenReady().then(() => {
registerIpc() registerIpc()
// OS-level effects (login item, sleep blocker) must apply even if the
// settings dialog is never opened this run.
applyStartupSystemSettings(loadSettings())
createWindow() createWindow()
app.on('activate', () => { app.on('activate', () => {
+54 -4
View File
@@ -1,4 +1,4 @@
import { app, ipcMain } from 'electron' import { app, ipcMain, powerSaveBlocker } from 'electron'
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs' import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs'
import { join } from 'path' import { join } from 'path'
import { Ipc } from '../shared/ipc' import { Ipc } from '../shared/ipc'
@@ -7,6 +7,7 @@ import {
DEFAULT_SETTINGS, DEFAULT_SETTINGS,
type AppSettings, type AppSettings,
type HighlightRule, type HighlightRule,
type SystemSettings,
type TerminalSettings type TerminalSettings
} from '../shared/settings' } from '../shared/settings'
import type { TerminalTheme } from '../shared/theme' import type { TerminalTheme } from '../shared/theme'
@@ -14,6 +15,8 @@ import { broadcast } from './broadcast'
const settingsPath = (): string => join(app.getPath('userData'), 'settings.json') 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)[]) const TERMINAL_KEYS = new Set(Object.keys(DEFAULT_SETTINGS.terminal) as (keyof TerminalSettings)[])
/** Light structural check for a persisted highlight rule. */ /** 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<string, unknown>
system = {
launchAtLogin: candidate.launchAtLogin === true,
preventSleep: candidate.preventSleep === true
}
}
}
const themes: TerminalTheme[] = Array.isArray(customThemes) ? (customThemes as TerminalTheme[]) : [] 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 { export function loadSettings(): AppSettings {
@@ -80,7 +127,8 @@ export function loadSettings(): AppSettings {
return { return {
terminal: { ...DEFAULT_SETTINGS.terminal }, terminal: { ...DEFAULT_SETTINGS.terminal },
customThemes: [...DEFAULT_SETTINGS.customThemes], 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({ const merged = deepMerge({
terminal: next.terminal, terminal: next.terminal,
customThemes: next.customThemes, customThemes: next.customThemes,
highlightRules: next.highlightRules highlightRules: next.highlightRules,
system: next.system ?? DEFAULT_SYSTEM
}).settings }).settings
applySystemSettings(merged.system)
const pretty = JSON.stringify(merged, null, 2) const pretty = JSON.stringify(merged, null, 2)
mkdirSync(app.getPath('userData'), { recursive: true }) mkdirSync(app.getPath('userData'), { recursive: true })
+3 -2
View File
@@ -3,7 +3,7 @@ import { Button, Modal, Tabs } from 'antd'
import type { TabsProps } from 'antd' import type { TabsProps } from 'antd'
import { DEFAULT_SETTINGS } from '@shared/settings' import { DEFAULT_SETTINGS } from '@shared/settings'
import { useSettingsStore } from './store' import { useSettingsStore } from './store'
import { CursorSettingsTab, FontSettingsTab, RenderSettingsTab } from './SettingsTabs' import { CursorSettingsTab, FontSettingsTab, RenderSettingsTab, SystemSettingsTab } from './SettingsTabs'
import { ThemeSettingsTab } from './ThemeSettingsTab' import { ThemeSettingsTab } from './ThemeSettingsTab'
import { HighlightTab } from './HighlightTab' import { HighlightTab } from './HighlightTab'
import { ThemeEditor } from '../theme/editor/ThemeEditor' import { ThemeEditor } from '../theme/editor/ThemeEditor'
@@ -34,7 +34,8 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps): React.JS
onCreateTheme={() => setEditorState({ open: true, themeId: undefined })} onCreateTheme={() => setEditorState({ open: true, themeId: undefined })}
/> />
) )
} },
{ key: 'system', label: '系统', children: <SystemSettingsTab /> }
] ]
const closeEditor = (): void => setEditorState((prev) => (prev.open ? { ...prev, open: false } : prev)) const closeEditor = (): void => setEditorState((prev) => (prev.open ? { ...prev, open: false } : prev))
@@ -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 (
<div className="settings-pane">
<SettingRow
label="开机时自启动"
desc="开机时自动启动软件(开发模式下注册的是开发版程序)"
control={
<Switch
checked={settings.system.launchAtLogin}
onChange={(checked) => void updateSystem({ launchAtLogin: checked })}
/>
}
/>
<SettingRow
label="阻止系统休眠"
desc="开启后 OpenTerminal 将阻止系统自动休眠;关闭则允许系统按系统设置休眠"
control={
<Switch
checked={settings.system.preventSleep}
onChange={(checked) => void updateSystem({ preventSleep: checked })}
/>
}
/>
</div>
)
}
/** /**
* Live preview of the current font settings, rendered as an `ls -la`-style * Live preview of the current font settings, rendered as an `ls -la`-style
* terminal screen with ANSI-colored tokens on the active theme background. * terminal screen with ANSI-colored tokens on the active theme background.
+16 -1
View File
@@ -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 { DEFAULT_SETTINGS } from '@shared/settings'
import type { TerminalTheme } from '@shared/theme' import type { TerminalTheme } from '@shared/theme'
import { getThemeById } from '@shared/theme' import { getThemeById } from '@shared/theme'
@@ -15,6 +15,8 @@ export interface SettingsState {
setCustomThemes: (themes: AppSettings['customThemes']) => Promise<void> setCustomThemes: (themes: AppSettings['customThemes']) => Promise<void>
/** replace highlightRules array and persist */ /** replace highlightRules array and persist */
setHighlightRules: (rules: AppSettings['highlightRules']) => Promise<void> setHighlightRules: (rules: AppSettings['highlightRules']) => Promise<void>
/** shallow-merge into settings.system and persist */
updateSystem: (partial: Partial<SystemSettings>) => Promise<void>
} }
/** unsubscriber for the cross-window SETTINGS_CHANGED listener; held module-level so hydrate() is idempotent */ /** unsubscriber for the cross-window SETTINGS_CHANGED listener; held module-level so hydrate() is idempotent */
@@ -86,6 +88,19 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
console.error('[settings] setHighlightRules failed, rolling back', err) console.error('[settings] setHighlightRules failed, rolling back', err)
set({ settings: prev }) 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 })
}
} }
})) }))
+10 -1
View File
@@ -36,12 +36,20 @@ export interface TerminalSettings {
pasteRiskConfirm: boolean 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 { export interface AppSettings {
terminal: TerminalSettings terminal: TerminalSettings
/** user-created themes; builtin themes are immutable and live in shared/theme.ts */ /** user-created themes; builtin themes are immutable and live in shared/theme.ts */
customThemes: TerminalTheme[] customThemes: TerminalTheme[]
/** terminal keyword highlight rules, applied at write time in priority order */ /** terminal keyword highlight rules, applied at write time in priority order */
highlightRules: HighlightRule[] highlightRules: HighlightRule[]
system: SystemSettings
} }
const RULE = ( const RULE = (
@@ -86,5 +94,6 @@ export const DEFAULT_SETTINGS: AppSettings = {
pasteRiskConfirm: true pasteRiskConfirm: true
}, },
customThemes: [], customThemes: [],
highlightRules: DEFAULT_HIGHLIGHT_RULES highlightRules: DEFAULT_HIGHLIGHT_RULES,
system: { launchAtLogin: false, preventSleep: false }
} }