starting janitorial work
This commit is contained in:
92
src/App.tsx
92
src/App.tsx
@@ -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)
|
||||||
|
.then(() => {
|
||||||
emit("toast.show", {
|
emit("toast.show", {
|
||||||
message: "Copied to clipboard",
|
message: "Copied to clipboard",
|
||||||
variant: "info",
|
variant: "info",
|
||||||
duration: 1500,
|
duration: 1500,
|
||||||
|
});
|
||||||
})
|
})
|
||||||
}).catch(() => {})
|
.catch(() => {});
|
||||||
})
|
});
|
||||||
|
|
||||||
const getPanels = createMemo(() => {
|
const getPanels = createMemo(() => {
|
||||||
const tab = activeTab();
|
const tab = activeTab();
|
||||||
@@ -156,7 +164,8 @@ export function App() {
|
|||||||
if (showAuthPanel()) {
|
if (showAuthPanel()) {
|
||||||
if (auth.isAuthenticated) {
|
if (auth.isAuthenticated) {
|
||||||
return {
|
return {
|
||||||
panels: [{
|
panels: [
|
||||||
|
{
|
||||||
title: "Account",
|
title: "Account",
|
||||||
content: (
|
content: (
|
||||||
<SyncProfile
|
<SyncProfile
|
||||||
@@ -168,7 +177,8 @@ export function App() {
|
|||||||
onManageSync={() => setShowAuthPanel(false)}
|
onManageSync={() => setShowAuthPanel(false)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
activePanelIndex: 0,
|
activePanelIndex: 0,
|
||||||
hint: "Esc back",
|
hint: "Esc back",
|
||||||
};
|
};
|
||||||
@@ -203,17 +213,20 @@ export function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
panels: [{
|
panels: [
|
||||||
|
{
|
||||||
title: "Sign In",
|
title: "Sign In",
|
||||||
content: authContent(),
|
content: authContent(),
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
activePanelIndex: 0,
|
activePanelIndex: 0,
|
||||||
hint: "Esc back",
|
hint: "Esc back",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
panels: [{
|
panels: [
|
||||||
|
{
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
content: (
|
content: (
|
||||||
<SettingsScreen
|
<SettingsScreen
|
||||||
@@ -223,18 +236,22 @@ export function App() {
|
|||||||
? `Signed in as ${auth.user?.email}`
|
? `Signed in as ${auth.user?.email}`
|
||||||
: "Not signed in"
|
: "Not signed in"
|
||||||
}
|
}
|
||||||
accountStatus={auth.isAuthenticated ? "signed-in" : "signed-out"}
|
accountStatus={
|
||||||
|
auth.isAuthenticated ? "signed-in" : "signed-out"
|
||||||
|
}
|
||||||
onExit={() => setLayerDepth(0)}
|
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",
|
title: "Discover",
|
||||||
content: (
|
content: (
|
||||||
<DiscoverPage
|
<DiscoverPage
|
||||||
@@ -242,14 +259,16 @@ export function App() {
|
|||||||
onExit={() => setLayerDepth(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",
|
title: "Search",
|
||||||
content: (
|
content: (
|
||||||
<SearchPage
|
<SearchPage
|
||||||
@@ -274,33 +293,41 @@ export function App() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
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",
|
title: "Player",
|
||||||
content: (
|
content: (
|
||||||
<Player focused={layerDepth() > 0} onExit={() => setLayerDepth(0)} />
|
<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,
|
title: tab,
|
||||||
content: (
|
content: (
|
||||||
<box padding={2}>
|
<box padding={2}>
|
||||||
<text>Coming soon</text>
|
<text>Coming soon</text>
|
||||||
</box>
|
</box>
|
||||||
),
|
),
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
activePanelIndex: 0,
|
activePanelIndex: 0,
|
||||||
hint: "",
|
hint: "",
|
||||||
};
|
};
|
||||||
@@ -308,24 +335,21 @@ export function App() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary fallback={(err) => (
|
<ErrorBoundary
|
||||||
|
fallback={(err) => (
|
||||||
<box border padding={2}>
|
<box border padding={2}>
|
||||||
<text fg="red">
|
<text fg="red">
|
||||||
Error: {err?.message ?? String(err)}{"\n"}
|
Error: {err?.message ?? String(err)}
|
||||||
|
{"\n"}
|
||||||
Press a number key (1-6) to switch tabs.
|
Press a number key (1-6) to switch tabs.
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user