feat(desktop): theme the terminal ANSI palette + restyle the Cmd-K / Ctrl-Tab HUDs

Imported VS Code themes now carry their integrated-terminal ANSI palette
(`terminal.ansi*`), keyed to the painted variant (terminal / darkTerminal).
The terminal adopts it when the full base-8 set is present and keeps its VS
Code defaults otherwise; withSurface still owns the background, so the pane
stays translucent.

Pull the command palette and session switcher into a shared top-center HUD
(`floating-hud.ts`): no dim/blur backdrop, one compact text + item-padding
size, sidebar-label-style section headers (brand-tinted, uppercase), and the
themed portal scrollbar.
This commit is contained in:
Brooklyn Nicholson
2026-06-09 23:37:50 -05:00
parent 33a5bfa3c4
commit 833410e02b
5 changed files with 163 additions and 14 deletions
+54
View File
@@ -8,6 +8,21 @@ import { buildThemeFromMarketplace } from './install'
const themeJson = (type: 'light' | 'dark', background: string, foreground: string) =>
JSON.stringify({ type, colors: { 'editor.background': background, 'editor.foreground': foreground } })
// A full base-8 ANSI set keyed off `red` so each variant is distinguishable.
const ansiColors = (red: string) => ({
'terminal.ansiBlack': '#000000',
'terminal.ansiRed': red,
'terminal.ansiGreen': '#00aa00',
'terminal.ansiYellow': '#aaaa00',
'terminal.ansiBlue': '#0000aa',
'terminal.ansiMagenta': '#aa00aa',
'terminal.ansiCyan': '#00aaaa',
'terminal.ansiWhite': '#aaaaaa'
})
const themeJsonWithAnsi = (type: 'light' | 'dark', background: string, foreground: string, red: string) =>
JSON.stringify({ type, colors: { 'editor.background': background, 'editor.foreground': foreground, ...ansiColors(red) } })
describe('buildThemeFromMarketplace', () => {
it('folds a light + dark variant into one family with both slots', () => {
const result: DesktopMarketplaceThemeResult = {
@@ -57,6 +72,45 @@ describe('buildThemeFromMarketplace', () => {
expect(theme.darkColors).toBe(theme.colors)
})
it('keys each variant terminal palette to its mode (terminal / darkTerminal)', () => {
const result: DesktopMarketplaceThemeResult = {
extensionId: 'ryanolsonx.solarized',
displayName: 'Solarized',
themes: [
{ label: 'Solarized Light', uiTheme: 'vs', contents: themeJsonWithAnsi('light', '#fdf6e3', '#586e75', '#dc322f') },
{ label: 'Solarized Dark', uiTheme: 'vs-dark', contents: themeJsonWithAnsi('dark', '#002b36', '#93a1a1', '#ff5f56') }
]
}
const theme = buildThemeFromMarketplace(result)
expect(theme.terminal?.red).toBe('#dc322f')
expect(theme.darkTerminal?.red).toBe('#ff5f56')
})
it('reuses the sole variant terminal palette for both modes', () => {
const result: DesktopMarketplaceThemeResult = {
extensionId: 'dracula-theme.theme-dracula',
displayName: 'Dracula',
themes: [{ label: 'Dracula', uiTheme: 'vs-dark', contents: themeJsonWithAnsi('dark', '#282a36', '#f8f8f2', '#ff5555') }]
}
const theme = buildThemeFromMarketplace(result)
expect(theme.terminal?.red).toBe('#ff5555')
expect(theme.darkTerminal?.red).toBe('#ff5555')
})
it('leaves terminal slots unset when no variant ships an ANSI palette', () => {
const result: DesktopMarketplaceThemeResult = {
extensionId: 'x.plain',
displayName: 'Plain',
themes: [{ label: 'Plain', uiTheme: 'vs-dark', contents: themeJson('dark', '#101010', '#fafafa') }]
}
const theme = buildThemeFromMarketplace(result)
expect(theme.terminal).toBeUndefined()
expect(theme.darkTerminal).toBeUndefined()
})
it('throws when the extension contributes no themes', () => {
expect(() =>
buildThemeFromMarketplace({ extensionId: 'x.y', displayName: 'X', themes: [] })
+58
View File
@@ -110,4 +110,62 @@ describe('convertVscodeColorTheme', () => {
it('throws when there is no colors map', () => {
expect(() => convertVscodeColorTheme({ name: 'Empty' })).toThrow(/colors/)
})
const fullAnsi = {
'terminal.ansiBlack': '#073642',
'terminal.ansiRed': '#dc322f',
'terminal.ansiGreen': '#859900',
'terminal.ansiYellow': '#b58900',
'terminal.ansiBlue': '#268bd2',
'terminal.ansiMagenta': '#d33682',
'terminal.ansiCyan': '#2aa198',
'terminal.ansiWhite': '#eee8d5',
'terminal.ansiBrightBlack': '#002b36',
'terminal.ansiBrightRed': '#cb4b16',
'terminal.ansiBrightGreen': '#586e75',
'terminal.ansiBrightYellow': '#657b83',
'terminal.ansiBrightBlue': '#839496',
'terminal.ansiBrightMagenta': '#6c71c4',
'terminal.ansiBrightCyan': '#93a1a1',
'terminal.ansiBrightWhite': '#fdf6e3'
}
it('lifts the ANSI palette when the full base-8 set is present', () => {
const { theme } = convertVscodeColorTheme({
name: 'Solarized Dark',
type: 'dark',
colors: {
'editor.background': '#002b36',
'editor.foreground': '#93a1a1',
'terminal.foreground': '#839496',
'terminalCursor.foreground': '#93a1a1',
// Alpha selection must survive un-flattened — xterm blends it.
'terminal.selectionBackground': '#073642aa',
...fullAnsi
}
})
expect(theme.terminal?.red).toBe('#dc322f')
expect(theme.terminal?.brightWhite).toBe('#fdf6e3')
expect(theme.terminal?.foreground).toBe('#839496')
expect(theme.terminal?.cursor).toBe('#93a1a1')
expect(theme.terminal?.selectionBackground).toBe('#073642aa')
// No background slot — the pane keeps the live surface (transparency).
expect('background' in (theme.terminal ?? {})).toBe(false)
})
it('keeps the default palette (no terminal slot) when the ANSI set is partial', () => {
const { theme } = convertVscodeColorTheme({
name: 'Half',
type: 'dark',
colors: {
'editor.background': '#101010',
'editor.foreground': '#fafafa',
'terminal.ansiRed': '#ff0000',
'terminal.ansiGreen': '#00ff00'
}
})
expect(theme.terminal).toBeUndefined()
})
})