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:
1 parent
db0eae5efd
commit
18506c926a
6 files changed
+116
-8
No files matched your search
@@ -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', () => {
|
||||||
|
|||||||
@@ -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,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.
|
||||||
|
|||||||
@@ -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
@@ -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 }
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user