Add app icon switcher to appearance settings (#4600)
This commit is contained in:
parent
eb96ef94de
commit
6b87b9a32c
Binary file not shown.
|
After Width: | Height: | Size: 353 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
|
|
@ -0,0 +1,79 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
browserWindowGetAllWindowsMock,
|
||||
createFromPathMock,
|
||||
dockSetIconMock,
|
||||
isMock,
|
||||
windowSetIconMock
|
||||
} = vi.hoisted(() => ({
|
||||
browserWindowGetAllWindowsMock: vi.fn(),
|
||||
createFromPathMock: vi.fn(),
|
||||
dockSetIconMock: vi.fn(),
|
||||
isMock: { dev: false },
|
||||
windowSetIconMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: { dock: { setIcon: dockSetIconMock } },
|
||||
BrowserWindow: { getAllWindows: browserWindowGetAllWindowsMock },
|
||||
nativeImage: { createFromPath: createFromPathMock }
|
||||
}))
|
||||
|
||||
vi.mock('@electron-toolkit/utils', () => ({
|
||||
is: isMock
|
||||
}))
|
||||
|
||||
vi.mock('../../resources/icon.png?asset', () => ({
|
||||
default: 'classic-icon'
|
||||
}))
|
||||
|
||||
vi.mock('../../resources/icon-dev.png?asset', () => ({
|
||||
default: 'classic-dev-icon'
|
||||
}))
|
||||
|
||||
vi.mock('../../resources/app-icons/orca-watercolor.png?asset', () => ({
|
||||
default: 'watercolor-icon'
|
||||
}))
|
||||
|
||||
vi.mock('../../resources/app-icons/orca-blue.png?asset', () => ({
|
||||
default: 'blue-icon'
|
||||
}))
|
||||
|
||||
import { applyAppIcon, getAppIconPath } from './app-icon'
|
||||
|
||||
describe('app icon selection', () => {
|
||||
beforeEach(() => {
|
||||
browserWindowGetAllWindowsMock.mockReset()
|
||||
createFromPathMock.mockReset()
|
||||
dockSetIconMock.mockReset()
|
||||
windowSetIconMock.mockReset()
|
||||
isMock.dev = false
|
||||
})
|
||||
|
||||
it('resolves classic, watercolor, blue, and invalid icon ids', () => {
|
||||
expect(getAppIconPath('classic')).toBe('classic-icon')
|
||||
expect(getAppIconPath('watercolor')).toBe('watercolor-icon')
|
||||
expect(getAppIconPath('blue')).toBe('blue-icon')
|
||||
expect(getAppIconPath('missing')).toBe('classic-icon')
|
||||
})
|
||||
|
||||
it('applies the selected icon to the dock and live windows', () => {
|
||||
const image = { isEmpty: () => false }
|
||||
createFromPathMock.mockReturnValue(image)
|
||||
browserWindowGetAllWindowsMock.mockReturnValue([
|
||||
{ isDestroyed: () => false, setIcon: windowSetIconMock },
|
||||
{ isDestroyed: () => true, setIcon: vi.fn() }
|
||||
])
|
||||
|
||||
applyAppIcon('watercolor')
|
||||
|
||||
expect(createFromPathMock).toHaveBeenCalledWith('watercolor-icon')
|
||||
if (process.platform === 'darwin') {
|
||||
expect(dockSetIconMock).toHaveBeenCalledWith(image)
|
||||
} else {
|
||||
expect(dockSetIconMock).not.toHaveBeenCalled()
|
||||
}
|
||||
expect(windowSetIconMock).toHaveBeenCalledWith(image)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { app, BrowserWindow, nativeImage } from 'electron'
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import classicIcon from '../../resources/icon.png?asset'
|
||||
import classicDevIcon from '../../resources/icon-dev.png?asset'
|
||||
import watercolorIcon from '../../resources/app-icons/orca-watercolor.png?asset'
|
||||
import blueIcon from '../../resources/app-icons/orca-blue.png?asset'
|
||||
import { normalizeAppIconId, type AppIconId } from '../shared/app-icon'
|
||||
|
||||
const APP_ICON_PATHS = {
|
||||
classic: is.dev ? classicDevIcon : classicIcon,
|
||||
watercolor: watercolorIcon,
|
||||
blue: blueIcon
|
||||
} satisfies Record<AppIconId, string>
|
||||
|
||||
export function getAppIconPath(value: unknown): string {
|
||||
return APP_ICON_PATHS[normalizeAppIconId(value)]
|
||||
}
|
||||
|
||||
export function createAppIconImage(value: unknown): Electron.NativeImage {
|
||||
return nativeImage.createFromPath(getAppIconPath(value))
|
||||
}
|
||||
|
||||
export function applyAppIcon(value: unknown): void {
|
||||
const image = createAppIconImage(value)
|
||||
if (image.isEmpty()) {
|
||||
return
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
app.dock?.setIcon(image)
|
||||
}
|
||||
for (const window of BrowserWindow.getAllWindows()) {
|
||||
if (!window.isDestroyed()) {
|
||||
window.setIcon(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +49,7 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
|
|||
branchPrefix: 'git-username',
|
||||
branchPrefixCustom: '',
|
||||
theme: 'system',
|
||||
appIcon: overrides.appIcon ?? 'classic',
|
||||
editorAutoSave: false,
|
||||
editorAutoSaveDelayMs: 1000,
|
||||
editorMinimapEnabled: false,
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
|
|||
branchPrefix: 'git-username',
|
||||
branchPrefixCustom: '',
|
||||
theme: 'system',
|
||||
appIcon: overrides.appIcon ?? 'classic',
|
||||
editorAutoSave: false,
|
||||
editorAutoSaveDelayMs: 1000,
|
||||
editorMinimapEnabled: false,
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ import { grantDirAcl } from './win32-utils'
|
|||
import { existsSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import os from 'node:os'
|
||||
import { app, BrowserWindow, nativeImage, nativeTheme } from 'electron'
|
||||
import { app, BrowserWindow, nativeTheme } from 'electron'
|
||||
import { electronApp, is } from '@electron-toolkit/utils'
|
||||
import * as QRCode from 'qrcode'
|
||||
import devIcon from '../../resources/icon-dev.png?asset'
|
||||
import { Store, initDataPath } from './persistence'
|
||||
import { applyAppIcon } from './app-icon'
|
||||
import { StatsCollector, initStatsPath } from './stats/collector'
|
||||
import { ClaudeUsageStore, initClaudeUsagePath } from './claude-usage/store'
|
||||
import { CodexUsageStore, initCodexUsagePath } from './codex-usage/store'
|
||||
|
|
@ -1052,12 +1052,8 @@ app.whenReady().then(async () => {
|
|||
electronApp.setAppUserModelId(devInstanceIdentity.appUserModelId)
|
||||
app.setName(devInstanceIdentity.name)
|
||||
|
||||
if (process.platform === 'darwin' && is.dev) {
|
||||
const dockIcon = nativeImage.createFromPath(devIcon)
|
||||
app.dock?.setIcon(dockIcon)
|
||||
}
|
||||
|
||||
store = new Store()
|
||||
applyAppIcon(store.getSettings().appIcon)
|
||||
if (shouldSuppressDevEducation({ isDev: is.dev })) {
|
||||
suppressDevEducationForStore(store)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
||||
|
||||
const {
|
||||
applyAppIconMock,
|
||||
applyElectronProxySettingsMock,
|
||||
browserWindowGetAllWindowsMock,
|
||||
handleMock,
|
||||
previewGhosttyImportMock
|
||||
} = vi.hoisted(() => ({
|
||||
applyAppIconMock: vi.fn(),
|
||||
applyElectronProxySettingsMock: vi.fn(),
|
||||
browserWindowGetAllWindowsMock: vi.fn(),
|
||||
handleMock: vi.fn(),
|
||||
|
|
@ -26,6 +28,10 @@ vi.mock('../network/proxy-settings', () => ({
|
|||
applyElectronProxySettings: applyElectronProxySettingsMock
|
||||
}))
|
||||
|
||||
vi.mock('../app-icon', () => ({
|
||||
applyAppIcon: applyAppIconMock
|
||||
}))
|
||||
|
||||
import { registerSettingsHandlers } from './settings'
|
||||
|
||||
const settingsInvokeEvent = { sender: { id: 1 } }
|
||||
|
|
@ -46,6 +52,7 @@ const store = {
|
|||
describe('registerSettingsHandlers', () => {
|
||||
beforeEach(() => {
|
||||
handleMock.mockClear()
|
||||
applyAppIconMock.mockClear()
|
||||
applyElectronProxySettingsMock.mockClear()
|
||||
applyElectronProxySettingsMock.mockResolvedValue({ source: 'settings' })
|
||||
previewGhosttyImportMock.mockClear()
|
||||
|
|
@ -216,4 +223,42 @@ describe('registerSettingsHandlers', () => {
|
|||
)
|
||||
expect(applyElectronProxySettingsMock).toHaveBeenCalledWith({ httpProxyUrl: '' })
|
||||
})
|
||||
|
||||
it('normalizes and applies app icon changes from renderer settings IPC', async () => {
|
||||
store.getSettings.mockReturnValue({ appIcon: 'classic' })
|
||||
store.updateSettings.mockReturnValue({ appIcon: 'watercolor' })
|
||||
registerSettingsHandlers(store as never)
|
||||
|
||||
const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as (
|
||||
_event: unknown,
|
||||
args: unknown
|
||||
) => Promise<unknown>
|
||||
|
||||
await handler(settingsInvokeEvent, { appIcon: 'watercolor' })
|
||||
|
||||
expect(store.updateSettings).toHaveBeenCalledWith(
|
||||
{ appIcon: 'watercolor' },
|
||||
{ notifyListeners: true, originWebContentsId: 1 }
|
||||
)
|
||||
expect(applyAppIconMock).toHaveBeenCalledWith('watercolor')
|
||||
})
|
||||
|
||||
it('falls back to the classic app icon for invalid renderer settings IPC values', async () => {
|
||||
store.getSettings.mockReturnValue({ appIcon: 'watercolor' })
|
||||
store.updateSettings.mockReturnValue({ appIcon: 'classic' })
|
||||
registerSettingsHandlers(store as never)
|
||||
|
||||
const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] as (
|
||||
_event: unknown,
|
||||
args: unknown
|
||||
) => Promise<unknown>
|
||||
|
||||
await handler(settingsInvokeEvent, { appIcon: 'not-real' })
|
||||
|
||||
expect(store.updateSettings).toHaveBeenCalledWith(
|
||||
{ appIcon: 'classic' },
|
||||
{ notifyListeners: true, originWebContentsId: 1 }
|
||||
)
|
||||
expect(applyAppIconMock).toHaveBeenCalledWith('classic')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import { sanitizeFloatingWorkspaceDirectorySetting } from './floating-workspace-
|
|||
import { applyAgentStatusHooksEnabled } from '../agent-hooks/managed-agent-hook-controls'
|
||||
import { applyElectronProxySettings } from '../network/proxy-settings'
|
||||
import { normalizeProxyBypassRules, normalizeProxyUrl } from '../../shared/network-proxy'
|
||||
import { normalizeAppIconId } from '../../shared/app-icon'
|
||||
import { applyAppIcon } from '../app-icon'
|
||||
|
||||
// Why: the whitelist is the source-of-truth for which keys we emit on. Casting
|
||||
// to a Set once at module load lets the IPC handler's per-key membership
|
||||
|
|
@ -63,6 +65,9 @@ export function registerSettingsHandlers(
|
|||
if ('httpProxyBypassRules' in args) {
|
||||
sanitizedArgs.httpProxyBypassRules = normalizeProxyBypassRules(args.httpProxyBypassRules)
|
||||
}
|
||||
if ('appIcon' in args) {
|
||||
sanitizedArgs.appIcon = normalizeAppIconId(args.appIcon)
|
||||
}
|
||||
if (args.theme) {
|
||||
nativeTheme.themeSource = args.theme
|
||||
}
|
||||
|
|
@ -98,6 +103,9 @@ export function registerSettingsHandlers(
|
|||
console.warn('[settings] failed to apply network proxy settings')
|
||||
}
|
||||
}
|
||||
if ('appIcon' in sanitizedArgs && before.appIcon !== result.appIcon) {
|
||||
applyAppIcon(result.appIcon)
|
||||
}
|
||||
|
||||
// Why: telemetry-plan.md§Settings — fire `settings_changed` only for
|
||||
// whitelisted keys, with `value_kind` distinguishing booleans from
|
||||
|
|
|
|||
|
|
@ -257,6 +257,7 @@ describe('Store', () => {
|
|||
expect(settings.branchPrefix).toBe('git-username')
|
||||
expect(settings.refreshLocalBaseRefOnWorktreeCreate).toBe(false)
|
||||
expect(settings.theme).toBe('system')
|
||||
expect(settings.appIcon).toBe('classic')
|
||||
expect(settings.appFontFamily).toBe('Geist')
|
||||
expect(settings.editorAutoSave).toBe(false)
|
||||
expect(settings.editorAutoSaveDelayMs).toBe(1000)
|
||||
|
|
@ -2269,6 +2270,24 @@ describe('Store', () => {
|
|||
expect(updated.disabledTuiAgents).toEqual(['gemini', 'opencode'])
|
||||
})
|
||||
|
||||
it('normalizes app icon on load and update', async () => {
|
||||
writeFileSync(
|
||||
join(testState.dir, 'orca-data.json'),
|
||||
JSON.stringify({
|
||||
settings: {
|
||||
appIcon: 'not-real'
|
||||
}
|
||||
})
|
||||
)
|
||||
const store = await createStore()
|
||||
|
||||
expect(store.getSettings().appIcon).toBe('classic')
|
||||
|
||||
expect(store.updateSettings({ appIcon: 'watercolor' }).appIcon).toBe('watercolor')
|
||||
expect(store.updateSettings({ appIcon: 'blue' }).appIcon).toBe('blue')
|
||||
expect(store.updateSettings({ appIcon: 'not-real' as never }).appIcon).toBe('classic')
|
||||
})
|
||||
|
||||
it('updateSettings keeps the legacy commit-message AI projection in sync', async () => {
|
||||
const store = await createStore()
|
||||
const current = store.getSettings().sourceControlAi!
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ import { normalizeTerminalQuickCommands } from '../shared/terminal-quick-command
|
|||
import { normalizeTaskProviderSettings } from '../shared/task-providers'
|
||||
import { normalizeOpenInApplications } from '../shared/open-in-applications'
|
||||
import { normalizeTerminalShortcutPolicy } from '../shared/keybindings'
|
||||
import { normalizeAppIconId } from '../shared/app-icon'
|
||||
import {
|
||||
normalizeFeatureInteractions,
|
||||
type FeatureInteractionId
|
||||
|
|
@ -1852,6 +1853,7 @@ export class Store {
|
|||
terminalQuickCommands: normalizeTerminalQuickCommands(
|
||||
parsed.settings?.terminalQuickCommands
|
||||
),
|
||||
appIcon: normalizeAppIconId(parsed.settings?.appIcon),
|
||||
defaultTaskSource: taskProviderSettings.defaultTaskSource,
|
||||
visibleTaskProviders: taskProviderSettings.visibleTaskProviders,
|
||||
visibleTaskProvidersDefaultedForJira: true,
|
||||
|
|
@ -2983,6 +2985,9 @@ export class Store {
|
|||
updates.terminalShortcutPolicy
|
||||
)
|
||||
}
|
||||
if ('appIcon' in updates) {
|
||||
sanitizedUpdates.appIcon = normalizeAppIconId(updates.appIcon)
|
||||
}
|
||||
const historyWithPreviousLayout = buildWorkspaceDirHistoryForUpdate(
|
||||
this.state.settings,
|
||||
sanitizedUpdates
|
||||
|
|
|
|||
|
|
@ -36,12 +36,8 @@ vi.mock('@electron-toolkit/utils', () => ({
|
|||
is: isMock
|
||||
}))
|
||||
|
||||
vi.mock('../../../resources/icon.png?asset', () => ({
|
||||
default: 'icon'
|
||||
}))
|
||||
|
||||
vi.mock('../../../resources/icon-dev.png?asset', () => ({
|
||||
default: 'icon-dev'
|
||||
vi.mock('../app-icon', () => ({
|
||||
getAppIconPath: vi.fn(() => 'icon')
|
||||
}))
|
||||
|
||||
vi.mock('../browser/browser-manager', () => ({
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
import { app, BrowserWindow, ipcMain, Menu, nativeTheme, screen, shell } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import icon from '../../../resources/icon.png?asset'
|
||||
import devIcon from '../../../resources/icon-dev.png?asset'
|
||||
import type { Store } from '../persistence'
|
||||
import { getAppIconPath } from '../app-icon'
|
||||
import { browserManager } from '../browser/browser-manager'
|
||||
import { browserSessionRegistry } from '../browser/browser-session-registry'
|
||||
import {
|
||||
|
|
@ -264,7 +263,7 @@ export function createMainWindow(
|
|||
}
|
||||
}
|
||||
: {}),
|
||||
icon: is.dev ? devIcon : icon,
|
||||
icon: getAppIconPath(settings?.appIcon),
|
||||
...platformBlurOptions,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
import type React from 'react'
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import classicIconUrl from '../../../../../resources/icon.png?url'
|
||||
import watercolorIconUrl from '../../../../../resources/app-icons/orca-watercolor.png?url'
|
||||
import blueIconUrl from '../../../../../resources/app-icons/orca-blue.png?url'
|
||||
import { APP_ICON_OPTIONS, normalizeAppIconId, type AppIconId } from '../../../../shared/app-icon'
|
||||
import { Button } from '../ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'
|
||||
|
||||
const APP_ICON_URLS = {
|
||||
classic: classicIconUrl,
|
||||
watercolor: watercolorIconUrl,
|
||||
blue: blueIconUrl
|
||||
} satisfies Record<AppIconId, string>
|
||||
|
||||
type AppIconSelectorProps = {
|
||||
value: AppIconId
|
||||
onChange: (value: AppIconId) => void
|
||||
}
|
||||
|
||||
function getAppIconOptionIndex(value: AppIconId): number {
|
||||
const index = APP_ICON_OPTIONS.findIndex((option) => option.id === value)
|
||||
return Math.max(index, 0)
|
||||
}
|
||||
|
||||
function getOffsetIcon(value: AppIconId, offset: -1 | 1): AppIconId {
|
||||
const index = getAppIconOptionIndex(value)
|
||||
const next = (index + offset + APP_ICON_OPTIONS.length) % APP_ICON_OPTIONS.length
|
||||
return APP_ICON_OPTIONS[next].id
|
||||
}
|
||||
|
||||
type IconCycleButtonProps = {
|
||||
label: string
|
||||
onClick: () => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function IconCycleButton({ label, onClick, children }: IconCycleButtonProps): React.JSX.Element {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon-sm" aria-label={label} onClick={onClick}>
|
||||
{children}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={4}>
|
||||
{label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export function AppIconSelector({ value, onChange }: AppIconSelectorProps): React.JSX.Element {
|
||||
const selected = normalizeAppIconId(value)
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<IconCycleButton label="Previous icon" onClick={() => onChange(getOffsetIcon(selected, -1))}>
|
||||
<ChevronLeft className="size-4" />
|
||||
</IconCycleButton>
|
||||
<img
|
||||
src={APP_ICON_URLS[selected]}
|
||||
alt="Selected app icon"
|
||||
className="size-24 rounded-2xl object-contain"
|
||||
/>
|
||||
<IconCycleButton label="Next icon" onClick={() => onChange(getOffsetIcon(selected, 1))}>
|
||||
<ChevronRight className="size-4" />
|
||||
</IconCycleButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -17,8 +17,10 @@ import {
|
|||
SettingsSwitchRow
|
||||
} from './SettingsFormControls'
|
||||
import { DEFAULT_APP_FONT_FAMILY } from '../../../../shared/constants'
|
||||
import { normalizeAppIconId } from '../../../../shared/app-icon'
|
||||
import { useAvailableStatusBarToggles } from '../status-bar/use-available-status-bar-toggles'
|
||||
import {
|
||||
APP_ICON_ENTRIES,
|
||||
APPEARANCE_PANE_SEARCH_ENTRIES,
|
||||
LAYOUT_ENTRIES,
|
||||
SIDEBAR_ENTRIES,
|
||||
|
|
@ -32,6 +34,7 @@ import {
|
|||
import { TERMINAL_APPEARANCE_SEARCH_ENTRIES } from './terminal-search'
|
||||
import { TerminalAppearanceSection } from './TerminalAppearanceSection'
|
||||
import type { UseGhosttyImportReturn } from './useGhosttyImport'
|
||||
import { AppIconSelector } from './AppIconSelector'
|
||||
export { APPEARANCE_PANE_SEARCH_ENTRIES }
|
||||
|
||||
type AppearancePaneProps = {
|
||||
|
|
@ -295,6 +298,25 @@ export function AppearancePane({
|
|||
</SearchableSetting>
|
||||
</div>
|
||||
</section>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, APP_ICON_ENTRIES) ? (
|
||||
<section key="app-icon" className="space-y-3">
|
||||
<SearchableSetting
|
||||
title="App Icon"
|
||||
description="Choose the app icon shown in the Dock and window switcher."
|
||||
keywords={APP_ICON_ENTRIES.flatMap((entry) => [
|
||||
entry.title,
|
||||
entry.description ?? '',
|
||||
...(entry.keywords ?? [])
|
||||
])}
|
||||
className="max-w-none py-2"
|
||||
>
|
||||
<AppIconSelector
|
||||
value={normalizeAppIconId(settings.appIcon)}
|
||||
onChange={(appIcon) => updateSettings({ appIcon })}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
</section>
|
||||
) : null
|
||||
].filter(Boolean)
|
||||
|
||||
|
|
|
|||
|
|
@ -120,6 +120,14 @@ export const SIDEBAR_ENTRIES: SettingsSearchEntry[] = [
|
|||
}
|
||||
]
|
||||
|
||||
export const APP_ICON_ENTRIES: SettingsSearchEntry[] = [
|
||||
{
|
||||
title: 'App Icon',
|
||||
description: 'Choose the app icon shown in the Dock and window switcher.',
|
||||
keywords: ['app icon', 'orca', 'dock', 'window', 'switcher', 'blue', 'watercolor']
|
||||
}
|
||||
]
|
||||
|
||||
export const APPEARANCE_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
|
||||
...THEME_ENTRIES,
|
||||
...TYPOGRAPHY_ENTRIES,
|
||||
|
|
@ -128,5 +136,6 @@ export const APPEARANCE_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
|
|||
...LAYOUT_ENTRIES,
|
||||
...TITLEBAR_ENTRIES,
|
||||
...STATUS_BAR_ENTRIES,
|
||||
...SIDEBAR_ENTRIES
|
||||
...SIDEBAR_ENTRIES,
|
||||
...APP_ICON_ENTRIES
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
export const APP_ICON_OPTIONS = [
|
||||
{ id: 'classic', label: 'Classic Orca' },
|
||||
{ id: 'watercolor', label: 'Watercolor Orca' },
|
||||
{ id: 'blue', label: 'Blue Orca' }
|
||||
] as const
|
||||
|
||||
export type AppIconId = (typeof APP_ICON_OPTIONS)[number]['id']
|
||||
|
||||
export const DEFAULT_APP_ICON_ID: AppIconId = 'classic'
|
||||
|
||||
export function normalizeAppIconId(value: unknown): AppIconId {
|
||||
return APP_ICON_OPTIONS.some((option) => option.id === value)
|
||||
? (value as AppIconId)
|
||||
: DEFAULT_APP_ICON_ID
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import { cloneDefaultWorkspaceStatuses } from './workspace-statuses'
|
|||
import { TASK_PROVIDERS } from './task-providers'
|
||||
import { DEFAULT_WORKTREE_CARD_PROPERTIES } from './worktree-card-properties'
|
||||
import { getDefaultSourceControlAiSettings } from './source-control-ai'
|
||||
import { DEFAULT_APP_ICON_ID } from './app-icon'
|
||||
|
||||
export { DEFAULT_STATUS_BAR_ITEMS } from './status-bar-defaults'
|
||||
export {
|
||||
|
|
@ -173,6 +174,7 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
|
|||
branchPrefixCustom: '',
|
||||
enableGitHubAttribution: false,
|
||||
theme: 'system',
|
||||
appIcon: DEFAULT_APP_ICON_ID,
|
||||
appFontFamily: DEFAULT_APP_FONT_FAMILY,
|
||||
editorAutoSave: false,
|
||||
editorAutoSaveDelayMs: DEFAULT_EDITOR_AUTO_SAVE_DELAY_MS,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import type { FeatureInteractionState } from './feature-interactions'
|
|||
import type { GitBranchChangeStatus } from './git-status-types'
|
||||
import type { KeybindingOverrides, TerminalShortcutPolicy } from './keybindings'
|
||||
import type { RepoIcon } from './repo-icon'
|
||||
import type { AppIconId } from './app-icon'
|
||||
import type {
|
||||
RepoSourceControlAiOverrides,
|
||||
SourceControlAiSettings
|
||||
|
|
@ -1952,6 +1953,7 @@ export type GlobalSettings = {
|
|||
branchPrefixCustom: string
|
||||
enableGitHubAttribution: boolean
|
||||
theme: 'system' | 'dark' | 'light'
|
||||
appIcon: AppIconId
|
||||
appFontFamily: string
|
||||
editorAutoSave: boolean
|
||||
editorAutoSaveDelayMs: number
|
||||
|
|
|
|||
Loading…
Reference in New Issue