starting janitorial work

This commit is contained in:
2026-02-06 13:41:44 -05:00
parent 920042ee2a
commit 1293d30225
4 changed files with 383 additions and 298 deletions

View File

@@ -53,7 +53,9 @@ export function App() {
// My Shows page returns panel renderers // My Shows page returns panel renderers
const myShows = MyShowsPage({ const myShows = MyShowsPage({
get focused() { return activeTab() === "shows" && layerDepth() > 0 }, get focused() {
return activeTab() === "shows" && layerDepth() > 0;
},
onPlayEpisode: (episode, feed) => { onPlayEpisode: (episode, feed) => {
handlePlayEpisode(episode); handlePlayEpisode(episode);
}, },
@@ -69,8 +71,12 @@ export function App() {
setActiveTab(tab); setActiveTab(tab);
setInputFocused(false); setInputFocused(false);
}, },
get inputFocused() { return inputFocused() }, get inputFocused() {
get navigationEnabled() { return layerDepth() === 0 }, return inputFocused();
},
get navigationEnabled() {
return layerDepth() === 0;
},
layerDepth, layerDepth,
onLayerChange: (newDepth) => { onLayerChange: (newDepth) => {
setLayerDepth(newDepth); setLayerDepth(newDepth);
@@ -94,18 +100,20 @@ export function App() {
// Copy selected text to clipboard when selection ends (mouse release) // Copy selected text to clipboard when selection ends (mouse release)
useSelectionHandler((selection: any) => { useSelectionHandler((selection: any) => {
if (!selection) return if (!selection) return;
const text = selection.getSelectedText?.() const text = selection.getSelectedText?.();
if (!text || text.trim().length === 0) return if (!text || text.trim().length === 0) return;
Clipboard.copy(text).then(() => { Clipboard.copy(text)
emit("toast.show", { .then(() => {
message: "Copied to clipboard", emit("toast.show", {
variant: "info", message: "Copied to clipboard",
duration: 1500, variant: "info",
duration: 1500,
});
}) })
}).catch(() => {}) .catch(() => {});
}) });
const getPanels = createMemo(() => { const getPanels = createMemo(() => {
const tab = activeTab(); const tab = activeTab();
@@ -156,19 +164,21 @@ export function App() {
if (showAuthPanel()) { if (showAuthPanel()) {
if (auth.isAuthenticated) { if (auth.isAuthenticated) {
return { return {
panels: [{ panels: [
title: "Account", {
content: ( title: "Account",
<SyncProfile content: (
focused={layerDepth() > 0} <SyncProfile
onLogout={() => { focused={layerDepth() > 0}
auth.logout(); onLogout={() => {
setShowAuthPanel(false); auth.logout();
}} setShowAuthPanel(false);
onManageSync={() => setShowAuthPanel(false)} }}
/> onManageSync={() => setShowAuthPanel(false)}
), />
}], ),
},
],
activePanelIndex: 0, activePanelIndex: 0,
hint: "Esc back", hint: "Esc back",
}; };
@@ -203,104 +213,121 @@ export function App() {
}; };
return { return {
panels: [{ panels: [
title: "Sign In", {
content: authContent(), title: "Sign In",
}], content: authContent(),
},
],
activePanelIndex: 0, activePanelIndex: 0,
hint: "Esc back", hint: "Esc back",
}; };
} }
return { return {
panels: [{ panels: [
title: "Settings", {
content: ( title: "Settings",
<SettingsScreen content: (
onOpenAccount={() => setShowAuthPanel(true)} <SettingsScreen
accountLabel={ onOpenAccount={() => setShowAuthPanel(true)}
auth.isAuthenticated accountLabel={
? `Signed in as ${auth.user?.email}` auth.isAuthenticated
: "Not signed in" ? `Signed in as ${auth.user?.email}`
} : "Not signed in"
accountStatus={auth.isAuthenticated ? "signed-in" : "signed-out"} }
onExit={() => setLayerDepth(0)} accountStatus={
/> auth.isAuthenticated ? "signed-in" : "signed-out"
), }
}], onExit={() => setLayerDepth(0)}
/>
),
},
],
activePanelIndex: 0, activePanelIndex: 0,
hint: "j/k navigate | Enter select | Esc back", hint: "j/k navigate | Enter select | Esc back",
}; };
case "discover": case "discover":
return { return {
panels: [{ panels: [
title: "Discover", {
content: ( title: "Discover",
<DiscoverPage content: (
focused={layerDepth() > 0} <DiscoverPage
onExit={() => setLayerDepth(0)} focused={layerDepth() > 0}
/> onExit={() => setLayerDepth(0)}
), />
}], ),
},
],
activePanelIndex: 0, activePanelIndex: 0,
hint: "Tab switch focus | j/k navigate | Enter subscribe | r refresh | Esc back", hint: "Tab switch focus | j/k navigate | Enter subscribe | r refresh | Esc back",
}; };
case "search": case "search":
return { return {
panels: [{ panels: [
title: "Search", {
content: ( title: "Search",
<SearchPage content: (
focused={layerDepth() > 0} <SearchPage
onInputFocusChange={setInputFocused} focused={layerDepth() > 0}
onExit={() => setLayerDepth(0)} onInputFocusChange={setInputFocused}
onSubscribe={(result) => { onExit={() => setLayerDepth(0)}
const feeds = feedStore.feeds(); onSubscribe={(result) => {
const alreadySubscribed = feeds.some( const feeds = feedStore.feeds();
(feed) => const alreadySubscribed = feeds.some(
feed.podcast.id === result.podcast.id || (feed) =>
feed.podcast.feedUrl === result.podcast.feedUrl, feed.podcast.id === result.podcast.id ||
); feed.podcast.feedUrl === result.podcast.feedUrl,
if (!alreadySubscribed) {
feedStore.addFeed(
{ ...result.podcast, isSubscribed: true },
result.sourceId,
FeedVisibility.PUBLIC,
); );
}
}} if (!alreadySubscribed) {
/> feedStore.addFeed(
), { ...result.podcast, isSubscribed: true },
}], result.sourceId,
FeedVisibility.PUBLIC,
);
}
}}
/>
),
},
],
activePanelIndex: 0, activePanelIndex: 0,
hint: "Tab switch focus | / search | Enter select | Esc back", hint: "Tab switch focus | / search | Enter select | Esc back",
}; };
case "player": case "player":
return { return {
panels: [{ panels: [
title: "Player", {
content: ( title: "Player",
<Player focused={layerDepth() > 0} onExit={() => setLayerDepth(0)} /> content: (
), <Player
}], focused={layerDepth() > 0}
onExit={() => setLayerDepth(0)}
/>
),
},
],
activePanelIndex: 0, activePanelIndex: 0,
hint: "Space play/pause | Esc back", hint: "Space play/pause | Esc back",
}; };
default: default:
return { return {
panels: [{ panels: [
title: tab, {
content: ( title: tab,
<box padding={2}> content: (
<text>Coming soon</text> <box padding={2}>
</box> <text>Coming soon</text>
), </box>
}], ),
},
],
activePanelIndex: 0, activePanelIndex: 0,
hint: "", hint: "",
}; };
@@ -308,24 +335,21 @@ export function App() {
}); });
return ( return (
<ErrorBoundary fallback={(err) => ( <ErrorBoundary
<box border padding={2}> fallback={(err) => (
<text fg="red"> <box border padding={2}>
Error: {err?.message ?? String(err)}{"\n"} <text fg="red">
Press a number key (1-6) to switch tabs. Error: {err?.message ?? String(err)}
</text> {"\n"}
</box> Press a number key (1-6) to switch tabs.
)}> </text>
</box>
)}
>
<Layout <Layout
header={ header={
<TabNavigation activeTab={activeTab()} onTabSelect={setActiveTab} /> <TabNavigation activeTab={activeTab()} onTabSelect={setActiveTab} />
} }
footer={
<box flexDirection="row" justifyContent="space-between" width="100%">
<Navigation activeTab={activeTab()} onTabSelect={setActiveTab} />
<text fg="gray">{getPanels().hint}</text>
</box>
}
panels={getPanels().panels} panels={getPanels().panels}
activePanelIndex={getPanels().activePanelIndex} activePanelIndex={getPanels().activePanelIndex}
/> />

View File

@@ -1,11 +1,17 @@
import { useTheme } from "../context/ThemeContext" import { useTheme } from "../context/ThemeContext";
export type TabId = "feed" | "shows" | "discover" | "search" | "player" | "settings" export type TabId =
| "feed"
| "shows"
| "discover"
| "search"
| "player"
| "settings";
export type TabDefinition = { export type TabDefinition = {
id: TabId id: TabId;
label: string label: string;
} };
export const tabs: TabDefinition[] = [ export const tabs: TabDefinition[] = [
{ id: "feed", label: "Feed" }, { id: "feed", label: "Feed" },
@@ -14,27 +20,31 @@ export const tabs: TabDefinition[] = [
{ id: "search", label: "Search" }, { id: "search", label: "Search" },
{ id: "player", label: "Player" }, { id: "player", label: "Player" },
{ id: "settings", label: "Settings" }, { id: "settings", label: "Settings" },
] ];
type TabProps = { type TabProps = {
tab: TabDefinition tab: TabDefinition;
active: boolean active: boolean;
onSelect: (tab: TabId) => void onSelect: (tab: TabId) => void;
} };
export function Tab(props: TabProps) { export function Tab(props: TabProps) {
const { theme } = useTheme() const { theme } = useTheme();
return ( return (
<box <box
border border
borderColor={theme.border}
onMouseDown={() => props.onSelect(props.tab.id)} onMouseDown={() => props.onSelect(props.tab.id)}
style={{ padding: 1, backgroundColor: props.active ? theme.primary : "transparent" }} style={{
padding: 1,
backgroundColor: props.active ? theme.primary : "transparent",
}}
> >
<text> <text style={{ fg: theme.text }}>
{props.active ? "[" : " "} {props.active ? "[" : " "}
{props.tab.label} {props.tab.label}
{props.active ? "]" : " "} {props.active ? "]" : " "}
</text> </text>
</box> </box>
) );
} }

View File

@@ -1,80 +1,91 @@
import { createEffect, createMemo, onMount, onCleanup } from "solid-js" import { createEffect, createMemo, onMount, onCleanup } from "solid-js";
import { createStore, produce } from "solid-js/store" import { createStore, produce } from "solid-js/store";
import { useRenderer } from "@opentui/solid" import { useRenderer } from "@opentui/solid";
import type { ThemeName } from "../types/settings" import type { ThemeName } from "../types/settings";
import type { ThemeJson } from "../types/theme-schema" import type { ThemeJson } from "../types/theme-schema";
import { useAppStore } from "../stores/app" import { useAppStore } from "../stores/app";
import { THEME_JSON } from "../constants/themes" import { THEME_JSON } from "../constants/themes";
import { generateSyntax, generateSubtleSyntax } from "../utils/syntax-highlighter" import {
import { resolveTerminalTheme, loadThemes } from "../utils/theme" generateSyntax,
import { createSimpleContext } from "./helper" generateSubtleSyntax,
import { setupThemeSignalHandler, emitThemeChanged, emitThemeModeChanged } from "../utils/theme-observer" } from "../utils/syntax-highlighter";
import { createTerminalPalette, type RGBA, type TerminalColors } from "@opentui/core" import { resolveTerminalTheme, loadThemes } from "../utils/theme";
import { createSimpleContext } from "./helper";
import {
setupThemeSignalHandler,
emitThemeChanged,
emitThemeModeChanged,
} from "../utils/theme-observer";
import {
createTerminalPalette,
type RGBA,
type TerminalColors,
} from "@opentui/core";
type ThemeResolved = { type ThemeResolved = {
primary: RGBA primary: RGBA;
secondary: RGBA secondary: RGBA;
accent: RGBA accent: RGBA;
error: RGBA error: RGBA;
warning: RGBA warning: RGBA;
success: RGBA success: RGBA;
info: RGBA info: RGBA;
text: RGBA text: RGBA;
textMuted: RGBA textMuted: RGBA;
selectedListItemText: RGBA selectedListItemText: RGBA;
background: RGBA background: RGBA;
backgroundPanel: RGBA backgroundPanel: RGBA;
backgroundElement: RGBA backgroundElement: RGBA;
backgroundMenu: RGBA backgroundMenu: RGBA;
border: RGBA border: RGBA;
borderActive: RGBA borderActive: RGBA;
borderSubtle: RGBA borderSubtle: RGBA;
diffAdded: RGBA diffAdded: RGBA;
diffRemoved: RGBA diffRemoved: RGBA;
diffContext: RGBA diffContext: RGBA;
diffHunkHeader: RGBA diffHunkHeader: RGBA;
diffHighlightAdded: RGBA diffHighlightAdded: RGBA;
diffHighlightRemoved: RGBA diffHighlightRemoved: RGBA;
diffAddedBg: RGBA diffAddedBg: RGBA;
diffRemovedBg: RGBA diffRemovedBg: RGBA;
diffContextBg: RGBA diffContextBg: RGBA;
diffLineNumber: RGBA diffLineNumber: RGBA;
diffAddedLineNumberBg: RGBA diffAddedLineNumberBg: RGBA;
diffRemovedLineNumberBg: RGBA diffRemovedLineNumberBg: RGBA;
markdownText: RGBA markdownText: RGBA;
markdownHeading: RGBA markdownHeading: RGBA;
markdownLink: RGBA markdownLink: RGBA;
markdownLinkText: RGBA markdownLinkText: RGBA;
markdownCode: RGBA markdownCode: RGBA;
markdownBlockQuote: RGBA markdownBlockQuote: RGBA;
markdownEmph: RGBA markdownEmph: RGBA;
markdownStrong: RGBA markdownStrong: RGBA;
markdownHorizontalRule: RGBA markdownHorizontalRule: RGBA;
markdownListItem: RGBA markdownListItem: RGBA;
markdownListEnumeration: RGBA markdownListEnumeration: RGBA;
markdownImage: RGBA markdownImage: RGBA;
markdownImageText: RGBA markdownImageText: RGBA;
markdownCodeBlock: RGBA markdownCodeBlock: RGBA;
syntaxComment: RGBA syntaxComment: RGBA;
syntaxKeyword: RGBA syntaxKeyword: RGBA;
syntaxFunction: RGBA syntaxFunction: RGBA;
syntaxVariable: RGBA syntaxVariable: RGBA;
syntaxString: RGBA syntaxString: RGBA;
syntaxNumber: RGBA syntaxNumber: RGBA;
syntaxType: RGBA syntaxType: RGBA;
syntaxOperator: RGBA syntaxOperator: RGBA;
syntaxPunctuation: RGBA syntaxPunctuation: RGBA;
muted?: RGBA muted?: RGBA;
surface?: RGBA surface?: RGBA;
layerBackgrounds?: { layerBackgrounds?: {
layer0: RGBA layer0: RGBA;
layer1: RGBA layer1: RGBA;
layer2: RGBA layer2: RGBA;
layer3: RGBA layer3: RGBA;
} };
_hasSelectedListItemText?: boolean _hasSelectedListItemText?: boolean;
thinkingOpacity?: number thinkingOpacity?: number;
} };
/** /**
* Theme context using the createSimpleContext pattern. * Theme context using the createSimpleContext pattern.
@@ -89,88 +100,104 @@ type ThemeResolved = {
export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
name: "Theme", name: "Theme",
init: (props: { mode: "dark" | "light" }) => { init: (props: { mode: "dark" | "light" }) => {
const appStore = useAppStore() const appStore = useAppStore();
const renderer = useRenderer() const renderer = useRenderer();
const [store, setStore] = createStore({ const [store, setStore] = createStore({
themes: THEME_JSON as Record<string, ThemeJson>, themes: THEME_JSON as Record<string, ThemeJson>,
mode: props.mode, mode: props.mode,
active: appStore.state().settings.theme as string, active: appStore.state().settings.theme as string,
system: undefined as undefined | TerminalColors, system: undefined as undefined | TerminalColors,
ready: false, ready: false,
}) });
function init() { function init() {
resolveSystemTheme() resolveSystemTheme();
loadThemes() loadThemes()
.then((custom) => { .then((custom) => {
setStore( setStore(
produce((draft) => { produce((draft) => {
Object.assign(draft.themes, custom) Object.assign(draft.themes, custom);
}) }),
) );
}) })
.catch(() => { .catch(() => {
// If custom themes fail to load, fall back to opencode theme // If custom themes fail to load, fall back to opencode theme
setStore("active", "opencode") setStore("active", "opencode");
}) })
.finally(() => { .finally(() => {
// Only set ready if not waiting for system theme // Only set ready if not waiting for system theme
if (store.active !== "system") { if (store.active !== "system") {
setStore("ready", true) setStore("ready", true);
} }
}) });
} }
async function waitForCapabilities(timeoutMs = 300) { async function waitForCapabilities(timeoutMs = 300) {
if (renderer.capabilities) return if (renderer.capabilities) return;
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
let done = false let done = false;
const onCaps = () => { const onCaps = () => {
if (done) return if (done) return;
done = true done = true;
renderer.off("capabilities", onCaps) renderer.off("capabilities", onCaps);
clearTimeout(timer) clearTimeout(timer);
resolve() resolve();
} };
const timer = setTimeout(() => { const timer = setTimeout(() => {
if (done) return if (done) return;
done = true done = true;
renderer.off("capabilities", onCaps) renderer.off("capabilities", onCaps);
resolve() resolve();
}, timeoutMs) }, timeoutMs);
renderer.on("capabilities", onCaps) renderer.on("capabilities", onCaps);
}) });
} }
async function resolveSystemTheme() { async function resolveSystemTheme() {
if (process.env.TMUX) { if (process.env.TMUX) {
await waitForCapabilities() await waitForCapabilities();
} }
let colors: TerminalColors | null = null let colors: TerminalColors | null = null;
try { try {
colors = await renderer.getPalette({ size: 16 }) colors = await renderer.getPalette({ size: 16 });
} catch { } catch {
colors = null colors = null;
} }
if (!colors?.palette?.[0] && process.env.TMUX) { if (!colors?.palette?.[0] && process.env.TMUX) {
const writeOut = (renderer as unknown as { writeOut?: (data: string | Buffer) => boolean }).writeOut const writeOut = (
const writeFn = typeof writeOut === "function" ? writeOut.bind(renderer) : process.stdout.write.bind(process.stdout) renderer as unknown as {
const detector = createTerminalPalette(process.stdin, process.stdout, writeFn, true) writeOut?: (data: string | Buffer) => boolean;
}
).writeOut;
const writeFn =
typeof writeOut === "function"
? writeOut.bind(renderer)
: process.stdout.write.bind(process.stdout);
const detector = createTerminalPalette(
process.stdin,
process.stdout,
writeFn,
true,
);
try { try {
const tmuxColors = await detector.detect({ size: 16, timeout: 1200 }) const tmuxColors = await detector.detect({ size: 16, timeout: 1200 });
if (tmuxColors?.palette?.[0]) { if (tmuxColors?.palette?.[0]) {
colors = tmuxColors colors = tmuxColors;
} }
} finally { } finally {
detector.cleanup() detector.cleanup();
} }
} }
const hasPalette = Boolean(colors?.palette?.some((value) => Boolean(value))) const hasPalette = Boolean(
const hasDefaultColors = Boolean(colors?.defaultBackground || colors?.defaultForeground) colors?.palette?.some((value) => Boolean(value)),
);
const hasDefaultColors = Boolean(
colors?.defaultBackground || colors?.defaultForeground,
);
if (!hasPalette && !hasDefaultColors) { if (!hasPalette && !hasDefaultColors) {
// No system colors available, fall back to default // No system colors available, fall back to default
@@ -179,89 +206,100 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
if (store.active === "system") { if (store.active === "system") {
setStore( setStore(
produce((draft) => { produce((draft) => {
draft.active = "opencode" draft.active = "opencode";
draft.ready = true draft.ready = true;
}) }),
) );
} }
return return;
} }
if (colors) { if (colors) {
setStore( setStore(
produce((draft) => { produce((draft) => {
draft.system = colors draft.system = colors;
if (store.active === "system") { if (store.active === "system") {
draft.ready = true draft.ready = true;
} }
}) }),
) );
} }
} }
onMount(init) onMount(init);
// Setup SIGUSR2 signal handler for dynamic theme reload // Setup SIGUSR2 signal handler for dynamic theme reload
// This allows external tools to trigger a theme refresh by sending: // This allows external tools to trigger a theme refresh by sending:
// `kill -USR2 <pid>` // `kill -USR2 <pid>`
const cleanupSignalHandler = setupThemeSignalHandler(() => { const cleanupSignalHandler = setupThemeSignalHandler(() => {
renderer.clearPaletteCache() renderer.clearPaletteCache();
init() init();
}) });
onCleanup(cleanupSignalHandler) onCleanup(cleanupSignalHandler);
// Sync active theme with app store settings // Sync active theme with app store settings
createEffect(() => { createEffect(() => {
const theme = appStore.state().settings.theme const theme = appStore.state().settings.theme;
if (theme) setStore("active", theme) if (theme) setStore("active", theme);
}) });
// Emit theme change events for observers // Emit theme change events for observers
createEffect(() => { createEffect(() => {
const theme = store.active const theme = store.active;
const mode = store.mode const mode = store.mode;
if (store.ready) { if (store.ready) {
emitThemeChanged(theme, mode) emitThemeChanged(theme, mode);
} }
}) });
const values = createMemo(() => { const values = createMemo(() => {
return resolveTerminalTheme(store.themes, store.active, store.mode, store.system) return resolveTerminalTheme(
}) store.themes,
store.active,
store.mode,
store.system,
);
});
const syntax = createMemo(() => generateSyntax(values() as unknown as Record<string, RGBA>)) const syntax = createMemo(() =>
generateSyntax(values() as unknown as Record<string, RGBA>),
);
const subtleSyntax = createMemo(() => const subtleSyntax = createMemo(() =>
generateSubtleSyntax(values() as unknown as Record<string, RGBA> & { thinkingOpacity?: number }) generateSubtleSyntax(
) values() as unknown as Record<string, RGBA> & {
thinkingOpacity?: number;
},
),
);
return { return {
theme: new Proxy(values(), { theme: new Proxy(values(), {
get(_target, prop) { get(_target, prop) {
// @ts-expect-error - dynamic property access // @ts-expect-error - dynamic property access
return values()[prop] return values()[prop];
}, },
}) as ThemeResolved, }) as ThemeResolved,
get selected() { get selected() {
return store.active return store.active;
}, },
all() { all() {
return store.themes return store.themes;
}, },
syntax, syntax,
subtleSyntax, subtleSyntax,
mode() { mode() {
return store.mode return store.mode;
}, },
setMode(mode: "dark" | "light") { setMode(mode: "dark" | "light") {
setStore("mode", mode) setStore("mode", mode);
emitThemeModeChanged(mode) emitThemeModeChanged(mode);
}, },
set(theme: string) { set(theme: string) {
appStore.setTheme(theme as ThemeName) appStore.setTheme(theme as ThemeName);
}, },
get ready() { get ready() {
return store.ready return store.ready;
}, },
} };
}, },
}) });

View File

@@ -1,36 +1,49 @@
import { RGBA, type TerminalColors } from "@opentui/core" import { RGBA, type TerminalColors } from "@opentui/core";
import { ansiToRgba } from "./ansi-to-rgba" import { ansiToRgba } from "./ansi-to-rgba";
import { generateGrayScale, generateMutedTextColor, tint } from "./color-generation" import {
import type { ThemeJson } from "../types/theme-schema" generateGrayScale,
generateMutedTextColor,
tint,
} from "./color-generation";
import type { ThemeJson } from "../types/theme-schema";
let cached: TerminalColors | null = null let cached: TerminalColors | null = null;
export function clearPaletteCache() { export function clearPaletteCache() {
cached = null cached = null;
} }
export function detectSystemTheme(colors: TerminalColors) { export function detectSystemTheme(colors: TerminalColors) {
const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0] ?? "#000000") const bg = RGBA.fromHex(
const luminance = 0.299 * bg.r + 0.587 * bg.g + 0.114 * bg.b colors.defaultBackground ?? colors.palette[0] ?? "#000000",
const mode = luminance > 0.5 ? "light" : "dark" );
return { mode, background: bg } const luminance = 0.299 * bg.r + 0.587 * bg.g + 0.114 * bg.b;
const mode = luminance > 0.5 ? "light" : "dark";
return { mode, background: bg };
} }
export function generateSystemTheme(colors: TerminalColors, mode: "dark" | "light"): ThemeJson { export function generateSystemTheme(
cached = colors colors: TerminalColors,
const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0] ?? "#000000") mode: "dark" | "light",
const fg = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7] ?? "#ffffff") ): ThemeJson {
const transparent = RGBA.fromInts(0, 0, 0, 0) cached = colors;
const isDark = mode === "dark" const bg = RGBA.fromHex(
colors.defaultBackground ?? colors.palette[0] ?? "#000000",
);
const fg = RGBA.fromHex(
colors.defaultForeground ?? colors.palette[7] ?? "#ffffff",
);
const transparent = RGBA.fromInts(0, 0, 0, 0);
const isDark = mode === "dark";
const col = (i: number) => { const col = (i: number) => {
const value = colors.palette[i] const value = colors.palette[i];
if (value) return RGBA.fromHex(value) if (value) return RGBA.fromHex(value);
return ansiToRgba(i) return ansiToRgba(i);
} };
const grays = generateGrayScale(bg, isDark) const grays = generateGrayScale(bg, isDark);
const textMuted = generateMutedTextColor(bg, isDark) const textMuted = generateMutedTextColor(bg, isDark);
const ansi = { const ansi = {
black: col(0), black: col(0),
@@ -43,13 +56,13 @@ export function generateSystemTheme(colors: TerminalColors, mode: "dark" | "ligh
white: col(7), white: col(7),
redBright: col(9), redBright: col(9),
greenBright: col(10), greenBright: col(10),
} };
const diffAlpha = isDark ? 0.22 : 0.14 const diffAlpha = isDark ? 0.22 : 0.14;
const diffAddedBg = tint(bg, ansi.green, diffAlpha) const diffAddedBg = tint(bg, ansi.green, diffAlpha);
const diffRemovedBg = tint(bg, ansi.red, diffAlpha) const diffRemovedBg = tint(bg, ansi.red, diffAlpha);
const diffAddedLineNumberBg = tint(grays[3], ansi.green, diffAlpha) const diffAddedLineNumberBg = tint(grays[3], ansi.green, diffAlpha);
const diffRemovedLineNumberBg = tint(grays[3], ansi.red, diffAlpha) const diffRemovedLineNumberBg = tint(grays[3], ansi.red, diffAlpha);
return { return {
theme: { theme: {
@@ -68,7 +81,7 @@ export function generateSystemTheme(colors: TerminalColors, mode: "dark" | "ligh
backgroundElement: grays[3], backgroundElement: grays[3],
backgroundMenu: grays[3], backgroundMenu: grays[3],
borderSubtle: grays[6], borderSubtle: grays[6],
border: grays[7], border: fg,
borderActive: grays[8], borderActive: grays[8],
diffAdded: ansi.green, diffAdded: ansi.green,
diffRemoved: ansi.red, diffRemoved: ansi.red,
@@ -106,5 +119,5 @@ export function generateSystemTheme(colors: TerminalColors, mode: "dark" | "ligh
syntaxOperator: ansi.cyan, syntaxOperator: ansi.cyan,
syntaxPunctuation: fg, syntaxPunctuation: fg,
}, },
} };
} }