refactor: reorganize Settings navigation order and labels (#3988)
* refactor: reorganize Settings navigation order and labels Rename the "Safety" group to "Privacy & Security" (the conventional term for its permissions + telemetry contents), move Floating Workspace to the end of Workflows so it sits with the Terminal/Browser surfaces it's built from, and de-scramble the section registry so it reads in group order. The registry array now mirrors SETTINGS_NAV_GROUPS top-to-bottom, which makes the Cmd+J palette list settings in the same grouped order as the sidebar instead of a jumbled sequence. * refactor: move terminal visuals into appearance settings --------- Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
This commit is contained in:
parent
dd1b28ba2e
commit
b7ea489456
|
|
@ -29,6 +29,9 @@ import {
|
|||
TYPOGRAPHY_ENTRIES,
|
||||
ZOOM_ENTRIES
|
||||
} from './appearance-search'
|
||||
import { TERMINAL_APPEARANCE_SEARCH_ENTRIES } from './terminal-search'
|
||||
import { TerminalAppearanceSection } from './TerminalAppearanceSection'
|
||||
import type { UseGhosttyImportReturn } from './useGhosttyImport'
|
||||
export { APPEARANCE_PANE_SEARCH_ENTRIES }
|
||||
|
||||
type AppearancePaneProps = {
|
||||
|
|
@ -36,6 +39,9 @@ type AppearancePaneProps = {
|
|||
updateSettings: (updates: Partial<GlobalSettings>) => void
|
||||
applyTheme: (theme: 'system' | 'dark' | 'light') => void
|
||||
fontSuggestions: string[]
|
||||
terminalFontSuggestions: string[]
|
||||
systemPrefersDark: boolean
|
||||
ghostty: UseGhosttyImportReturn
|
||||
}
|
||||
|
||||
function ShortcutHintList({ combos }: { combos: string[][] }): React.JSX.Element {
|
||||
|
|
@ -61,7 +67,10 @@ export function AppearancePane({
|
|||
settings,
|
||||
updateSettings,
|
||||
applyTheme,
|
||||
fontSuggestions
|
||||
fontSuggestions,
|
||||
terminalFontSuggestions,
|
||||
systemPrefersDark,
|
||||
ghostty
|
||||
}: AppearancePaneProps): React.JSX.Element {
|
||||
const searchQuery = useAppStore((state) => state.settingsSearchQuery)
|
||||
const zoomInKeyCombos = useShortcutKeyCombos('zoom.in')
|
||||
|
|
@ -149,6 +158,16 @@ export function AppearancePane({
|
|||
) : null}
|
||||
</section>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_APPEARANCE_SEARCH_ENTRIES) ? (
|
||||
<TerminalAppearanceSection
|
||||
key="terminal-appearance"
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
systemPrefersDark={systemPrefersDark}
|
||||
terminalFontSuggestions={terminalFontSuggestions}
|
||||
ghostty={ghostty}
|
||||
/>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, LAYOUT_ENTRIES) ? (
|
||||
<section key="layout" className="space-y-3">
|
||||
<SettingsSubsectionHeader title="File Explorer" />
|
||||
|
|
|
|||
|
|
@ -24,9 +24,7 @@ import { ShortcutsPane } from './ShortcutsPane'
|
|||
import { TerminalPane } from './TerminalPane'
|
||||
import { FloatingWorkspacePane } from './FloatingWorkspacePane'
|
||||
import { useGhosttyImport } from './useGhosttyImport'
|
||||
import { Button } from '../ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
|
||||
import ghosttyIcon from '../../../../../resources/ghostty.svg'
|
||||
import { RepositoryPane } from './RepositoryPane'
|
||||
import { GitPane } from './GitPane'
|
||||
import { CommitMessageAiPane } from './CommitMessageAiPane'
|
||||
|
|
@ -80,7 +78,7 @@ const SETTINGS_NAV_GROUPS = [
|
|||
{ id: 'workflows', title: 'Workflows' },
|
||||
{ id: 'interface', title: 'Interface' },
|
||||
{ id: 'remote', title: 'Remote Access' },
|
||||
{ id: 'safety', title: 'Safety' },
|
||||
{ id: 'security', title: 'Privacy & Security' },
|
||||
{ id: 'experimental', title: 'Experimental' }
|
||||
] as const
|
||||
|
||||
|
|
@ -194,9 +192,8 @@ function Settings(): React.JSX.Element {
|
|||
// reveals controls that the renderer will intentionally hide.
|
||||
const [scrollbackMode, setScrollbackMode] = useState<'preset' | 'custom'>('preset')
|
||||
const [prevScrollbackBytes, setPrevScrollbackBytes] = useState(settings?.terminalScrollbackBytes)
|
||||
// Why: lifted out of TerminalPane so the Terminal section header can render
|
||||
// the import trigger as a headerAction. The modal itself still lives inside
|
||||
// TerminalPane, driven by this shared state.
|
||||
// Why: Appearance owns terminal visual controls, but the Ghostty import flow
|
||||
// still needs Settings-level state so the modal survives section remounts.
|
||||
const ghostty = useGhosttyImport(updateSettings, settings)
|
||||
const [fontSuggestions, setFontSuggestions] = useState<string[]>(
|
||||
Array.from(new Set([DEFAULT_APP_FONT_FAMILY, ...getFallbackTerminalFonts()]))
|
||||
|
|
@ -951,45 +948,18 @@ function Settings(): React.JSX.Element {
|
|||
) : null}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
id="floating-workspace"
|
||||
title="Floating Workspace"
|
||||
description="Global terminal, browser, and markdown tabs."
|
||||
searchEntries={getSectionSearchEntries('floating-workspace')}
|
||||
>
|
||||
{isSectionMounted('floating-workspace') ? (
|
||||
<FloatingWorkspacePane settings={settings} updateSettings={updateSettings} />
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
id="terminal"
|
||||
title="Terminal"
|
||||
description="Shells, terminal appearance, and pane behavior."
|
||||
description="Shells, renderer, sessions, and terminal behavior."
|
||||
searchEntries={getSectionSearchEntries('terminal')}
|
||||
headerAction={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-1.5"
|
||||
onClick={() => void ghostty.handleClick()}
|
||||
>
|
||||
<img src={ghosttyIcon} alt="" aria-hidden="true" className="size-4" />
|
||||
Import from Ghostty
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{isSectionMounted('terminal') ? (
|
||||
<TerminalPane
|
||||
settings={settings}
|
||||
updateSettings={updateSettings}
|
||||
systemPrefersDark={systemPrefersDark}
|
||||
terminalFontSuggestions={fontSuggestions.filter(
|
||||
(font) => font !== DEFAULT_APP_FONT_FAMILY
|
||||
)}
|
||||
scrollbackMode={scrollbackMode}
|
||||
setScrollbackMode={setScrollbackMode}
|
||||
ghostty={ghostty}
|
||||
wslAvailable={windowsTerminalCapabilities.wslAvailable}
|
||||
wslDistros={windowsTerminalCapabilities.wslDistros}
|
||||
wslCapabilitiesLoading={windowsTerminalCapabilities.isLoading}
|
||||
|
|
@ -1031,10 +1001,21 @@ function Settings(): React.JSX.Element {
|
|||
</SettingsSection>
|
||||
) : null}
|
||||
|
||||
<SettingsSection
|
||||
id="floating-workspace"
|
||||
title="Floating Workspace"
|
||||
description="Global terminal, browser, and markdown tabs."
|
||||
searchEntries={getSectionSearchEntries('floating-workspace')}
|
||||
>
|
||||
{isSectionMounted('floating-workspace') ? (
|
||||
<FloatingWorkspacePane settings={settings} updateSettings={updateSettings} />
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
id="appearance"
|
||||
title="Appearance"
|
||||
description="Theme, zoom, app font, sidebars, and status bar."
|
||||
description="Theme, zoom, app and terminal appearance, sidebars, and status bar."
|
||||
searchEntries={getSectionSearchEntries('appearance')}
|
||||
>
|
||||
{isSectionMounted('appearance') ? (
|
||||
|
|
@ -1043,6 +1024,11 @@ function Settings(): React.JSX.Element {
|
|||
updateSettings={updateSettings}
|
||||
applyTheme={applyTheme}
|
||||
fontSuggestions={fontSuggestions}
|
||||
terminalFontSuggestions={fontSuggestions.filter(
|
||||
(font) => font !== DEFAULT_APP_FONT_FAMILY
|
||||
)}
|
||||
systemPrefersDark={systemPrefersDark}
|
||||
ghostty={ghostty}
|
||||
/>
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ const ghosttyMock = {
|
|||
handleOpenChange: vi.fn()
|
||||
}
|
||||
|
||||
import { TerminalPane } from './TerminalPane'
|
||||
import { TerminalAppearanceSection } from './TerminalAppearanceSection'
|
||||
|
||||
type ReactElementLike = {
|
||||
type: unknown
|
||||
|
|
@ -234,40 +234,36 @@ function findGhosttyImportModal(node: unknown): ReactElementLike | null {
|
|||
return null
|
||||
}
|
||||
|
||||
describe('TerminalPane ghostty import wiring', () => {
|
||||
describe('TerminalAppearanceSection ghostty import wiring', () => {
|
||||
beforeEach(() => {
|
||||
mockStateValues.length = 0
|
||||
resetMockState()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Why: the Ghostty import trigger button lives on the section header in
|
||||
// Settings.tsx (headerAction) — not inside TerminalPane. Keep this test
|
||||
// around so a regression that moves the button back into the pane fails.
|
||||
it('does not render an Import from Ghostty button inside the pane', () => {
|
||||
const element = TerminalPane({
|
||||
it('renders the Import from Ghostty button with terminal appearance controls', () => {
|
||||
const element = TerminalAppearanceSection({
|
||||
settings: {} as never,
|
||||
updateSettings: () => {},
|
||||
systemPrefersDark: true,
|
||||
terminalFontSuggestions: [],
|
||||
scrollbackMode: 'preset',
|
||||
setScrollbackMode: () => {},
|
||||
ghostty: ghosttyMock
|
||||
})
|
||||
|
||||
const buttons = findButtons(element)
|
||||
const importButton = buttons.find((b) => b.text === 'Import from Ghostty')
|
||||
expect(importButton).toBeUndefined()
|
||||
expect(importButton).toBeDefined()
|
||||
|
||||
importButton?.onClick?.()
|
||||
expect(ghosttyMock.handleClick).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('passes hook state to GhosttyImportModal', () => {
|
||||
const element = TerminalPane({
|
||||
const element = TerminalAppearanceSection({
|
||||
settings: {} as never,
|
||||
updateSettings: () => {},
|
||||
systemPrefersDark: true,
|
||||
terminalFontSuggestions: [],
|
||||
scrollbackMode: 'preset',
|
||||
setScrollbackMode: () => {},
|
||||
ghostty: ghosttyMock
|
||||
})
|
||||
|
||||
|
|
@ -0,0 +1,440 @@
|
|||
/* eslint-disable max-lines -- Why: terminal visual controls stay together under Appearance so
|
||||
search, previews, and the Ghostty import flow move as one user-facing surface. */
|
||||
import { useState } from 'react'
|
||||
import type { GlobalSettings } from '../../../../shared/types'
|
||||
import {
|
||||
DEFAULT_TERMINAL_FONT_WEIGHT,
|
||||
TERMINAL_FONT_WEIGHT_MAX,
|
||||
TERMINAL_FONT_WEIGHT_MIN,
|
||||
TERMINAL_FONT_WEIGHT_STEP,
|
||||
normalizeTerminalFontWeight
|
||||
} from '../../../../shared/terminal-fonts'
|
||||
import {
|
||||
fontFamilyHasKnownLigatures,
|
||||
resolveTerminalLigaturesEnabled
|
||||
} from '../../../../shared/terminal-ligatures'
|
||||
import { Minus, Plus } from 'lucide-react'
|
||||
import { Button } from '../ui/button'
|
||||
import { Input } from '../ui/input'
|
||||
import {
|
||||
FontAutocomplete,
|
||||
NumberField,
|
||||
SettingsRow,
|
||||
SettingsSegmentedControl,
|
||||
SettingsSubsectionHeader,
|
||||
SettingsSwitchRow
|
||||
} from './SettingsFormControls'
|
||||
import { SearchableSetting } from './SearchableSetting'
|
||||
import { matchesSettingsSearch } from './settings-search'
|
||||
import { useAppStore } from '../../store'
|
||||
import { clampNumber, resolvePaneStyleOptions } from '@/lib/terminal-theme'
|
||||
import {
|
||||
TERMINAL_CURSOR_SEARCH_ENTRIES,
|
||||
TERMINAL_DARK_THEME_SEARCH_ENTRIES,
|
||||
TERMINAL_GHOSTTY_IMPORT_SEARCH_ENTRIES,
|
||||
TERMINAL_LIGHT_THEME_SEARCH_ENTRIES,
|
||||
TERMINAL_PANE_APPEARANCE_SEARCH_ENTRIES,
|
||||
TERMINAL_TYPOGRAPHY_SEARCH_ENTRIES,
|
||||
TERMINAL_WINDOW_SEARCH_ENTRIES
|
||||
} from './terminal-search'
|
||||
import { DarkTerminalThemeSection, LightTerminalThemeSection } from './TerminalThemeSections'
|
||||
import { TerminalWindowSection } from './TerminalWindowSection'
|
||||
import { TerminalSettingsPreview } from './TerminalSettingsPreview'
|
||||
import { GhosttyImportModal } from './GhosttyImportModal'
|
||||
import type { UseGhosttyImportReturn } from './useGhosttyImport'
|
||||
import ghosttyIcon from '../../../../../resources/ghostty.svg'
|
||||
|
||||
type TerminalAppearanceSectionProps = {
|
||||
settings: GlobalSettings
|
||||
updateSettings: (updates: Partial<GlobalSettings>) => void
|
||||
systemPrefersDark: boolean
|
||||
terminalFontSuggestions: string[]
|
||||
ghostty: UseGhosttyImportReturn
|
||||
}
|
||||
|
||||
export function TerminalAppearanceSection({
|
||||
settings,
|
||||
updateSettings,
|
||||
systemPrefersDark,
|
||||
terminalFontSuggestions,
|
||||
ghostty
|
||||
}: TerminalAppearanceSectionProps): React.JSX.Element {
|
||||
const searchQuery = useAppStore((state) => state.settingsSearchQuery)
|
||||
const [themeSearchDark, setThemeSearchDark] = useState('')
|
||||
const [themeSearchLight, setThemeSearchLight] = useState('')
|
||||
// Why: hover preview lets the font picker update the sample without committing a setting.
|
||||
const [previewFontFamily, setPreviewFontFamily] = useState<string | null>(null)
|
||||
const paneStyleOptions = resolvePaneStyleOptions(settings)
|
||||
|
||||
const visibleSections = [
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_GHOSTTY_IMPORT_SEARCH_ENTRIES) ||
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_TYPOGRAPHY_SEARCH_ENTRIES) ? (
|
||||
<section key="typography" className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<div className="min-w-0 space-y-3">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<SettingsSubsectionHeader
|
||||
title="Terminal Typography"
|
||||
description="Default terminal typography for new panes and live updates."
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-1.5"
|
||||
onClick={() => void ghostty.handleClick()}
|
||||
>
|
||||
<img src={ghosttyIcon} alt="" aria-hidden="true" className="size-4" />
|
||||
Import from Ghostty
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-border/40">
|
||||
<SearchableSetting
|
||||
title="Font Size"
|
||||
description="Default terminal font size for new panes and live updates."
|
||||
keywords={['terminal', 'typography', 'text size']}
|
||||
>
|
||||
<SettingsRow
|
||||
label="Font Size"
|
||||
description="Default terminal font size for new panes and live updates."
|
||||
control={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
onClick={() => {
|
||||
const next = Math.max(10, settings.terminalFontSize - 1)
|
||||
updateSettings({ terminalFontSize: next })
|
||||
}}
|
||||
disabled={settings.terminalFontSize <= 10}
|
||||
>
|
||||
<Minus className="size-3" />
|
||||
</Button>
|
||||
<Input
|
||||
type="number"
|
||||
min={10}
|
||||
max={24}
|
||||
value={settings.terminalFontSize}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value, 10)
|
||||
if (!Number.isNaN(value) && value >= 10 && value <= 24) {
|
||||
updateSettings({ terminalFontSize: value })
|
||||
}
|
||||
}}
|
||||
className="w-14 text-center tabular-nums"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
onClick={() => {
|
||||
const next = Math.min(24, settings.terminalFontSize + 1)
|
||||
updateSettings({ terminalFontSize: next })
|
||||
}}
|
||||
disabled={settings.terminalFontSize >= 24}
|
||||
>
|
||||
<Plus className="size-3" />
|
||||
</Button>
|
||||
<span className="text-xs text-muted-foreground">px</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title="Font Family"
|
||||
description="Default terminal font family for new panes and live updates."
|
||||
keywords={['terminal', 'typography', 'font']}
|
||||
>
|
||||
<SettingsRow
|
||||
alignTop
|
||||
label="Font Family"
|
||||
description="Default terminal font family for new panes and live updates."
|
||||
control={
|
||||
<FontAutocomplete
|
||||
value={settings.terminalFontFamily}
|
||||
suggestions={terminalFontSuggestions}
|
||||
onChange={(value) => updateSettings({ terminalFontFamily: value })}
|
||||
onPreviewFontFamily={setPreviewFontFamily}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title="Font Weight"
|
||||
description="Controls the terminal text font weight."
|
||||
keywords={['terminal', 'typography', 'weight']}
|
||||
>
|
||||
<NumberField
|
||||
label="Font Weight"
|
||||
description="Controls the terminal text font weight."
|
||||
value={normalizeTerminalFontWeight(settings.terminalFontWeight)}
|
||||
defaultValue={DEFAULT_TERMINAL_FONT_WEIGHT}
|
||||
min={TERMINAL_FONT_WEIGHT_MIN}
|
||||
max={TERMINAL_FONT_WEIGHT_MAX}
|
||||
step={TERMINAL_FONT_WEIGHT_STEP}
|
||||
suffix="100-900"
|
||||
onChange={(value) =>
|
||||
updateSettings({
|
||||
terminalFontWeight: normalizeTerminalFontWeight(value)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title="Line Height"
|
||||
description="Controls the terminal line height multiplier."
|
||||
keywords={['terminal', 'typography', 'line height', 'spacing']}
|
||||
>
|
||||
<NumberField
|
||||
label="Line Height"
|
||||
description="Controls the terminal line height multiplier."
|
||||
value={settings.terminalLineHeight}
|
||||
defaultValue={1}
|
||||
min={1}
|
||||
max={3}
|
||||
step={0.1}
|
||||
suffix="1-3"
|
||||
onChange={(value) =>
|
||||
updateSettings({
|
||||
terminalLineHeight: clampNumber(value, 1, 3)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title="Font Ligatures"
|
||||
description='Render programming ligatures (e.g. =>, !=, ===) for fonts that ship them. "Auto" enables ligatures only for known ligature fonts (Fira Code, JetBrains Mono, Cascadia Code, Iosevka, etc.).'
|
||||
keywords={[
|
||||
'terminal',
|
||||
'typography',
|
||||
'ligatures',
|
||||
'ligature',
|
||||
'fira code',
|
||||
'jetbrains mono',
|
||||
'cascadia code',
|
||||
'iosevka',
|
||||
'calt',
|
||||
'font features'
|
||||
]}
|
||||
>
|
||||
<SettingsRow
|
||||
label="Font Ligatures"
|
||||
description={
|
||||
settings.terminalLigatures === 'on'
|
||||
? 'Always on. Fonts without ligatures simply render as-is.'
|
||||
: settings.terminalLigatures === 'off'
|
||||
? 'Always off, even for fonts that ship them.'
|
||||
: fontFamilyHasKnownLigatures(settings.terminalFontFamily)
|
||||
? `Auto - enabled for "${settings.terminalFontFamily}".`
|
||||
: `Auto - disabled for "${
|
||||
settings.terminalFontFamily || 'the current font'
|
||||
}".`
|
||||
}
|
||||
control={
|
||||
<SettingsSegmentedControl
|
||||
ariaLabel="Font Ligatures"
|
||||
value={settings.terminalLigatures ?? 'auto'}
|
||||
onChange={(option) => updateSettings({ terminalLigatures: option })}
|
||||
options={[
|
||||
{ value: 'auto', label: 'Auto' },
|
||||
{ value: 'on', label: 'On' },
|
||||
{ value: 'off', label: 'Off' }
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* Why: surface the resolved state explicitly so the "Auto" label
|
||||
isn't ambiguous when a user is staring at it. */}
|
||||
<p className="sr-only" aria-live="polite">
|
||||
Ligatures are currently{' '}
|
||||
{resolveTerminalLigaturesEnabled(
|
||||
settings.terminalLigatures,
|
||||
settings.terminalFontFamily
|
||||
)
|
||||
? 'enabled'
|
||||
: 'disabled'}
|
||||
.
|
||||
</p>
|
||||
</SearchableSetting>
|
||||
</div>
|
||||
</div>
|
||||
<TerminalSettingsPreview
|
||||
title="Preview"
|
||||
settings={settings}
|
||||
systemPrefersDark={systemPrefersDark}
|
||||
previewFontFamily={previewFontFamily}
|
||||
showThemeToggle
|
||||
/>
|
||||
</section>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_CURSOR_SEARCH_ENTRIES) ? (
|
||||
<section key="cursor" className="space-y-3">
|
||||
<SettingsSubsectionHeader
|
||||
title="Terminal Cursor"
|
||||
description="Default cursor appearance for Orca terminal panes."
|
||||
/>
|
||||
|
||||
<div className="divide-y divide-border/40">
|
||||
<SearchableSetting
|
||||
title="Cursor Shape"
|
||||
description="Default cursor appearance for Orca terminal panes."
|
||||
keywords={['terminal', 'cursor', 'bar', 'block', 'underline']}
|
||||
>
|
||||
<SettingsRow
|
||||
label="Cursor Shape"
|
||||
description="Default cursor appearance for Orca terminal panes."
|
||||
control={
|
||||
<SettingsSegmentedControl
|
||||
ariaLabel="Cursor Shape"
|
||||
value={settings.terminalCursorStyle}
|
||||
onChange={(option) => updateSettings({ terminalCursorStyle: option })}
|
||||
options={[
|
||||
{ value: 'bar', label: 'Bar' },
|
||||
{ value: 'block', label: 'Block' },
|
||||
{ value: 'underline', label: 'Underline' }
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title="Blinking Cursor"
|
||||
description="Uses the blinking variant of the selected cursor shape."
|
||||
keywords={['terminal', 'cursor', 'blink']}
|
||||
>
|
||||
<SettingsSwitchRow
|
||||
label="Blinking Cursor"
|
||||
description="Uses the blinking variant of the selected cursor shape."
|
||||
checked={settings.terminalCursorBlink}
|
||||
onChange={() =>
|
||||
updateSettings({ terminalCursorBlink: !settings.terminalCursorBlink })
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title="Cursor Opacity"
|
||||
description="Opacity of the terminal cursor."
|
||||
keywords={['terminal', 'cursor', 'opacity', 'transparency']}
|
||||
>
|
||||
<NumberField
|
||||
label="Cursor Opacity"
|
||||
description="Opacity of the terminal cursor."
|
||||
value={settings.terminalCursorOpacity ?? 1}
|
||||
defaultValue={1}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
suffix="0-1"
|
||||
onChange={(value) =>
|
||||
updateSettings({
|
||||
terminalCursorOpacity: clampNumber(value, 0, 1)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
</div>
|
||||
</section>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_PANE_APPEARANCE_SEARCH_ENTRIES) ? (
|
||||
<section key="pane-appearance" className="space-y-3">
|
||||
<SettingsSubsectionHeader
|
||||
title="Terminal Panes"
|
||||
description="Control inactive pane dimming and split divider thickness."
|
||||
/>
|
||||
|
||||
<div className="divide-y divide-border/40">
|
||||
<SearchableSetting
|
||||
title="Inactive Pane Opacity"
|
||||
description="Opacity applied to panes that are not currently active."
|
||||
keywords={['pane', 'opacity', 'dimming']}
|
||||
>
|
||||
<NumberField
|
||||
label="Inactive Pane Opacity"
|
||||
description="Opacity applied to panes that are not currently active."
|
||||
value={paneStyleOptions.inactivePaneOpacity}
|
||||
defaultValue={0.8}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
suffix="0-1"
|
||||
onChange={(value) =>
|
||||
updateSettings({
|
||||
terminalInactivePaneOpacity: clampNumber(value, 0, 1)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
<SearchableSetting
|
||||
title="Divider Thickness"
|
||||
description="Thickness of the pane divider line."
|
||||
keywords={['pane', 'divider', 'thickness']}
|
||||
>
|
||||
<NumberField
|
||||
label="Divider Thickness"
|
||||
description="Thickness of the pane divider line."
|
||||
value={paneStyleOptions.dividerThicknessPx}
|
||||
defaultValue={1}
|
||||
min={1}
|
||||
max={32}
|
||||
step={1}
|
||||
suffix="px"
|
||||
onChange={(value) =>
|
||||
updateSettings({
|
||||
terminalDividerThicknessPx: clampNumber(value, 1, 32)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
</div>
|
||||
</section>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_WINDOW_SEARCH_ENTRIES) ? (
|
||||
<TerminalWindowSection key="window" settings={settings} updateSettings={updateSettings} />
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_DARK_THEME_SEARCH_ENTRIES) ? (
|
||||
<DarkTerminalThemeSection
|
||||
key="dark-theme"
|
||||
settings={settings}
|
||||
systemPrefersDark={systemPrefersDark}
|
||||
themeSearchDark={themeSearchDark}
|
||||
setThemeSearchDark={setThemeSearchDark}
|
||||
updateSettings={updateSettings}
|
||||
previewFontFamily={previewFontFamily}
|
||||
/>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_LIGHT_THEME_SEARCH_ENTRIES) ? (
|
||||
<LightTerminalThemeSection
|
||||
key="light-theme"
|
||||
settings={settings}
|
||||
themeSearchLight={themeSearchLight}
|
||||
setThemeSearchLight={setThemeSearchLight}
|
||||
updateSettings={updateSettings}
|
||||
previewFontFamily={previewFontFamily}
|
||||
/>
|
||||
) : null
|
||||
].filter(Boolean)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{visibleSections.map((section, index) => (
|
||||
<div key={index} className="space-y-6">
|
||||
{index > 0 ? <div className="h-px bg-border/60" /> : null}
|
||||
{section}
|
||||
</div>
|
||||
))}
|
||||
<GhosttyImportModal
|
||||
open={ghostty.open}
|
||||
onOpenChange={ghostty.handleOpenChange}
|
||||
preview={ghostty.preview}
|
||||
loading={ghostty.loading}
|
||||
onApply={ghostty.handleApply}
|
||||
applied={ghostty.applied}
|
||||
applyError={ghostty.applyError}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -149,17 +149,6 @@ vi.mock('@/lib/terminal-theme', () => ({
|
|||
resolvePaneStyleOptions: () => ({ inactivePaneOpacity: 0.8, dividerThicknessPx: 1 })
|
||||
}))
|
||||
|
||||
const ghosttyMock = {
|
||||
open: false,
|
||||
preview: null,
|
||||
loading: false,
|
||||
applied: false,
|
||||
applyError: null,
|
||||
handleClick: vi.fn(),
|
||||
handleApply: vi.fn(),
|
||||
handleOpenChange: vi.fn()
|
||||
}
|
||||
|
||||
import { TerminalPane } from './TerminalPane'
|
||||
|
||||
type ReactElementLike = {
|
||||
|
|
@ -239,11 +228,8 @@ describe('TerminalPane PowerShell version setting', () => {
|
|||
terminalWordSeparator: ''
|
||||
} as never,
|
||||
updateSettings: () => {},
|
||||
systemPrefersDark: true,
|
||||
terminalFontSuggestions: [],
|
||||
scrollbackMode: 'preset',
|
||||
setScrollbackMode: () => {},
|
||||
ghostty: ghosttyMock,
|
||||
wslAvailable: false,
|
||||
pwshAvailable: false,
|
||||
gitBashAvailable: false
|
||||
|
|
@ -264,11 +250,8 @@ describe('TerminalPane PowerShell version setting', () => {
|
|||
terminalWordSeparator: ''
|
||||
} as never,
|
||||
updateSettings: () => {},
|
||||
systemPrefersDark: true,
|
||||
terminalFontSuggestions: [],
|
||||
scrollbackMode: 'preset',
|
||||
setScrollbackMode: () => {},
|
||||
ghostty: ghosttyMock,
|
||||
wslAvailable: true,
|
||||
wslDistros: ['Ubuntu'],
|
||||
pwshAvailable: false,
|
||||
|
|
@ -287,11 +270,8 @@ describe('TerminalPane PowerShell version setting', () => {
|
|||
terminalWordSeparator: ''
|
||||
} as never,
|
||||
updateSettings: () => {},
|
||||
systemPrefersDark: true,
|
||||
terminalFontSuggestions: [],
|
||||
scrollbackMode: 'preset',
|
||||
setScrollbackMode: () => {},
|
||||
ghostty: ghosttyMock,
|
||||
wslAvailable: false,
|
||||
pwshAvailable: false,
|
||||
gitBashAvailable: false
|
||||
|
|
@ -310,11 +290,8 @@ describe('TerminalPane PowerShell version setting', () => {
|
|||
terminalWordSeparator: ''
|
||||
} as never,
|
||||
updateSettings: () => {},
|
||||
systemPrefersDark: true,
|
||||
terminalFontSuggestions: [],
|
||||
scrollbackMode: 'preset',
|
||||
setScrollbackMode: () => {},
|
||||
ghostty: ghosttyMock,
|
||||
wslAvailable: true,
|
||||
wslDistros: ['Ubuntu', 'Debian'],
|
||||
pwshAvailable: false,
|
||||
|
|
@ -337,11 +314,8 @@ describe('TerminalPane PowerShell version setting', () => {
|
|||
terminalWordSeparator: ''
|
||||
} as never,
|
||||
updateSettings: () => {},
|
||||
systemPrefersDark: true,
|
||||
terminalFontSuggestions: [],
|
||||
scrollbackMode: 'preset',
|
||||
setScrollbackMode: () => {},
|
||||
ghostty: ghosttyMock,
|
||||
wslAvailable: false,
|
||||
pwshAvailable: false,
|
||||
gitBashAvailable: true
|
||||
|
|
@ -359,11 +333,8 @@ describe('TerminalPane PowerShell version setting', () => {
|
|||
terminalWordSeparator: ''
|
||||
} as never,
|
||||
updateSettings: () => {},
|
||||
systemPrefersDark: true,
|
||||
terminalFontSuggestions: [],
|
||||
scrollbackMode: 'preset',
|
||||
setScrollbackMode: () => {},
|
||||
ghostty: ghosttyMock,
|
||||
wslAvailable: false,
|
||||
pwshAvailable: false,
|
||||
gitBashAvailable: false
|
||||
|
|
|
|||
|
|
@ -1,29 +1,12 @@
|
|||
/* eslint-disable max-lines -- Why: TerminalPane is the single owner of all terminal settings UI;
|
||||
splitting individual settings into separate files would scatter related controls without a
|
||||
meaningful abstraction boundary. Mirrors the same decision made for GeneralPane.tsx. */
|
||||
import { useState } from 'react'
|
||||
/* eslint-disable max-lines -- Why: TerminalPane keeps terminal workflow, runtime, and recovery
|
||||
settings together so search shows one focused terminal behavior surface. */
|
||||
import type { GlobalSettings, SetupScriptLaunchMode } from '../../../../shared/types'
|
||||
import {
|
||||
DEFAULT_TERMINAL_FONT_WEIGHT,
|
||||
TERMINAL_FONT_WEIGHT_MAX,
|
||||
TERMINAL_FONT_WEIGHT_MIN,
|
||||
TERMINAL_FONT_WEIGHT_STEP,
|
||||
normalizeTerminalFontWeight
|
||||
} from '../../../../shared/terminal-fonts'
|
||||
import {
|
||||
fontFamilyHasKnownLigatures,
|
||||
resolveTerminalLigaturesEnabled
|
||||
} from '../../../../shared/terminal-ligatures'
|
||||
import { Button } from '../ui/button'
|
||||
import { Input } from '../ui/input'
|
||||
import { Separator } from '../ui/separator'
|
||||
import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
||||
import { Minus, Plus } from 'lucide-react'
|
||||
import { clampNumber, resolvePaneStyleOptions } from '@/lib/terminal-theme'
|
||||
import { clampNumber } from '@/lib/terminal-theme'
|
||||
import {
|
||||
FontAutocomplete,
|
||||
NumberField,
|
||||
SettingsRow,
|
||||
SettingsSegmentedControl,
|
||||
SettingsSubsectionHeader,
|
||||
|
|
@ -37,16 +20,11 @@ import { isMacUserAgent, isWindowsUserAgent } from '@/components/terminal-pane/p
|
|||
import {
|
||||
MANAGE_SESSIONS_SEARCH_ENTRIES,
|
||||
TERMINAL_ADVANCED_SEARCH_ENTRIES,
|
||||
TERMINAL_CURSOR_SEARCH_ENTRIES,
|
||||
TERMINAL_DARK_THEME_SEARCH_ENTRIES,
|
||||
TERMINAL_LIGHT_THEME_SEARCH_ENTRIES,
|
||||
TERMINAL_MAC_OPTION_SEARCH_ENTRIES,
|
||||
TERMINAL_MAC_YEN_SEARCH_ENTRIES,
|
||||
TERMINAL_PANE_STYLE_SEARCH_ENTRIES,
|
||||
TERMINAL_PANE_INTERACTION_SEARCH_ENTRIES,
|
||||
TERMINAL_RENDERING_SEARCH_ENTRIES,
|
||||
TERMINAL_SETUP_SCRIPT_SEARCH_ENTRIES,
|
||||
TERMINAL_TYPOGRAPHY_SEARCH_ENTRIES,
|
||||
TERMINAL_WINDOW_SEARCH_ENTRIES
|
||||
TERMINAL_SETUP_SCRIPT_SEARCH_ENTRIES
|
||||
} from './terminal-search'
|
||||
import {
|
||||
TERMINAL_RIGHT_CLICK_TO_PASTE_SEARCH_ENTRY,
|
||||
|
|
@ -54,26 +32,15 @@ import {
|
|||
TERMINAL_WINDOWS_SHELL_SEARCH_ENTRY
|
||||
} from './terminal-windows-search'
|
||||
import { useDetectedOptionAsAlt } from '@/lib/keyboard-layout/use-effective-mac-option-as-alt'
|
||||
import { DarkTerminalThemeSection, LightTerminalThemeSection } from './TerminalThemeSections'
|
||||
import { TerminalWindowSection } from './TerminalWindowSection'
|
||||
import { GhosttyImportModal } from './GhosttyImportModal'
|
||||
import type { UseGhosttyImportReturn } from './useGhosttyImport'
|
||||
import { ManageSessionsSection } from './ManageSessionsSection'
|
||||
import { TerminalSettingsPreview } from './TerminalSettingsPreview'
|
||||
import { OSC52_CLIPBOARD_SETTING_ID } from '../terminal-pane/osc52-clipboard-setting-anchor'
|
||||
import { WINDOWS_GIT_BASH_SHELL } from '../../../../shared/windows-terminal-shell'
|
||||
|
||||
type TerminalPaneProps = {
|
||||
settings: GlobalSettings
|
||||
updateSettings: (updates: Partial<GlobalSettings>) => void
|
||||
systemPrefersDark: boolean
|
||||
terminalFontSuggestions: string[]
|
||||
scrollbackMode: 'preset' | 'custom'
|
||||
setScrollbackMode: (mode: 'preset' | 'custom') => void
|
||||
/** Ghostty import modal state + handlers. Lifted to the Settings shell so
|
||||
* the section header can render the trigger button as a headerAction
|
||||
* instead of taking its own row inside the settings list. */
|
||||
ghostty: UseGhosttyImportReturn
|
||||
/** Whether WSL is installed on this Windows machine. */
|
||||
wslAvailable?: boolean
|
||||
/** Installed WSL distro names, used to choose the default WSL terminal target. */
|
||||
|
|
@ -89,11 +56,8 @@ type TerminalPaneProps = {
|
|||
export function TerminalPane({
|
||||
settings,
|
||||
updateSettings,
|
||||
systemPrefersDark,
|
||||
terminalFontSuggestions,
|
||||
scrollbackMode,
|
||||
setScrollbackMode,
|
||||
ghostty,
|
||||
wslAvailable,
|
||||
wslDistros = [],
|
||||
wslCapabilitiesLoading = false,
|
||||
|
|
@ -103,12 +67,6 @@ export function TerminalPane({
|
|||
const searchQuery = useAppStore((state) => state.settingsSearchQuery)
|
||||
const isWindows = isWindowsUserAgent()
|
||||
const isMac = isMacUserAgent()
|
||||
const [themeSearchDark, setThemeSearchDark] = useState('')
|
||||
const [themeSearchLight, setThemeSearchLight] = useState('')
|
||||
// Why: hover preview lets the font picker update the sample without committing a setting.
|
||||
const [previewFontFamily, setPreviewFontFamily] = useState<string | null>(null)
|
||||
|
||||
const paneStyleOptions = resolvePaneStyleOptions(settings)
|
||||
const detectedLayout = useDetectedOptionAsAlt()
|
||||
const detectedLayoutLabel =
|
||||
detectedLayout === 'us'
|
||||
|
|
@ -225,196 +183,6 @@ export function TerminalPane({
|
|||
</div>
|
||||
</section>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_TYPOGRAPHY_SEARCH_ENTRIES) ? (
|
||||
<section key="typography" className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<div className="min-w-0 space-y-3">
|
||||
<SettingsSubsectionHeader
|
||||
title="Typography"
|
||||
description="Default terminal typography for new panes and live updates."
|
||||
/>
|
||||
|
||||
<div className="divide-y divide-border/40">
|
||||
<SearchableSetting
|
||||
title="Font Size"
|
||||
description="Default terminal font size for new panes and live updates."
|
||||
keywords={['terminal', 'typography', 'text size']}
|
||||
>
|
||||
<SettingsRow
|
||||
label="Font Size"
|
||||
description="Default terminal font size for new panes and live updates."
|
||||
control={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
onClick={() => {
|
||||
const next = Math.max(10, settings.terminalFontSize - 1)
|
||||
updateSettings({ terminalFontSize: next })
|
||||
}}
|
||||
disabled={settings.terminalFontSize <= 10}
|
||||
>
|
||||
<Minus className="size-3" />
|
||||
</Button>
|
||||
<Input
|
||||
type="number"
|
||||
min={10}
|
||||
max={24}
|
||||
value={settings.terminalFontSize}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value, 10)
|
||||
if (!Number.isNaN(value) && value >= 10 && value <= 24) {
|
||||
updateSettings({ terminalFontSize: value })
|
||||
}
|
||||
}}
|
||||
className="w-14 text-center tabular-nums"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
onClick={() => {
|
||||
const next = Math.min(24, settings.terminalFontSize + 1)
|
||||
updateSettings({ terminalFontSize: next })
|
||||
}}
|
||||
disabled={settings.terminalFontSize >= 24}
|
||||
>
|
||||
<Plus className="size-3" />
|
||||
</Button>
|
||||
<span className="text-xs text-muted-foreground">px</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title="Font Family"
|
||||
description="Default terminal font family for new panes and live updates."
|
||||
keywords={['terminal', 'typography', 'font']}
|
||||
>
|
||||
<SettingsRow
|
||||
alignTop
|
||||
label="Font Family"
|
||||
description="Default terminal font family for new panes and live updates."
|
||||
control={
|
||||
<FontAutocomplete
|
||||
value={settings.terminalFontFamily}
|
||||
suggestions={terminalFontSuggestions}
|
||||
onChange={(value) => updateSettings({ terminalFontFamily: value })}
|
||||
onPreviewFontFamily={setPreviewFontFamily}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title="Font Weight"
|
||||
description="Controls the terminal text font weight."
|
||||
keywords={['terminal', 'typography', 'weight']}
|
||||
>
|
||||
<NumberField
|
||||
label="Font Weight"
|
||||
description="Controls the terminal text font weight."
|
||||
value={normalizeTerminalFontWeight(settings.terminalFontWeight)}
|
||||
defaultValue={DEFAULT_TERMINAL_FONT_WEIGHT}
|
||||
min={TERMINAL_FONT_WEIGHT_MIN}
|
||||
max={TERMINAL_FONT_WEIGHT_MAX}
|
||||
step={TERMINAL_FONT_WEIGHT_STEP}
|
||||
suffix="100–900"
|
||||
onChange={(value) =>
|
||||
updateSettings({
|
||||
terminalFontWeight: normalizeTerminalFontWeight(value)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title="Line Height"
|
||||
description="Controls the terminal line height multiplier."
|
||||
keywords={['terminal', 'typography', 'line height', 'spacing']}
|
||||
>
|
||||
<NumberField
|
||||
label="Line Height"
|
||||
description="Controls the terminal line height multiplier."
|
||||
value={settings.terminalLineHeight}
|
||||
defaultValue={1}
|
||||
min={1}
|
||||
max={3}
|
||||
step={0.1}
|
||||
suffix="1–3"
|
||||
onChange={(value) =>
|
||||
updateSettings({
|
||||
terminalLineHeight: clampNumber(value, 1, 3)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title="Font Ligatures"
|
||||
description='Render programming ligatures (e.g. =>, !=, ===) for fonts that ship them. "Auto" enables ligatures only for known ligature fonts (Fira Code, JetBrains Mono, Cascadia Code, Iosevka, etc.).'
|
||||
keywords={[
|
||||
'terminal',
|
||||
'typography',
|
||||
'ligatures',
|
||||
'ligature',
|
||||
'fira code',
|
||||
'jetbrains mono',
|
||||
'cascadia code',
|
||||
'iosevka',
|
||||
'calt',
|
||||
'font features'
|
||||
]}
|
||||
>
|
||||
<SettingsRow
|
||||
label="Font Ligatures"
|
||||
description={
|
||||
settings.terminalLigatures === 'on'
|
||||
? 'Always on. Fonts without ligatures simply render as-is.'
|
||||
: settings.terminalLigatures === 'off'
|
||||
? 'Always off, even for fonts that ship them.'
|
||||
: fontFamilyHasKnownLigatures(settings.terminalFontFamily)
|
||||
? `Auto — enabled for "${settings.terminalFontFamily}".`
|
||||
: `Auto — disabled for "${
|
||||
settings.terminalFontFamily || 'the current font'
|
||||
}".`
|
||||
}
|
||||
control={
|
||||
<SettingsSegmentedControl
|
||||
ariaLabel="Font Ligatures"
|
||||
value={settings.terminalLigatures ?? 'auto'}
|
||||
onChange={(option) => updateSettings({ terminalLigatures: option })}
|
||||
options={[
|
||||
{ value: 'auto', label: 'Auto' },
|
||||
{ value: 'on', label: 'On' },
|
||||
{ value: 'off', label: 'Off' }
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* Why: surface the resolved state explicitly so the "Auto" label
|
||||
isn't ambiguous when a user is staring at it. */}
|
||||
<p className="sr-only" aria-live="polite">
|
||||
Ligatures are currently{' '}
|
||||
{resolveTerminalLigaturesEnabled(
|
||||
settings.terminalLigatures,
|
||||
settings.terminalFontFamily
|
||||
)
|
||||
? 'enabled'
|
||||
: 'disabled'}
|
||||
.
|
||||
</p>
|
||||
</SearchableSetting>
|
||||
</div>
|
||||
</div>
|
||||
<TerminalSettingsPreview
|
||||
title="Preview"
|
||||
settings={settings}
|
||||
systemPrefersDark={systemPrefersDark}
|
||||
previewFontFamily={previewFontFamily}
|
||||
showThemeToggle
|
||||
/>
|
||||
</section>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_RENDERING_SEARCH_ENTRIES) ? (
|
||||
<section key="rendering" className="space-y-3">
|
||||
<SettingsSubsectionHeader
|
||||
|
|
@ -464,129 +232,16 @@ export function TerminalPane({
|
|||
</div>
|
||||
</section>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_CURSOR_SEARCH_ENTRIES) ? (
|
||||
<section key="cursor" className="space-y-3">
|
||||
<SettingsSubsectionHeader
|
||||
title="Cursor"
|
||||
description="Default cursor appearance for Orca terminal panes."
|
||||
/>
|
||||
|
||||
<div className="divide-y divide-border/40">
|
||||
<SearchableSetting
|
||||
title="Cursor Shape"
|
||||
description="Default cursor appearance for Orca terminal panes."
|
||||
keywords={['terminal', 'cursor', 'bar', 'block', 'underline']}
|
||||
>
|
||||
<SettingsRow
|
||||
label="Cursor Shape"
|
||||
description="Default cursor appearance for Orca terminal panes."
|
||||
control={
|
||||
<SettingsSegmentedControl
|
||||
ariaLabel="Cursor Shape"
|
||||
value={settings.terminalCursorStyle}
|
||||
onChange={(option) => updateSettings({ terminalCursorStyle: option })}
|
||||
options={[
|
||||
{ value: 'bar', label: 'Bar' },
|
||||
{ value: 'block', label: 'Block' },
|
||||
{ value: 'underline', label: 'Underline' }
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title="Blinking Cursor"
|
||||
description="Uses the blinking variant of the selected cursor shape."
|
||||
keywords={['terminal', 'cursor', 'blink']}
|
||||
>
|
||||
<SettingsSwitchRow
|
||||
label="Blinking Cursor"
|
||||
description="Uses the blinking variant of the selected cursor shape."
|
||||
checked={settings.terminalCursorBlink}
|
||||
onChange={() =>
|
||||
updateSettings({ terminalCursorBlink: !settings.terminalCursorBlink })
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title="Cursor Opacity"
|
||||
description="Opacity of the terminal cursor."
|
||||
keywords={['terminal', 'cursor', 'opacity', 'transparency']}
|
||||
>
|
||||
<NumberField
|
||||
label="Cursor Opacity"
|
||||
description="Opacity of the terminal cursor."
|
||||
value={settings.terminalCursorOpacity ?? 1}
|
||||
defaultValue={1}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
suffix="0–1"
|
||||
onChange={(value) =>
|
||||
updateSettings({
|
||||
terminalCursorOpacity: clampNumber(value, 0, 1)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
</div>
|
||||
</section>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_PANE_STYLE_SEARCH_ENTRIES) ||
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_PANE_INTERACTION_SEARCH_ENTRIES) ||
|
||||
(isWindows &&
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_RIGHT_CLICK_TO_PASTE_SEARCH_ENTRY)) ? (
|
||||
<section key="pane-styling" className="space-y-3">
|
||||
<section key="pane-interaction" className="space-y-3">
|
||||
<SettingsSubsectionHeader
|
||||
title="Pane Styling"
|
||||
description="Control inactive pane dimming, divider thickness, mouse behavior, and transition timing."
|
||||
title="Terminal Interaction"
|
||||
description="Mouse and clipboard behavior for terminal panes."
|
||||
/>
|
||||
|
||||
<div className="divide-y divide-border/40">
|
||||
<SearchableSetting
|
||||
title="Inactive Pane Opacity"
|
||||
description="Opacity applied to panes that are not currently active."
|
||||
keywords={['pane', 'opacity', 'dimming']}
|
||||
>
|
||||
<NumberField
|
||||
label="Inactive Pane Opacity"
|
||||
description="Opacity applied to panes that are not currently active."
|
||||
value={paneStyleOptions.inactivePaneOpacity}
|
||||
defaultValue={0.8}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
suffix="0–1"
|
||||
onChange={(value) =>
|
||||
updateSettings({
|
||||
terminalInactivePaneOpacity: clampNumber(value, 0, 1)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
<SearchableSetting
|
||||
title="Divider Thickness"
|
||||
description="Thickness of the pane divider line."
|
||||
keywords={['pane', 'divider', 'thickness']}
|
||||
>
|
||||
<NumberField
|
||||
label="Divider Thickness"
|
||||
description="Thickness of the pane divider line."
|
||||
value={paneStyleOptions.dividerThicknessPx}
|
||||
defaultValue={1}
|
||||
min={1}
|
||||
max={32}
|
||||
step={1}
|
||||
suffix="px"
|
||||
onChange={(value) =>
|
||||
updateSettings({
|
||||
terminalDividerThicknessPx: clampNumber(value, 1, 32)
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
{/* Why: the Windows-only right-click toggle lives in this section, so the
|
||||
section must also match that search term or settings search would hide
|
||||
the control even though it is present. */}
|
||||
|
|
@ -687,30 +342,6 @@ export function TerminalPane({
|
|||
</div>
|
||||
</section>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_WINDOW_SEARCH_ENTRIES) ? (
|
||||
<TerminalWindowSection key="window" settings={settings} updateSettings={updateSettings} />
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_DARK_THEME_SEARCH_ENTRIES) ? (
|
||||
<DarkTerminalThemeSection
|
||||
key="dark-theme"
|
||||
settings={settings}
|
||||
systemPrefersDark={systemPrefersDark}
|
||||
themeSearchDark={themeSearchDark}
|
||||
setThemeSearchDark={setThemeSearchDark}
|
||||
updateSettings={updateSettings}
|
||||
previewFontFamily={previewFontFamily}
|
||||
/>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_LIGHT_THEME_SEARCH_ENTRIES) ? (
|
||||
<LightTerminalThemeSection
|
||||
key="light-theme"
|
||||
settings={settings}
|
||||
themeSearchLight={themeSearchLight}
|
||||
setThemeSearchLight={setThemeSearchLight}
|
||||
updateSettings={updateSettings}
|
||||
previewFontFamily={previewFontFamily}
|
||||
/>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_SETUP_SCRIPT_SEARCH_ENTRIES) ? (
|
||||
<section key="setup-script" className="space-y-3">
|
||||
<SettingsSubsectionHeader
|
||||
|
|
@ -1048,15 +679,6 @@ export function TerminalPane({
|
|||
{section}
|
||||
</div>
|
||||
))}
|
||||
<GhosttyImportModal
|
||||
open={ghostty.open}
|
||||
onOpenChange={ghostty.handleOpenChange}
|
||||
preview={ghostty.preview}
|
||||
loading={ghostty.loading}
|
||||
onApply={ghostty.handleApply}
|
||||
applied={ghostty.applied}
|
||||
applyError={ghostty.applyError}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { StatusBarItem } from '../../../../shared/types'
|
||||
import type { SettingsSearchEntry } from './settings-search'
|
||||
import { TERMINAL_APPEARANCE_SEARCH_ENTRIES } from './terminal-search'
|
||||
|
||||
export const STATUS_BAR_TOGGLES: readonly {
|
||||
id: StatusBarItem
|
||||
|
|
@ -123,6 +124,7 @@ export const APPEARANCE_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
|
|||
...THEME_ENTRIES,
|
||||
...TYPOGRAPHY_ENTRIES,
|
||||
...ZOOM_ENTRIES,
|
||||
...TERMINAL_APPEARANCE_SEARCH_ENTRIES,
|
||||
...LAYOUT_ENTRIES,
|
||||
...TITLEBAR_ENTRIES,
|
||||
...STATUS_BAR_ENTRIES,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { getTerminalPaneSearchEntries } from './terminal-search'
|
||||
import { APPEARANCE_PANE_SEARCH_ENTRIES } from './appearance-search'
|
||||
|
||||
describe('getTerminalPaneSearchEntries', () => {
|
||||
it('includes the Windows right-click setting on Windows', () => {
|
||||
|
|
@ -66,12 +67,18 @@ describe('getTerminalPaneSearchEntries', () => {
|
|||
).toBe(true)
|
||||
})
|
||||
|
||||
it('includes the Ghostty import setting on all platforms', () => {
|
||||
it('keeps terminal appearance settings in the Appearance search index', () => {
|
||||
const entriesWindows = getTerminalPaneSearchEntries({ isWindows: true, isMac: false })
|
||||
const entriesMac = getTerminalPaneSearchEntries({ isWindows: false, isMac: true })
|
||||
const entriesLinux = getTerminalPaneSearchEntries({ isWindows: false, isMac: false })
|
||||
expect(entriesWindows.some((entry) => entry.title === 'Import from Ghostty')).toBe(true)
|
||||
expect(entriesMac.some((entry) => entry.title === 'Import from Ghostty')).toBe(true)
|
||||
expect(entriesLinux.some((entry) => entry.title === 'Import from Ghostty')).toBe(true)
|
||||
|
||||
expect(entriesWindows.some((entry) => entry.title === 'Import from Ghostty')).toBe(false)
|
||||
expect(entriesMac.some((entry) => entry.title === 'Font Size')).toBe(false)
|
||||
expect(entriesLinux.some((entry) => entry.title === 'Dark Theme')).toBe(false)
|
||||
expect(
|
||||
APPEARANCE_PANE_SEARCH_ENTRIES.some((entry) => entry.title === 'Import from Ghostty')
|
||||
).toBe(true)
|
||||
expect(APPEARANCE_PANE_SEARCH_ENTRIES.some((entry) => entry.title === 'Font Size')).toBe(true)
|
||||
expect(APPEARANCE_PANE_SEARCH_ENTRIES.some((entry) => entry.title === 'Dark Theme')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export const TERMINAL_CURSOR_SEARCH_ENTRIES: SettingsSearchEntry[] = [
|
|||
}
|
||||
]
|
||||
|
||||
export const TERMINAL_PANE_STYLE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
|
||||
export const TERMINAL_PANE_APPEARANCE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
|
||||
{
|
||||
title: 'Inactive Pane Opacity',
|
||||
description: 'Opacity applied to panes that are not currently active.',
|
||||
|
|
@ -89,7 +89,10 @@ export const TERMINAL_PANE_STYLE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
|
|||
title: 'Divider Thickness',
|
||||
description: 'Thickness of the pane divider line.',
|
||||
keywords: ['pane', 'divider', 'thickness']
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
export const TERMINAL_PANE_INTERACTION_SEARCH_ENTRIES: SettingsSearchEntry[] = [
|
||||
{
|
||||
title: 'Focus Follows Mouse',
|
||||
description:
|
||||
|
|
@ -269,6 +272,16 @@ export const TERMINAL_SETUP_SCRIPT_SEARCH_ENTRIES: SettingsSearchEntry[] = [
|
|||
}
|
||||
]
|
||||
|
||||
export const TERMINAL_APPEARANCE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
|
||||
...TERMINAL_TYPOGRAPHY_SEARCH_ENTRIES,
|
||||
...TERMINAL_CURSOR_SEARCH_ENTRIES,
|
||||
...TERMINAL_PANE_APPEARANCE_SEARCH_ENTRIES,
|
||||
...TERMINAL_DARK_THEME_SEARCH_ENTRIES,
|
||||
...TERMINAL_LIGHT_THEME_SEARCH_ENTRIES,
|
||||
...TERMINAL_WINDOW_SEARCH_ENTRIES,
|
||||
...TERMINAL_GHOSTTY_IMPORT_SEARCH_ENTRIES
|
||||
]
|
||||
|
||||
export function getTerminalPaneSearchEntries(platform: {
|
||||
isWindows: boolean
|
||||
isMac: boolean
|
||||
|
|
@ -277,16 +290,10 @@ export function getTerminalPaneSearchEntries(platform: {
|
|||
// platform-only controls out of other platforms' search results prevents
|
||||
// users from landing on an option the UI intentionally hides.
|
||||
return [
|
||||
...TERMINAL_TYPOGRAPHY_SEARCH_ENTRIES,
|
||||
...TERMINAL_RENDERING_SEARCH_ENTRIES,
|
||||
...TERMINAL_CURSOR_SEARCH_ENTRIES,
|
||||
...TERMINAL_PANE_STYLE_SEARCH_ENTRIES,
|
||||
...TERMINAL_PANE_INTERACTION_SEARCH_ENTRIES,
|
||||
...(platform.isWindows ? TERMINAL_WINDOWS_SEARCH_ENTRIES : []),
|
||||
...TERMINAL_DARK_THEME_SEARCH_ENTRIES,
|
||||
...TERMINAL_LIGHT_THEME_SEARCH_ENTRIES,
|
||||
...TERMINAL_WINDOW_SEARCH_ENTRIES,
|
||||
...TERMINAL_SETUP_SCRIPT_SEARCH_ENTRIES,
|
||||
...TERMINAL_GHOSTTY_IMPORT_SEARCH_ENTRIES,
|
||||
...MANAGE_SESSIONS_SEARCH_ENTRIES,
|
||||
...TERMINAL_ADVANCED_SEARCH_ENTRIES,
|
||||
...(platform.isMac
|
||||
|
|
|
|||
|
|
@ -92,6 +92,9 @@ export function buildSettingsNavigationMetadata({
|
|||
: RUNTIME_ENVIRONMENTS_SEARCH_ENTRY
|
||||
|
||||
return [
|
||||
// Why: this array's order must mirror SETTINGS_NAV_GROUPS so the Settings
|
||||
// sidebar and the Cmd+J palette both read top-to-bottom in the same grouped
|
||||
// order — keep each new entry beside its group's siblings.
|
||||
{
|
||||
id: 'agents',
|
||||
title: 'Agents',
|
||||
|
|
@ -173,34 +176,10 @@ export function buildSettingsNavigationMetadata({
|
|||
searchEntries: TASKS_PANE_SEARCH_ENTRIES,
|
||||
group: 'workflows'
|
||||
},
|
||||
{
|
||||
id: 'floating-workspace',
|
||||
title: 'Floating Workspace',
|
||||
description: 'Global terminal, browser, and markdown tabs.',
|
||||
icon: PanelsTopLeft,
|
||||
searchEntries: FLOATING_WORKSPACE_SEARCH_ENTRIES,
|
||||
group: 'workflows'
|
||||
},
|
||||
{
|
||||
id: 'appearance',
|
||||
title: 'Appearance',
|
||||
description: 'Theme, zoom, app font, sidebars, and status bar.',
|
||||
icon: Palette,
|
||||
searchEntries: APPEARANCE_PANE_SEARCH_ENTRIES,
|
||||
group: 'interface'
|
||||
},
|
||||
{
|
||||
id: 'input',
|
||||
title: 'Input & Editing',
|
||||
description: 'Selection and editing behavior.',
|
||||
icon: TextCursorInput,
|
||||
searchEntries: INPUT_PANE_SEARCH_ENTRIES,
|
||||
group: 'interface'
|
||||
},
|
||||
{
|
||||
id: 'terminal',
|
||||
title: 'Terminal',
|
||||
description: 'Shells, terminal appearance, and pane behavior.',
|
||||
description: 'Shells, renderer, sessions, and terminal behavior.',
|
||||
icon: SquareTerminal,
|
||||
searchEntries: terminalPaneSearchEntries,
|
||||
group: 'workflows'
|
||||
|
|
@ -222,7 +201,35 @@ export function buildSettingsNavigationMetadata({
|
|||
icon: Globe,
|
||||
searchEntries: BROWSER_PANE_SEARCH_ENTRIES,
|
||||
group: 'workflows'
|
||||
},
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'floating-workspace',
|
||||
title: 'Floating Workspace',
|
||||
description: 'Global terminal, browser, and markdown tabs.',
|
||||
icon: PanelsTopLeft,
|
||||
searchEntries: FLOATING_WORKSPACE_SEARCH_ENTRIES,
|
||||
group: 'workflows'
|
||||
},
|
||||
{
|
||||
id: 'appearance',
|
||||
title: 'Appearance',
|
||||
description: 'Theme, zoom, app and terminal appearance, sidebars, and status bar.',
|
||||
icon: Palette,
|
||||
searchEntries: APPEARANCE_PANE_SEARCH_ENTRIES,
|
||||
group: 'interface'
|
||||
},
|
||||
{
|
||||
id: 'input',
|
||||
title: 'Input & Editing',
|
||||
description: 'Selection and editing behavior.',
|
||||
icon: TextCursorInput,
|
||||
searchEntries: INPUT_PANE_SEARCH_ENTRIES,
|
||||
group: 'interface'
|
||||
},
|
||||
...(showDesktopOnlySettings
|
||||
? [
|
||||
{
|
||||
id: 'notifications',
|
||||
title: 'Notifications',
|
||||
|
|
@ -233,6 +240,22 @@ export function buildSettingsNavigationMetadata({
|
|||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'shortcuts',
|
||||
title: 'Shortcuts',
|
||||
description: 'Keyboard shortcuts for common actions.',
|
||||
icon: Keyboard,
|
||||
searchEntries: SHORTCUTS_PANE_SEARCH_ENTRIES,
|
||||
group: 'interface'
|
||||
},
|
||||
{
|
||||
id: 'stats',
|
||||
title: 'Stats & Usage',
|
||||
description: 'Orca stats plus Claude, Codex, and OpenCode usage analytics.',
|
||||
icon: BarChart3,
|
||||
searchEntries: STATS_PANE_SEARCH_ENTRIES,
|
||||
group: 'interface'
|
||||
},
|
||||
{
|
||||
id: 'servers',
|
||||
title: 'Remote Orca Servers',
|
||||
|
|
@ -272,7 +295,7 @@ export function buildSettingsNavigationMetadata({
|
|||
description: 'macOS privacy access for terminal-launched developer tools.',
|
||||
icon: ShieldCheck,
|
||||
searchEntries: DEVELOPER_PERMISSIONS_PANE_SEARCH_ENTRIES,
|
||||
group: 'safety'
|
||||
group: 'security'
|
||||
}
|
||||
]
|
||||
: []),
|
||||
|
|
@ -282,23 +305,7 @@ export function buildSettingsNavigationMetadata({
|
|||
description: 'Anonymous usage data and telemetry controls.',
|
||||
icon: Lock,
|
||||
searchEntries: PRIVACY_PANE_SEARCH_ENTRIES,
|
||||
group: 'safety'
|
||||
},
|
||||
{
|
||||
id: 'shortcuts',
|
||||
title: 'Shortcuts',
|
||||
description: 'Keyboard shortcuts for common actions.',
|
||||
icon: Keyboard,
|
||||
searchEntries: SHORTCUTS_PANE_SEARCH_ENTRIES,
|
||||
group: 'interface'
|
||||
},
|
||||
{
|
||||
id: 'stats',
|
||||
title: 'Stats & Usage',
|
||||
description: 'Orca stats plus Claude, Codex, and OpenCode usage analytics.',
|
||||
icon: BarChart3,
|
||||
searchEntries: STATS_PANE_SEARCH_ENTRIES,
|
||||
group: 'interface'
|
||||
group: 'security'
|
||||
},
|
||||
{
|
||||
id: 'experimental',
|
||||
|
|
|
|||
Loading…
Reference in New Issue