getting terminal colors working

This commit is contained in:
2026-02-05 13:46:47 -05:00
parent 9fa52d71ca
commit e239b33042
45 changed files with 1718 additions and 2055 deletions

View File

@@ -1,4 +1,5 @@
import { createSignal } from "solid-js"; import { createSignal } from "solid-js";
import { useRenderer } from "@opentui/solid";
import { Layout } from "./components/Layout"; import { Layout } from "./components/Layout";
import { Navigation } from "./components/Navigation"; import { Navigation } from "./components/Navigation";
import { TabNavigation } from "./components/TabNavigation"; import { TabNavigation } from "./components/TabNavigation";
@@ -32,31 +33,31 @@ export function App() {
// Centralized keyboard handler for all tab navigation and shortcuts // Centralized keyboard handler for all tab navigation and shortcuts
useAppKeyboard({ useAppKeyboard({
get activeTab() { get activeTab() {
return activeTab() return activeTab();
}, },
onTabChange: setActiveTab, onTabChange: setActiveTab,
inputFocused: inputFocused(), inputFocused: inputFocused(),
navigationEnabled: layerDepth() === 0, navigationEnabled: layerDepth() === 0,
layerDepth, layerDepth,
onLayerChange: (newDepth) => { onLayerChange: (newDepth) => {
setLayerDepth(newDepth) setLayerDepth(newDepth);
}, },
onAction: (action) => { onAction: (action) => {
if (action === "escape") { if (action === "escape") {
if (layerDepth() > 0) { if (layerDepth() > 0) {
setLayerDepth(0) setLayerDepth(0);
setInputFocused(false) setInputFocused(false);
} else { } else {
setShowAuthPanel(false) setShowAuthPanel(false);
setInputFocused(false) setInputFocused(false);
} }
} }
if (action === "enter" && layerDepth() === 0) { if (action === "enter" && layerDepth() === 0) {
setLayerDepth(1) setLayerDepth(1);
} }
}, },
}) });
const renderContent = () => { const renderContent = () => {
const tab = activeTab(); const tab = activeTab();

View File

@@ -1,5 +1,6 @@
import type { JSX } from "solid-js" import type { JSX } from "solid-js"
import type { RGBA } from "@opentui/core" import type { RGBA } from "@opentui/core"
import { Show, createMemo } from "solid-js"
import { useTheme } from "../context/ThemeContext" import { useTheme } from "../context/ThemeContext"
import { LayerIndicator } from "./LayerIndicator" import { LayerIndicator } from "./LayerIndicator"
@@ -16,52 +17,50 @@ type LayoutProps = {
} }
export function Layout(props: LayoutProps) { export function Layout(props: LayoutProps) {
const { theme } = useTheme() const context = useTheme()
// Get layer configuration based on depth // Get layer configuration based on depth - wrapped in createMemo for reactivity
const getLayerConfig = (depth: number): LayerConfig => { const currentLayer = createMemo((): LayerConfig => {
const backgrounds = theme.layerBackgrounds const depth = props.layerDepth || 0
const backgrounds = context.theme.layerBackgrounds
const depthMap: Record<number, LayerConfig> = { const depthMap: Record<number, LayerConfig> = {
0: { depth: 0, background: backgrounds?.layer0 ?? theme.background }, 0: { depth: 0, background: backgrounds?.layer0 ?? context.theme.background },
1: { depth: 1, background: backgrounds?.layer1 ?? theme.backgroundPanel }, 1: { depth: 1, background: backgrounds?.layer1 ?? context.theme.backgroundPanel },
2: { depth: 2, background: backgrounds?.layer2 ?? theme.backgroundElement }, 2: { depth: 2, background: backgrounds?.layer2 ?? context.theme.backgroundElement },
3: { depth: 3, background: backgrounds?.layer3 ?? theme.backgroundMenu }, 3: { depth: 3, background: backgrounds?.layer3 ?? context.theme.backgroundMenu },
} }
return depthMap[depth] || { depth: 0, background: theme.background } return depthMap[depth] || { depth: 0, background: context.theme.background }
} })
// Get current layer background
const currentLayer = getLayerConfig(props.layerDepth || 0)
// Note: No need for a ready check here - the ThemeProvider uses
// createSimpleContext which gates children rendering until ready
return ( return (
<box <box
flexDirection="column" flexDirection="column"
width="100%" width="100%"
height="100%" height="100%"
backgroundColor={theme.background} backgroundColor={context.theme.background}
> >
{/* Header */} {/* Header */}
{props.header ? ( <Show when={props.header} fallback={<box style={{ height: 4 }} />}>
<box <box
style={{ style={{
height: 4, height: 4,
backgroundColor: theme.surface ?? theme.backgroundPanel, backgroundColor: context.theme.surface ?? context.theme.backgroundPanel,
}} }}
> >
<box style={{ padding: 1 }}> <box style={{ padding: 1 }}>
{props.header} {props.header}
</box> </box>
</box> </box>
) : ( </Show>
<box style={{ height: 4 }} />
)}
{/* Main content area with layer background */} {/* Main content area with layer background */}
<box <box
style={{ style={{
flexGrow: 1, flexGrow: 1,
backgroundColor: currentLayer.background, backgroundColor: currentLayer().background,
paddingLeft: 2, paddingLeft: 2,
paddingRight: 2, paddingRight: 2,
}} }}
@@ -72,34 +71,32 @@ export function Layout(props: LayoutProps) {
</box> </box>
{/* Footer */} {/* Footer */}
{props.footer ? ( <Show when={props.footer} fallback={<box style={{ height: 2 }} />}>
<box <box
style={{ style={{
height: 2, height: 2,
backgroundColor: theme.surface ?? theme.backgroundPanel, backgroundColor: context.theme.surface ?? context.theme.backgroundPanel,
}} }}
> >
<box style={{ padding: 1 }}> <box style={{ padding: 1 }}>
{props.footer} {props.footer}
</box> </box>
</box> </box>
) : ( </Show>
<box style={{ height: 2 }} />
)}
{/* Layer indicator */} {/* Layer indicator */}
{props.layerDepth !== undefined && ( <Show when={props.layerDepth !== undefined}>
<box <box
style={{ style={{
height: 1, height: 1,
backgroundColor: theme.surface ?? theme.backgroundPanel, backgroundColor: context.theme.surface ?? context.theme.backgroundPanel,
}} }}
> >
<box style={{ padding: 1 }}> <box style={{ padding: 1 }}>
<LayerIndicator layerDepth={props.layerDepth} /> <LayerIndicator layerDepth={props.layerDepth as number} />
</box> </box>
</box> </box>
)} </Show>
</box> </box>
) )
} }

View File

@@ -0,0 +1,129 @@
import { createMemo } from "solid-js"
import type { ParsedKey, Renderable } from "@opentui/core"
import { createStore } from "solid-js/store"
import { useKeyboard, useRenderer } from "@opentui/solid"
import { createSimpleContext } from "./helper"
import { Keybind, DEFAULT_KEYBINDS, type KeybindsConfig } from "../utils/keybind"
/**
* Keybind context provider for managing keyboard shortcuts.
*
* Features:
* - Leader key support (like vim's leader key)
* - Configurable keybindings
* - Key parsing and matching
* - Display-friendly key representations
*/
export const { use: useKeybind, provider: KeybindProvider } = createSimpleContext({
name: "Keybind",
init: (props: { keybinds?: Partial<KeybindsConfig> }) => {
// Merge default keybinds with custom keybinds
const customKeybinds = props.keybinds ?? {}
const mergedKeybinds = { ...DEFAULT_KEYBINDS, ...customKeybinds }
const keybinds = createMemo(() => {
const result: Record<string, Keybind.Info[]> = {}
for (const [key, value] of Object.entries(mergedKeybinds)) {
result[key] = Keybind.parse(value)
}
return result
})
const [store, setStore] = createStore({
leader: false,
})
const renderer = useRenderer()
let focus: Renderable | null = null
let timeout: NodeJS.Timeout | undefined
function leader(active: boolean) {
if (active) {
setStore("leader", true)
focus = renderer.currentFocusedRenderable
focus?.blur()
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
if (!store.leader) return
leader(false)
if (!focus || focus.isDestroyed) return
focus.focus()
}, 2000) // Leader key timeout
return
}
if (!active) {
if (focus && !renderer.currentFocusedRenderable) {
focus.focus()
}
setStore("leader", false)
}
}
// Handle leader key
useKeyboard(async (evt) => {
if (!store.leader && result.match("leader", evt)) {
leader(true)
return
}
if (store.leader && evt.name) {
setImmediate(() => {
if (focus && renderer.currentFocusedRenderable === focus) {
focus.focus()
}
leader(false)
})
}
})
const result = {
get all() {
return keybinds()
},
get leader() {
return store.leader
},
/**
* Parse a keyboard event into a Keybind.Info.
*/
parse(evt: ParsedKey): Keybind.Info {
// Handle special case for Ctrl+Underscore (represented as \x1F)
if (evt.name === "\x1F") {
return Keybind.fromParsedKey({ ...evt, name: "_", ctrl: true }, store.leader)
}
return Keybind.fromParsedKey(evt, store.leader)
},
/**
* Check if a keyboard event matches a registered keybind.
*/
match(key: keyof KeybindsConfig, evt: ParsedKey): boolean {
const keybind = keybinds()[key]
if (!keybind) return false
const parsed: Keybind.Info = result.parse(evt)
for (const kb of keybind) {
if (Keybind.match(kb, parsed)) {
return true
}
}
return false
},
/**
* Get a display string for a registered keybind.
*/
print(key: keyof KeybindsConfig): string {
const first = keybinds()[key]?.at(0)
if (!first) return ""
const display = Keybind.toString(first)
// Replace leader placeholder with actual leader key
const leaderKey = keybinds().leader?.[0]
if (leaderKey) {
return display.replace("<leader>", Keybind.toString(leaderKey))
}
return display
},
}
return result
},
})

View File

@@ -1,13 +1,14 @@
import { createContext, createEffect, createMemo, createSignal, Show, useContext } 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 { resolveTheme } from "../utils/theme-resolver"
import { generateSyntax, generateSubtleSyntax } from "../utils/syntax-highlighter" import { generateSyntax, generateSubtleSyntax } from "../utils/syntax-highlighter"
import { resolveTerminalTheme, loadThemes } from "../utils/theme" import { resolveTerminalTheme, loadThemes } from "../utils/theme"
import { createSimpleContext } from "./helper"
import { setupThemeSignalHandler, emitThemeChanged, emitThemeModeChanged } from "../utils/theme-observer"
import type { RGBA, TerminalColors } from "@opentui/core" import type { RGBA, TerminalColors } from "@opentui/core"
type ThemeResolved = { type ThemeResolved = {
@@ -75,32 +76,31 @@ type ThemeResolved = {
thinkingOpacity?: number thinkingOpacity?: number
} }
type ThemeContextValue = { /**
theme: ThemeResolved * Theme context using the createSimpleContext pattern.
selected: () => string *
all: () => Record<string, ThemeJson> * This ensures children are NOT rendered until the theme is ready,
syntax: () => unknown * preventing "useTheme must be used within a ThemeProvider" errors.
subtleSyntax: () => unknown *
mode: () => "dark" | "light" * The key insight from opencode's implementation is that the provider
setMode: (mode: "dark" | "light") => void * uses `<Show when={ready}>` to gate rendering, so components can
set: (theme: string) => void * safely call useTheme() without checking ready state.
ready: () => boolean */
} export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
name: "Theme",
const ThemeContext = createContext<ThemeContextValue>() init: (props: { mode: "dark" | "light" }) => {
export function ThemeProvider({ children }: { children: any }) {
const appStore = useAppStore() const appStore = useAppStore()
const renderer = useRenderer() const renderer = useRenderer()
const [ready, setReady] = createSignal(false)
const [store, setStore] = createStore({ const [store, setStore] = createStore({
themes: {} as Record<string, ThemeJson>, themes: THEME_JSON as Record<string, ThemeJson>,
mode: "dark" as "dark" | "light", mode: props.mode,
active: appStore.state().settings.theme as ThemeName, active: appStore.state().settings.theme as string,
system: undefined as undefined | TerminalColors, system: undefined as undefined | TerminalColors,
ready: false,
}) })
const init = () => { function init() {
resolveSystemTheme()
loadThemes() loadThemes()
.then((custom) => { .then((custom) => {
setStore( setStore(
@@ -109,25 +109,86 @@ export function ThemeProvider({ children }: { children: any }) {
}) })
) )
}) })
.finally(() => setReady(true)) .catch(() => {
// If custom themes fail to load, fall back to opencode theme
setStore("active", "opencode")
})
.finally(() => {
// Only set ready if not waiting for system theme
if (store.active !== "system") {
setStore("ready", true)
}
})
} }
init() function resolveSystemTheme() {
createEffect(() => {
setStore("active", appStore.state().settings.theme)
})
createEffect(() => {
renderer renderer
.getPalette({ size: 16 }) .getPalette({ size: 16 })
.then((colors) => setStore("system", colors)) .then((colors) => {
.catch(() => {}) if (!colors.palette[0]) {
// No system colors available, fall back to default
// This happens when the terminal doesn't support OSC palette queries
// (e.g., running inside tmux, or on unsupported terminals)
if (store.active === "system") {
setStore(
produce((draft) => {
draft.active = "opencode"
draft.ready = true
})
)
}
return
}
setStore(
produce((draft) => {
draft.system = colors
if (store.active === "system") {
draft.ready = true
}
})
)
})
.catch(() => {
// On error, fall back to default theme if using system
if (store.active === "system") {
setStore(
produce((draft) => {
draft.active = "opencode"
draft.ready = true
})
)
}
})
}
onMount(init)
// Setup SIGUSR2 signal handler for dynamic theme reload
// This allows external tools to trigger a theme refresh by sending:
// `kill -USR2 <pid>`
const cleanupSignalHandler = setupThemeSignalHandler(() => {
renderer.clearPaletteCache()
init()
})
onCleanup(cleanupSignalHandler)
// Sync active theme with app store settings
createEffect(() => {
const theme = appStore.state().settings.theme
if (theme) setStore("active", theme)
})
// Emit theme change events for observers
createEffect(() => {
const theme = store.active
const mode = store.mode
if (store.ready) {
emitThemeChanged(theme, mode)
}
}) })
const values = createMemo(() => { const values = createMemo(() => {
const themes = Object.keys(store.themes).length ? store.themes : THEME_JSON return resolveTerminalTheme(store.themes, store.active, store.mode, store.system)
return resolveTerminalTheme(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>))
@@ -135,33 +196,34 @@ export function ThemeProvider({ children }: { children: any }) {
generateSubtleSyntax(values() as unknown as Record<string, RGBA> & { thinkingOpacity?: number }) generateSubtleSyntax(values() as unknown as Record<string, RGBA> & { thinkingOpacity?: number })
) )
const context: ThemeContextValue = { return {
theme: new Proxy(values(), { theme: new Proxy(values(), {
get(_target, prop) { get(_target, prop) {
return values()[prop as keyof typeof values] // @ts-expect-error - dynamic property access
return values()[prop]
}, },
}) as ThemeResolved, }) as ThemeResolved,
selected: () => store.active, get selected() {
all: () => store.themes, return store.active
},
all() {
return store.themes
},
syntax, syntax,
subtleSyntax, subtleSyntax,
mode: () => store.mode, mode() {
setMode: (mode) => setStore("mode", mode), return store.mode
set: (theme) => appStore.setTheme(theme as ThemeName), },
ready, setMode(mode: "dark" | "light") {
} setStore("mode", mode)
emitThemeModeChanged(mode)
return ( },
<Show when={ready()}> set(theme: string) {
<ThemeContext.Provider value={context}>{children}</ThemeContext.Provider> appStore.setTheme(theme as ThemeName)
</Show> },
) get ready() {
} return store.ready
},
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider")
}
return context
} }
},
})

53
src/context/helper.tsx Normal file
View File

@@ -0,0 +1,53 @@
import { createContext, Show, useContext, type ParentProps } from "solid-js"
/**
* Creates a simple context with automatic ready-state handling.
*
* This pattern ensures that child components are NOT rendered until the
* context's `ready` property is true (or undefined, meaning no ready check needed).
*
* This prevents the "useX must be used within a XProvider" errors that occur
* when child components try to use context values before the provider has
* finished async initialization.
*
* Usage:
* ```tsx
* export const { use: useMyContext, provider: MyProvider } = createSimpleContext({
* name: "MyContext",
* init: (props: { someProp: string }) => {
* const [ready, setReady] = createSignal(false)
* // ... async initialization ...
* return {
* get ready() { return ready() },
* // ... other values
* }
* },
* })
* ```
*/
export function createSimpleContext<T, Props extends Record<string, any>>(input: {
name: string
init: ((input: Props) => T) | (() => T)
}) {
const ctx = createContext<T>()
return {
provider: (props: ParentProps<Props>) => {
const init = input.init(props)
// Use an arrow function accessor for the ready check to maintain reactivity.
// The getter `init.ready` reads from a store, so wrapping it in an
// accessor allows Solid to track changes reactively.
return (
// @ts-expect-error - ready may not exist on all context types
<Show when={init.ready === undefined || init.ready}>
<ctx.Provider value={init}>{props.children}</ctx.Provider>
</Show>
)
},
use() {
const value = useContext(ctx)
if (!value) throw new Error(`${input.name} context must be used within a context provider`)
return value
},
}
}

View File

@@ -1,10 +1,22 @@
import { render } from "@opentui/solid" import { render } from "@opentui/solid"
import { App } from "./App" import { App } from "./App"
import { ThemeProvider } from "./context/ThemeContext" import { ThemeProvider } from "./context/ThemeContext"
import "./styles/theme.css" import { ToastProvider, Toast } from "./ui/toast"
import { KeybindProvider } from "./context/KeybindContext"
import { DialogProvider } from "./ui/dialog"
import { CommandProvider } from "./ui/command"
render(() => ( render(() => (
<ThemeProvider> <ToastProvider>
<ThemeProvider mode="dark">
<KeybindProvider>
<DialogProvider>
<CommandProvider>
<App /> <App />
<Toast />
</CommandProvider>
</DialogProvider>
</KeybindProvider>
</ThemeProvider> </ThemeProvider>
</ToastProvider>
)) ))

View File

@@ -1,7 +1,7 @@
import type { RGBA } from "@opentui/core" import type { RGBA } from "@opentui/core"
import type { ColorValue, ThemeJson, Variant } from "./theme-schema" import type { ColorValue, ThemeJson, Variant } from "./theme-schema"
export type ThemeName = "system" | "catppuccin" | "gruvbox" | "tokyo" | "nord" | "custom" export type ThemeName = "system" | "opencode" | "catppuccin" | "gruvbox" | "tokyo" | "nord" | "custom"
export type LayerBackgrounds = { export type LayerBackgrounds = {
layer0: ColorValue layer0: ColorValue

307
src/ui/command.tsx Normal file
View File

@@ -0,0 +1,307 @@
import {
createContext,
createMemo,
createSignal,
onCleanup,
useContext,
type Accessor,
type ParentProps,
For,
Show,
} from "solid-js"
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
import { useKeybind } from "../context/KeybindContext"
import { useDialog } from "./dialog"
import { useTheme } from "../context/ThemeContext"
import type { KeybindsConfig } from "../utils/keybind"
import { TextAttributes } from "@opentui/core"
import { emit } from "../utils/event-bus"
/**
* Command option for the command palette.
*/
export type CommandOption = {
/** Display title */
title: string
/** Unique identifier */
value: string
/** Description shown below title */
description?: string
/** Category for grouping */
category?: string
/** Keybind reference */
keybind?: keyof KeybindsConfig
/** Whether this command is suggested */
suggested?: boolean
/** Slash command configuration */
slash?: {
name: string
aliases?: string[]
}
/** Whether to hide from command list */
hidden?: boolean
/** Whether command is enabled */
enabled?: boolean
/** Footer text (usually keybind display) */
footer?: string
/** Handler when command is selected */
onSelect?: (dialog: ReturnType<typeof useDialog>) => void
}
type CommandContext = ReturnType<typeof init>
const ctx = createContext<CommandContext>()
function init() {
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
const [suspendCount, setSuspendCount] = createSignal(0)
const dialog = useDialog()
const keybind = useKeybind()
const entries = createMemo(() => {
const all = registrations().flatMap((x) => x())
return all.map((x) => ({
...x,
footer: x.keybind ? keybind.print(x.keybind) : undefined,
}))
})
const isEnabled = (option: CommandOption) => option.enabled !== false
const isVisible = (option: CommandOption) => isEnabled(option) && !option.hidden
const visibleOptions = createMemo(() => entries().filter((option) => isVisible(option)))
const suggestedOptions = createMemo(() =>
visibleOptions()
.filter((option) => option.suggested)
.map((option) => ({
...option,
value: `suggested:${option.value}`,
category: "Suggested",
})),
)
const suspended = () => suspendCount() > 0
// Handle keybind shortcuts
useKeyboard((evt) => {
if (suspended()) return
if (dialog.isOpen) return
for (const option of entries()) {
if (!isEnabled(option)) continue
if (option.keybind && keybind.match(option.keybind, evt)) {
evt.preventDefault()
option.onSelect?.(dialog)
emit("command.execute", { command: option.value })
return
}
}
})
const result = {
/**
* Trigger a command by its value.
*/
trigger(name: string) {
for (const option of entries()) {
if (option.value === name) {
if (!isEnabled(option)) return
option.onSelect?.(dialog)
emit("command.execute", { command: name })
return
}
}
},
/**
* Get all slash commands.
*/
slashes() {
return visibleOptions().flatMap((option) => {
const slash = option.slash
if (!slash) return []
return {
display: "/" + slash.name,
description: option.description ?? option.title,
aliases: slash.aliases?.map((alias) => "/" + alias),
onSelect: () => result.trigger(option.value),
}
})
},
/**
* Enable/disable keybinds temporarily.
*/
keybinds(enabled: boolean) {
setSuspendCount((count) => count + (enabled ? -1 : 1))
},
suspended,
/**
* Show the command palette dialog.
*/
show() {
dialog.replace(() => <CommandDialog options={visibleOptions()} suggestedOptions={suggestedOptions()} />)
},
/**
* Register commands. Returns cleanup function.
*/
register(cb: () => CommandOption[]) {
const results = createMemo(cb)
setRegistrations((arr) => [results, ...arr])
onCleanup(() => {
setRegistrations((arr) => arr.filter((x) => x !== results))
})
},
/**
* Get all visible options.
*/
get options() {
return visibleOptions()
},
}
return result
}
export function useCommandDialog() {
const value = useContext(ctx)
if (!value) {
throw new Error("useCommandDialog must be used within a CommandProvider")
}
return value
}
export function CommandProvider(props: ParentProps) {
const value = init()
const dialog = useDialog()
const keybind = useKeybind()
// Open command palette on ctrl+p or command_list keybind
useKeyboard((evt) => {
if (value.suspended()) return
if (dialog.isOpen) return
if (evt.defaultPrevented) return
if (keybind.match("command_list", evt)) {
evt.preventDefault()
value.show()
return
}
})
return <ctx.Provider value={value}>{props.children}</ctx.Provider>
}
/**
* Command palette dialog component.
*/
function CommandDialog(props: { options: CommandOption[]; suggestedOptions: CommandOption[] }) {
const { theme } = useTheme()
const dialog = useDialog()
const dimensions = useTerminalDimensions()
const [filter, setFilter] = createSignal("")
const [selectedIndex, setSelectedIndex] = createSignal(0)
const filteredOptions = createMemo(() => {
const query = filter().toLowerCase()
if (!query) {
return [...props.suggestedOptions, ...props.options]
}
return props.options.filter(
(option) =>
option.title.toLowerCase().includes(query) ||
option.description?.toLowerCase().includes(query) ||
option.category?.toLowerCase().includes(query)
)
})
// Reset selection when filter changes
createMemo(() => {
filter()
setSelectedIndex(0)
})
useKeyboard((evt) => {
if (evt.name === "escape") {
dialog.clear()
evt.preventDefault()
return
}
if (evt.name === "return" || evt.name === "enter") {
const option = filteredOptions()[selectedIndex()]
if (option) {
option.onSelect?.(dialog)
dialog.clear()
}
evt.preventDefault()
return
}
if (evt.name === "up" || (evt.ctrl && evt.name === "p")) {
setSelectedIndex((i) => Math.max(0, i - 1))
evt.preventDefault()
return
}
if (evt.name === "down" || (evt.ctrl && evt.name === "n")) {
setSelectedIndex((i) => Math.min(filteredOptions().length - 1, i + 1))
evt.preventDefault()
return
}
// Handle text input
if (evt.name && evt.name.length === 1 && !evt.ctrl && !evt.meta) {
setFilter((f) => f + evt.name)
return
}
if (evt.name === "backspace") {
setFilter((f) => f.slice(0, -1))
return
}
})
const maxHeight = Math.floor(dimensions().height * 0.6)
return (
<box flexDirection="column" padding={1}>
{/* Search input */}
<box marginBottom={1}>
<text fg={theme.textMuted}>
{"> "}
</text>
<text fg={theme.text}>
{filter() || "Type to search commands..."}
</text>
</box>
{/* Command list */}
<box flexDirection="column" maxHeight={maxHeight}>
<For each={filteredOptions().slice(0, 10)}>
{(option, index) => (
<box
backgroundColor={index() === selectedIndex() ? theme.primary : undefined}
padding={1}
>
<box flexDirection="column" flexGrow={1}>
<box flexDirection="row" justifyContent="space-between">
<text
fg={index() === selectedIndex() ? theme.selectedListItemText : theme.text}
attributes={index() === selectedIndex() ? TextAttributes.BOLD : undefined}
>
{option.title}
</text>
<Show when={option.footer}>
<text fg={theme.textMuted}>{option.footer}</text>
</Show>
</box>
<Show when={option.description}>
<text fg={theme.textMuted}>{option.description}</text>
</Show>
</box>
</box>
)}
</For>
<Show when={filteredOptions().length === 0}>
<text fg={theme.textMuted} style={{ padding: 1 }}>
No commands found
</text>
</Show>
</box>
</box>
)
}

224
src/ui/dialog.tsx Normal file
View File

@@ -0,0 +1,224 @@
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
import { batch, createContext, Show, useContext, type JSX, type ParentProps } from "solid-js"
import { useTheme } from "../context/ThemeContext"
import { RGBA, Renderable } from "@opentui/core"
import { createStore } from "solid-js/store"
import { Clipboard } from "../utils/clipboard"
import { useToast } from "./toast"
import { emit } from "../utils/event-bus"
export type DialogSize = "medium" | "large"
/**
* Dialog component that renders a modal overlay with content.
*/
export function Dialog(
props: ParentProps<{
size?: DialogSize
onClose: () => void
}>,
) {
const dimensions = useTerminalDimensions()
const { theme } = useTheme()
const renderer = useRenderer()
return (
<box
onMouseUp={async () => {
if (renderer.getSelection()) return
props.onClose?.()
}}
width={dimensions().width}
height={dimensions().height}
alignItems="center"
position="absolute"
paddingTop={Math.floor(dimensions().height / 4)}
left={0}
top={0}
backgroundColor={RGBA.fromInts(0, 0, 0, 150)}
>
<box
onMouseUp={async (e) => {
if (renderer.getSelection()) return
e.stopPropagation()
}}
width={props.size === "large" ? 80 : 60}
maxWidth={dimensions().width - 2}
backgroundColor={theme.backgroundPanel}
paddingTop={1}
>
{props.children}
</box>
</box>
)
}
type DialogStackItem = {
element: JSX.Element
onClose?: () => void
}
function init() {
const [store, setStore] = createStore({
stack: [] as DialogStackItem[],
size: "medium" as DialogSize,
})
const renderer = useRenderer()
let focus: Renderable | null = null
function refocus() {
setTimeout(() => {
if (!focus) return
if (focus.isDestroyed) return
function find(item: Renderable): boolean {
for (const child of item.getChildren()) {
if (child === focus) return true
if (find(child)) return true
}
return false
}
const found = find(renderer.root)
if (!found) return
focus.focus()
}, 1)
}
useKeyboard((evt) => {
if (evt.name === "escape" && store.stack.length > 0) {
const current = store.stack.at(-1)!
current.onClose?.()
setStore("stack", store.stack.slice(0, -1))
evt.preventDefault()
evt.stopPropagation()
refocus()
emit("dialog.close", {})
}
})
return {
/**
* Clear all dialogs from the stack.
*/
clear() {
for (const item of store.stack) {
if (item.onClose) item.onClose()
}
batch(() => {
setStore("size", "medium")
setStore("stack", [])
})
refocus()
emit("dialog.close", {})
},
/**
* Replace all dialogs with a new one.
*/
replace(input: JSX.Element | (() => JSX.Element), onClose?: () => void) {
if (store.stack.length === 0) {
focus = renderer.currentFocusedRenderable
focus?.blur()
}
for (const item of store.stack) {
if (item.onClose) item.onClose()
}
const element = typeof input === "function" ? input() : input
setStore("size", "medium")
setStore("stack", [{ element, onClose }])
emit("dialog.open", { dialogId: "dialog" })
},
/**
* Push a new dialog onto the stack.
*/
push(input: JSX.Element | (() => JSX.Element), onClose?: () => void) {
if (store.stack.length === 0) {
focus = renderer.currentFocusedRenderable
focus?.blur()
}
const element = typeof input === "function" ? input() : input
setStore("stack", [...store.stack, { element, onClose }])
emit("dialog.open", { dialogId: "dialog" })
},
/**
* Pop the top dialog from the stack.
*/
pop() {
if (store.stack.length === 0) return
const current = store.stack.at(-1)!
current.onClose?.()
setStore("stack", store.stack.slice(0, -1))
if (store.stack.length === 0) {
refocus()
}
emit("dialog.close", {})
},
get stack() {
return store.stack
},
get size() {
return store.size
},
setSize(size: DialogSize) {
setStore("size", size)
},
get isOpen() {
return store.stack.length > 0
},
}
}
export type DialogContext = ReturnType<typeof init>
const ctx = createContext<DialogContext>()
/**
* DialogProvider wraps the application and provides dialog functionality.
* Also handles clipboard copy on text selection within dialogs.
*/
export function DialogProvider(props: ParentProps) {
const value = init()
const renderer = useRenderer()
const toast = useToast()
return (
<ctx.Provider value={value}>
{props.children}
<box
position="absolute"
onMouseUp={async () => {
const text = renderer.getSelection()?.getSelectedText()
if (text && text.length > 0) {
await Clipboard.copy(text)
.then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
.catch(toast.error)
renderer.clearSelection()
}
}}
>
<Show when={value.stack.length > 0}>
<Dialog onClose={() => value.clear()} size={value.size}>
{value.stack.at(-1)!.element}
</Dialog>
</Show>
</box>
</ctx.Provider>
)
}
/**
* Hook to access the dialog context.
*/
export function useDialog() {
const value = useContext(ctx)
if (!value) {
throw new Error("useDialog must be used within a DialogProvider")
}
return value
}

153
src/ui/toast.tsx Normal file
View File

@@ -0,0 +1,153 @@
import { createContext, useContext, type ParentProps, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { useTheme } from "../context/ThemeContext"
import { useTerminalDimensions } from "@opentui/solid"
import { TextAttributes } from "@opentui/core"
import { emit } from "../utils/event-bus"
export type ToastVariant = "info" | "success" | "warning" | "error"
export type ToastOptions = {
title?: string
message: string
variant: ToastVariant
duration?: number
}
const DEFAULT_DURATION = 5000
/**
* Toast component that displays at the top-right of the screen.
* NOTE: This component must be rendered INSIDE ThemeProvider since it uses useTheme().
* The ToastProvider itself can be placed outside ThemeProvider if needed.
*/
export function Toast() {
const toast = useToast()
const { theme } = useTheme()
const dimensions = useTerminalDimensions()
const getVariantColor = (variant: ToastVariant) => {
switch (variant) {
case "success":
return theme.success
case "warning":
return theme.warning
case "error":
return theme.error
case "info":
default:
return theme.info
}
}
return (
<Show when={toast.currentToast}>
{(current) => (
<box
position="absolute"
justifyContent="center"
alignItems="flex-start"
top={2}
right={2}
maxWidth={Math.min(60, dimensions().width - 6)}
paddingLeft={2}
paddingRight={2}
paddingTop={1}
paddingBottom={1}
backgroundColor={theme.backgroundPanel}
borderColor={getVariantColor(current().variant)}
border={["left", "right"]}
>
<box flexDirection="column">
<Show when={current().title}>
<text attributes={TextAttributes.BOLD} style={{ marginBottom: 1 }} fg={theme.text}>
{current().title}
</text>
</Show>
<text fg={theme.text} wrapMode="word" width="100%">
{current().message}
</text>
</box>
</box>
)}
</Show>
)
}
function init() {
const [store, setStore] = createStore({
currentToast: null as ToastOptions | null,
})
let timeoutHandle: NodeJS.Timeout | null = null
const toast = {
show(options: ToastOptions) {
const duration = options.duration ?? DEFAULT_DURATION
setStore("currentToast", {
title: options.title,
message: options.message,
variant: options.variant,
})
// Emit event for other listeners
emit("toast.show", options)
if (timeoutHandle) clearTimeout(timeoutHandle)
timeoutHandle = setTimeout(() => {
setStore("currentToast", null)
}, duration)
},
error: (err: unknown) => {
if (err instanceof Error) {
return toast.show({
variant: "error",
message: err.message,
})
}
toast.show({
variant: "error",
message: "An unknown error has occurred",
})
},
info: (message: string, title?: string) => {
toast.show({ variant: "info", message, title })
},
success: (message: string, title?: string) => {
toast.show({ variant: "success", message, title })
},
warning: (message: string, title?: string) => {
toast.show({ variant: "warning", message, title })
},
clear: () => {
if (timeoutHandle) clearTimeout(timeoutHandle)
setStore("currentToast", null)
},
get currentToast(): ToastOptions | null {
return store.currentToast
},
}
return toast
}
export type ToastContext = ReturnType<typeof init>
const ctx = createContext<ToastContext>()
/**
* ToastProvider provides toast functionality.
* NOTE: The Toast UI component is NOT rendered here - you must render <Toast />
* separately inside your component tree, after ThemeProvider.
*/
export function ToastProvider(props: ParentProps) {
const value = init()
return <ctx.Provider value={value}>{props.children}</ctx.Provider>
}
export function useToast() {
const value = useContext(ctx)
if (!value) {
throw new Error("useToast must be used within a ToastProvider")
}
return value
}

221
src/utils/clipboard.ts Normal file
View File

@@ -0,0 +1,221 @@
import { $ } from "bun"
import { platform, release } from "os"
import { tmpdir } from "os"
import path from "path"
/**
* Writes text to clipboard via OSC 52 escape sequence.
* This allows clipboard operations to work over SSH by having
* the terminal emulator handle the clipboard locally.
*/
function writeOsc52(text: string): void {
if (!process.stdout.isTTY) return
const base64 = Buffer.from(text).toString("base64")
const osc52 = `\x1b]52;c;${base64}\x07`
const passthrough = process.env["TMUX"] || process.env["STY"]
const sequence = passthrough ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52
process.stdout.write(sequence)
}
/**
* Lazy initialization for clipboard copy method.
* Detects the best clipboard method for the current platform.
*/
function createLazy<T>(factory: () => T): () => T {
let value: T | undefined
return () => {
if (value === undefined) {
value = factory()
}
return value
}
}
export namespace Clipboard {
export interface Content {
data: string
mime: string
}
/**
* Read content from the clipboard.
* Supports text and image (PNG) content on macOS, Windows, and Linux.
*/
export async function read(): Promise<Content | undefined> {
const os = platform()
// macOS: Try to read PNG image first
if (os === "darwin") {
const tmpfile = path.join(tmpdir(), "podtui-clipboard.png")
try {
await $`osascript -e 'set imageData to the clipboard as "PNGf"' -e 'set fileRef to open for access POSIX file "${tmpfile}" with write permission' -e 'set eof fileRef to 0' -e 'write imageData to fileRef' -e 'close access fileRef'`
.nothrow()
.quiet()
const file = Bun.file(tmpfile)
const buffer = await file.arrayBuffer()
if (buffer.byteLength > 0) {
return { data: Buffer.from(buffer).toString("base64"), mime: "image/png" }
}
} catch {
// Ignore errors, fall through to text
} finally {
await $`rm -f "${tmpfile}"`.nothrow().quiet()
}
}
// Windows/WSL: Try to read PNG image
if (os === "win32" || release().includes("WSL")) {
const script =
"Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); [System.Convert]::ToBase64String($ms.ToArray()) }"
const base64 = await $`powershell.exe -NonInteractive -NoProfile -command "${script}"`.nothrow().text()
if (base64) {
const imageBuffer = Buffer.from(base64.trim(), "base64")
if (imageBuffer.length > 0) {
return { data: imageBuffer.toString("base64"), mime: "image/png" }
}
}
}
// Linux: Try Wayland or X11
if (os === "linux") {
// Try Wayland first
const wayland = await $`wl-paste -t image/png`.nothrow().arrayBuffer()
if (wayland && wayland.byteLength > 0) {
return { data: Buffer.from(wayland).toString("base64"), mime: "image/png" }
}
// Try X11
const x11 = await $`xclip -selection clipboard -t image/png -o`.nothrow().arrayBuffer()
if (x11 && x11.byteLength > 0) {
return { data: Buffer.from(x11).toString("base64"), mime: "image/png" }
}
}
// Fall back to reading text
try {
const text = await readText()
if (text) {
return { data: text, mime: "text/plain" }
}
} catch {
// Ignore errors
}
return undefined
}
/**
* Read text from the clipboard.
*/
export async function readText(): Promise<string | undefined> {
const os = platform()
if (os === "darwin") {
const result = await $`pbpaste`.nothrow().text()
return result || undefined
}
if (os === "linux") {
// Try Wayland first
if (process.env["WAYLAND_DISPLAY"]) {
const result = await $`wl-paste`.nothrow().text()
if (result) return result
}
// Try X11
const result = await $`xclip -selection clipboard -o`.nothrow().text()
return result || undefined
}
if (os === "win32" || release().includes("WSL")) {
const result = await $`powershell.exe -NonInteractive -NoProfile -command "Get-Clipboard"`.nothrow().text()
return result?.trim() || undefined
}
return undefined
}
const getCopyMethod = createLazy(() => {
const os = platform()
if (os === "darwin" && Bun.which("osascript")) {
return async (text: string) => {
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
await $`osascript -e 'set the clipboard to "${escaped}"'`.nothrow().quiet()
}
}
if (os === "linux") {
if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-copy")) {
return async (text: string) => {
const proc = Bun.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
proc.stdin.write(text)
proc.stdin.end()
await proc.exited.catch(() => {})
}
}
if (Bun.which("xclip")) {
return async (text: string) => {
const proc = Bun.spawn(["xclip", "-selection", "clipboard"], {
stdin: "pipe",
stdout: "ignore",
stderr: "ignore",
})
proc.stdin.write(text)
proc.stdin.end()
await proc.exited.catch(() => {})
}
}
if (Bun.which("xsel")) {
return async (text: string) => {
const proc = Bun.spawn(["xsel", "--clipboard", "--input"], {
stdin: "pipe",
stdout: "ignore",
stderr: "ignore",
})
proc.stdin.write(text)
proc.stdin.end()
await proc.exited.catch(() => {})
}
}
}
if (os === "win32") {
return async (text: string) => {
// Pipe via stdin to avoid PowerShell string interpolation ($env:FOO, $(), etc.)
const proc = Bun.spawn(
[
"powershell.exe",
"-NonInteractive",
"-NoProfile",
"-Command",
"[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())",
],
{
stdin: "pipe",
stdout: "ignore",
stderr: "ignore",
},
)
proc.stdin.write(text)
proc.stdin.end()
await proc.exited.catch(() => {})
}
}
// Fallback: No native clipboard support
return async (_text: string) => {
console.warn("No clipboard support available on this platform")
}
})
/**
* Copy text to the clipboard.
* Uses OSC 52 for SSH/tmux support and native clipboard for local.
*/
export async function copy(text: string): Promise<void> {
// Always try OSC 52 first for SSH/tmux support
writeOsc52(text)
// Then use native clipboard
await getCopyMethod()(text)
}
}

View File

@@ -4,6 +4,9 @@ import type { ThemeJson } from "../types/theme-schema"
import { THEME_JSON } from "../constants/themes" import { THEME_JSON } from "../constants/themes"
import { validateTheme } from "./theme-loader" import { validateTheme } from "./theme-loader"
// Files to exclude from theme loading (not actual themes)
const EXCLUDED_FILES = new Set(["schema", "schema.json"])
export async function getCustomThemes() { export async function getCustomThemes() {
const home = process.env.HOME ?? "" const home = process.env.HOME ?? ""
if (!home) return {} if (!home) return {}
@@ -22,6 +25,10 @@ export async function getCustomThemes() {
const glob = new Bun.Glob("*.json") const glob = new Bun.Glob("*.json")
for await (const item of glob.scan({ absolute: true, followSymlinks: true, cwd: dir })) { for await (const item of glob.scan({ absolute: true, followSymlinks: true, cwd: dir })) {
const name = path.basename(item, ".json") const name = path.basename(item, ".json")
// Skip non-theme files
if (EXCLUDED_FILES.has(name) || EXCLUDED_FILES.has(path.basename(item))) {
continue
}
const json = (await Bun.file(item).json()) as ThemeJson const json = (await Bun.file(item).json()) as ThemeJson
validateTheme(json, item) validateTheme(json, item)
result[name] = json result[name] = json

136
src/utils/event-bus.ts Normal file
View File

@@ -0,0 +1,136 @@
/**
* Simple event bus for inter-component communication.
*
* This provides a decoupled way for components to communicate without
* direct dependencies. Components can publish events and subscribe to
* events they're interested in.
*
* Usage:
* ```tsx
* // Subscribe to events
* const unsub = EventBus.on("theme.changed", (data) => {
* console.log("Theme changed to:", data.theme)
* })
*
* // Publish events
* EventBus.emit("theme.changed", { theme: "dark" })
*
* // Cleanup
* unsub()
* ```
*/
type EventHandler<T = unknown> = (data: T) => void
// Export EventHandler type for external use
export type { EventHandler }
interface EventBusInstance {
on<T = unknown>(event: string, handler: EventHandler<T>): () => void
once<T = unknown>(event: string, handler: EventHandler<T>): () => void
off<T = unknown>(event: string, handler: EventHandler<T>): void
emit<T = unknown>(event: string, data: T): void
clear(): void
}
function createEventBus(): EventBusInstance {
const handlers = new Map<string, Set<EventHandler>>()
return {
on<T = unknown>(event: string, handler: EventHandler<T>): () => void {
if (!handlers.has(event)) {
handlers.set(event, new Set())
}
handlers.get(event)!.add(handler as EventHandler)
// Return unsubscribe function
return () => {
this.off(event, handler)
}
},
once<T = unknown>(event: string, handler: EventHandler<T>): () => void {
const wrappedHandler: EventHandler<T> = (data) => {
this.off(event, wrappedHandler)
handler(data)
}
return this.on(event, wrappedHandler)
},
off<T = unknown>(event: string, handler: EventHandler<T>): void {
const eventHandlers = handlers.get(event)
if (eventHandlers) {
eventHandlers.delete(handler as EventHandler)
if (eventHandlers.size === 0) {
handlers.delete(event)
}
}
},
emit<T = unknown>(event: string, data: T): void {
const eventHandlers = handlers.get(event)
if (eventHandlers) {
for (const handler of eventHandlers) {
try {
handler(data)
} catch (error) {
console.error(`Error in event handler for "${event}":`, error)
}
}
}
},
clear(): void {
handlers.clear()
},
}
}
// Singleton event bus instance
export const EventBus = createEventBus()
// Common event types for the application
export type AppEvents = {
"theme.changed": { theme: string; mode: "dark" | "light" }
"theme.mode.changed": { mode: "dark" | "light" }
"theme.reload": {}
"navigation.tab.changed": { tab: string; previousTab?: string }
"navigation.layer.changed": { depth: number; previousDepth: number }
"feed.subscribed": { feedId: string; feedUrl: string }
"feed.unsubscribed": { feedId: string }
"player.play": { episodeId: string }
"player.pause": { episodeId: string }
"player.stop": {}
"auth.login": { userId: string }
"auth.logout": {}
"toast.show": { message: string; variant: "info" | "success" | "warning" | "error"; title?: string; duration?: number }
"dialog.open": { dialogId: string }
"dialog.close": { dialogId?: string }
"command.execute": { command: string; args?: unknown }
}
// Type-safe emit and on functions
export function emit<K extends keyof AppEvents>(event: K, data: AppEvents[K]): void {
EventBus.emit(event, data)
}
export function on<K extends keyof AppEvents>(
event: K,
handler: EventHandler<AppEvents[K]>
): () => void {
return EventBus.on(event, handler)
}
export function once<K extends keyof AppEvents>(
event: K,
handler: EventHandler<AppEvents[K]>
): () => void {
return EventBus.once(event, handler)
}
export function off<K extends keyof AppEvents>(
event: K,
handler: EventHandler<AppEvents[K]>
): void {
EventBus.off(event, handler)
}

187
src/utils/keybind.ts Normal file
View File

@@ -0,0 +1,187 @@
import type { ParsedKey } from "@opentui/core"
/**
* Keyboard shortcut parsing and matching utilities.
*
* Supports key combinations like:
* - "ctrl+c" - Control + c
* - "alt+x" - Alt + x
* - "shift+enter" - Shift + Enter
* - "<leader>n" - Leader key followed by n
* - "ctrl+shift+p" - Control + Shift + p
*/
export namespace Keybind {
export interface Info {
key: string
ctrl: boolean
alt: boolean
shift: boolean
meta: boolean
leader: boolean
}
/**
* Parse a keybind string into a structured Info object.
*
* Examples:
* - "ctrl+c" -> { key: "c", ctrl: true, ... }
* - "<leader>n" -> { key: "n", leader: true, ... }
* - "alt+shift+x" -> { key: "x", alt: true, shift: true, ... }
*/
export function parse(input: string): Info[] {
if (!input) return []
// Handle multiple keybinds separated by comma or space
const parts = input.split(/[,\s]+/).filter(Boolean)
return parts.map((part) => {
const info: Info = {
key: "",
ctrl: false,
alt: false,
shift: false,
meta: false,
leader: false,
}
// Check for leader key prefix
if (part.startsWith("<leader>")) {
info.leader = true
part = part.substring(8) // Remove "<leader>"
}
// Split by + for modifiers
const tokens = part.toLowerCase().split("+")
for (const token of tokens) {
switch (token) {
case "ctrl":
case "control":
info.ctrl = true
break
case "alt":
case "option":
info.alt = true
break
case "shift":
info.shift = true
break
case "meta":
case "cmd":
case "command":
case "win":
case "super":
info.meta = true
break
default:
// The last non-modifier token is the key
info.key = token
}
}
return info
})
}
/**
* Convert a ParsedKey event to a Keybind.Info.
*/
export function fromParsedKey(evt: ParsedKey, leader: boolean = false): Info {
// ParsedKey has ctrl, shift, meta but may not have alt directly
// We need to check what properties are available
const evtAny = evt as unknown as Record<string, unknown>
return {
key: evt.name?.toLowerCase() ?? "",
ctrl: evt.ctrl ?? false,
alt: (evtAny.alt as boolean) ?? false,
shift: evt.shift ?? false,
meta: evt.meta ?? false,
leader,
}
}
/**
* Check if a keybind matches a parsed key event.
*/
export function match(keybind: Info, evt: Info): boolean {
return (
keybind.key === evt.key &&
keybind.ctrl === evt.ctrl &&
keybind.alt === evt.alt &&
keybind.shift === evt.shift &&
keybind.meta === evt.meta &&
keybind.leader === evt.leader
)
}
/**
* Convert a keybind Info to a display string.
*/
export function toString(info: Info): string {
const parts: string[] = []
if (info.leader) parts.push("<leader>")
if (info.ctrl) parts.push("Ctrl")
if (info.alt) parts.push("Alt")
if (info.shift) parts.push("Shift")
if (info.meta) parts.push("Cmd")
if (info.key) {
// Capitalize special keys
const displayKey = info.key.length === 1 ? info.key.toUpperCase() : capitalize(info.key)
parts.push(displayKey)
}
return parts.join("+")
}
function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
}
/**
* Default keybindings configuration.
*/
export const DEFAULT_KEYBINDS = {
// Leader key (space by default)
leader: "space",
// Navigation
tab_next: "tab",
tab_prev: "shift+tab",
// App commands
command_list: "ctrl+p",
help: "?",
quit: "ctrl+c",
// Session/content
session_new: "<leader>n",
session_list: "<leader>s",
// Theme
theme_list: "<leader>t",
// Player
player_play: "space",
player_pause: "space",
player_next: "n",
player_prev: "p",
player_seek_forward: "l",
player_seek_backward: "h",
// List navigation
list_up: "k",
list_down: "j",
list_top: "g",
list_bottom: "G",
list_select: "enter",
// Search
search_focus: "/",
search_clear: "escape",
}
export type KeybindsConfig = typeof DEFAULT_KEYBINDS

104
src/utils/theme-observer.ts Normal file
View File

@@ -0,0 +1,104 @@
/**
* Theme observer utility for detecting and responding to theme changes.
*
* This module provides utilities for:
* - Listening to SIGUSR2 signals for theme reload
* - Emitting theme change events via the event bus
* - Tracking theme change state
*/
import { emit, on, off, type EventHandler } from "./event-bus"
/**
* Subscribe to theme reload events.
* These are triggered by SIGUSR2 signals.
*/
export function onThemeReload(handler: EventHandler<{}>): () => void {
return on("theme.reload", handler)
}
/**
* Subscribe to theme changed events.
* These are triggered when the theme selection changes.
*/
export function onThemeChanged(
handler: EventHandler<{ theme: string; mode: "dark" | "light" }>
): () => void {
return on("theme.changed", handler)
}
/**
* Subscribe to theme mode changed events.
* These are triggered when switching between dark/light mode.
*/
export function onThemeModeChanged(
handler: EventHandler<{ mode: "dark" | "light" }>
): () => void {
return on("theme.mode.changed", handler)
}
/**
* Emit a theme reload event.
*/
export function emitThemeReload(): void {
emit("theme.reload", {})
}
/**
* Emit a theme changed event.
*/
export function emitThemeChanged(theme: string, mode: "dark" | "light"): void {
emit("theme.changed", { theme, mode })
}
/**
* Emit a theme mode changed event.
*/
export function emitThemeModeChanged(mode: "dark" | "light"): void {
emit("theme.mode.changed", { mode })
}
/**
* Setup SIGUSR2 signal handler for theme reload.
* This allows external tools to trigger a theme refresh by sending SIGUSR2 to the process.
*
* Usage: `kill -USR2 <pid>` to trigger a theme reload
*
* @param onReload - Callback to execute when SIGUSR2 is received
* @returns Cleanup function to remove the handler
*/
export function setupThemeSignalHandler(onReload: () => void): () => void {
const handler = () => {
emitThemeReload()
onReload()
}
process.on("SIGUSR2", handler)
return () => {
process.off("SIGUSR2", handler)
}
}
/**
* Create a debounced theme change handler to prevent rapid consecutive updates.
*
* @param handler - The handler to debounce
* @param delay - Delay in milliseconds (default: 100ms)
*/
export function createDebouncedThemeHandler<T>(
handler: (event: T) => void,
delay: number = 100
): (event: T) => void {
let timeout: NodeJS.Timeout | null = null
return (event: T) => {
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(() => {
handler(event)
timeout = null
}, delay)
}
}

View File

@@ -1,50 +0,0 @@
# 01. Analyze Current Navigation and Layer System
meta:
id: podtui-navigation-theming-improvements-01
feature: podtui-navigation-theming-improvements
priority: P1
depends_on: []
tags: [analysis, debugging, navigation]
objective:
- Analyze current navigation implementation and layer system
- Identify issues with the existing layerDepth signal
- Document current navigation behavior and identify gaps
- Understand how layers should work per user requirements
deliverables:
- Analysis document with current navigation state
- List of identified issues and gaps
- Recommendations for navigation improvements
steps:
- Read src/App.tsx to understand current layerDepth implementation
- Read src/components/Layout.tsx to understand layout structure
- Read src/hooks/useAppKeyboard.ts to understand keyboard handling
- Read src/components/TabNavigation.tsx and src/components/Navigation.tsx
- Review how layerDepth is used across components
- Identify issues with current navigation UX
- Document requirements: clear layer separation, active layer bg colors, left/right navigation, enter/escape controls
- Create analysis summary
tests:
- Unit: None (analysis task)
- Integration: None (analysis task)
acceptance_criteria:
- Analysis document is created and saved
- All current navigation patterns are documented
- All identified issues and gaps are listed
- Clear recommendations are provided for navigation improvements
validation:
- Review analysis document for completeness
- Verify all relevant files were analyzed
- Check that requirements are clearly documented
notes:
- Focus on understanding the gap between current implementation and user requirements
- Pay special attention to how layerDepth signal is managed
- Note any issues with keyboard event handling
- Consider how to make navigation more intuitive

View File

@@ -1,51 +0,0 @@
# 02. Fix Discover Tab Crash
meta:
id: podtui-navigation-theming-improvements-02
feature: podtui-navigation-theming-improvements
priority: P1
depends_on: [podtui-navigation-theming-improvements-01]
tags: [bug-fix, discover, crash]
objective:
- Identify and fix crash when Discover tab is selected
- Ensure DiscoverPage component loads without errors
- Test all functionality in Discover tab
deliverables:
- Fixed DiscoverPage.tsx component
- Debugged crash identified and resolved
- Test results showing no crashes
steps:
- Read src/components/DiscoverPage.tsx thoroughly
- Check for null/undefined references in DiscoverPage
- Verify useDiscoverStore() is properly initialized
- Check DISCOVER_CATEGORIES constant
- Verify TrendingShows component works correctly
- Check CategoryFilter component for issues
- Add null checks and error boundaries if needed
- Test tab selection in App.tsx
- Verify no console errors
- Test all keyboard shortcuts in Discover tab
tests:
- Unit: Test DiscoverPage component with mocked store
- Integration: Test Discover tab selection and navigation
acceptance_criteria:
- Discover tab can be selected without crashes
- No console errors when Discover tab is active
- All DiscoverPage functionality works (keyboard shortcuts, navigation)
- TrendingShows and CategoryFilter components render correctly
validation:
- Run `bun run start` and select Discover tab
- Check console for errors
- Test all keyboard interactions (j/k, tab, enter, escape, r)
- Verify content renders correctly
notes:
- Common crash causes: null store, undefined categories, missing component imports
- Check for unhandled promises or async operations
- Verify all props are properly passed from App.tsx

View File

@@ -1,50 +0,0 @@
# 03. Fix My Feeds Tab Crash
meta:
id: podtui-navigation-theming-improvements-03
feature: podtui-navigation-theming-improvements
priority: P1
depends_on: [podtui-navigation-theming-improvements-01]
tags: [bug-fix, feeds, crash]
objective:
- Identify and fix crash when My Feeds tab is selected
- Ensure FeedList component loads without errors
- Test all functionality in My Feeds tab
deliverables:
- Fixed FeedList.tsx component
- Debugged crash identified and resolved
- Test results showing no crashes
steps:
- Read src/components/FeedList.tsx thoroughly
- Check for null/undefined references in FeedList
- Verify useFeedStore() is properly initialized
- Check FeedItem component for issues
- Verify filteredFeeds() returns valid array
- Add null checks and error boundaries if needed
- Test tab selection in App.tsx
- Verify no console errors
- Test all keyboard shortcuts in FeedList
tests:
- Unit: Test FeedList component with mocked store
- Integration: Test Feeds tab selection and navigation
acceptance_criteria:
- My Feeds tab can be selected without crashes
- No console errors when My Feeds tab is active
- All FeedList functionality works (keyboard shortcuts, navigation)
- FeedItem components render correctly
validation:
- Run `bun run start` and select My Feeds tab
- Check console for errors
- Test all keyboard interactions (j/k, enter, f, s, esc)
- Verify feed list renders correctly
notes:
- Common crash causes: null store, undefined feeds, missing component imports
- Check for unhandled promises or async operations
- Verify all props are properly passed from App.tsx

View File

@@ -1,54 +0,0 @@
# 04. Fix Settings/Sources Sub-tab Crash
meta:
id: podtui-navigation-theming-improvements-04
feature: podtui-navigation-theming-improvements
priority: P1
depends_on: [podtui-navigation-theming-improvements-01]
tags: [bug-fix, settings, crash]
objective:
- Identify and fix crash when Settings/Sources sub-tab is selected
- Ensure SourceManager component loads without errors
- Test all functionality in Settings/Sources sub-tab
deliverables:
- Fixed SourceManager.tsx component
- Debugged crash identified and resolved
- Test results showing no crashes
steps:
- Read src/components/SourceManager.tsx thoroughly
- Check for null/undefined references in SourceManager
- Verify useFeedStore() is properly initialized
- Check all focus areas (list, add, url, country, explicit, language)
- Verify input component is properly imported and used
- Add null checks and error boundaries if needed
- Test Settings tab selection in App.tsx
- Test Sources sub-tab selection in SettingsScreen.tsx
- Verify no console errors
- Test all keyboard shortcuts and interactions
tests:
- Unit: Test SourceManager component with mocked store
- Integration: Test Settings tab → Sources sub-tab navigation
acceptance_criteria:
- Settings tab can be selected without crashes
- Sources sub-tab can be selected without crashes
- No console errors when Settings/Sources sub-tab is active
- All SourceManager functionality works (keyboard shortcuts, navigation)
- All form inputs and buttons work correctly
validation:
- Run `bun run start` and select Settings tab
- Select Sources sub-tab and verify it loads
- Check console for errors
- Test all keyboard interactions (tab, esc, enter, space, a, d)
- Verify form inputs work correctly
notes:
- Common crash causes: null store, undefined sources, missing component imports
- Check for unhandled promises or async operations
- Verify all props are properly passed from SettingsScreen.tsx
- Ensure useKeyboard hook doesn't conflict with parent keyboard handlers

View File

@@ -1,51 +0,0 @@
# 05. Design Layered Navigation UI System
meta:
id: podtui-navigation-theming-improvements-05
feature: podtui-navigation-theming-improvements
priority: P1
depends_on: [podtui-navigation-theming-improvements-02, podtui-navigation-theming-improvements-03, podtui-navigation-theming-improvements-04]
tags: [navigation, ui-design, layer-system]
objective:
- Design a visual layered navigation system that clearly shows depth
- Implement active layer indicators and highlighting
- Create smooth layer transition animations
- Establish visual hierarchy for nested content
deliverables:
- Enhanced Layout component with layer background system
- Layer indicator component
- Layer transition animations
- Visual hierarchy documentation
steps:
- Create layer background color system in constants/themes.ts
- Enhance Layout.tsx to support layer backgrounds
- Create LayerIndicator component
- Implement layer depth visual cues
- Add smooth transitions between layers
- Test layer visibility and transitions
tests:
- Unit: Test LayerIndicator component
- Integration: Test layer navigation visual feedback
acceptance_criteria:
- Layer backgrounds are visible and distinct
- Active layer is clearly highlighted
- Layer depth is visually indicated
- Transitions are smooth and intuitive
- Visual hierarchy is clear
validation:
- Run `bun run start` and test layer navigation
- Verify layer backgrounds appear at different depths
- Check that active layer is clearly visible
- Test smooth transitions between layers
notes:
- Use subtle color variations for layer backgrounds
- Ensure high contrast for readability
- Consider animation duration (200-300ms)
- Layer depth should be limited to 3-4 levels max

View File

@@ -1,54 +0,0 @@
# 06. Implement Left/Right Layer Navigation Controls
meta:
id: podtui-navigation-theming-improvements-06
feature: podtui-navigation-theming-improvements
priority: P1
depends_on: [podtui-navigation-theming-improvements-05]
tags: [implementation, navigation, keyboard]
objective:
- Enhance left/right arrow key navigation between layers
- Add visual feedback when navigating layers
- Prevent invalid layer transitions (can't go left from layer 0)
- Add navigation hints in Navigation component
deliverables:
- Enhanced keyboard handler with layer navigation
- Updated Navigation component with layer hints
- Visual feedback for layer navigation
- Layer boundary prevention logic
steps:
- Update useAppKeyboard hook to handle left/right for layer navigation
- Add layer navigation visual feedback
- Prevent invalid layer transitions (can't go left from layer 0, can't go right beyond max)
- Update Navigation component to show layer navigation hints
- Add visual indicators for current layer position
- Test navigation between layers
- Ensure keyboard shortcuts don't conflict with page-specific shortcuts
tests:
- Unit: Test keyboard handler with mocked key events
- Integration: Test left/right navigation between layers
acceptance_criteria:
- <left> key navigates to previous layer (prevents going below layer 0)
- <right> key navigates to next layer (prevents exceeding max depth)
- Current layer is visually indicated
- Navigation hints are shown in Navigation component
- No keyboard conflicts with page-specific shortcuts
- Navigation works correctly at layer boundaries
validation:
- Run `bun run start` and test left/right navigation
- Verify current layer is highlighted
- Check that navigation hints are visible
- Test at layer boundaries (first/last layer)
- Verify no conflicts with page shortcuts
notes:
- Use existing useAppKeyboard hook as base
- Consider max layer depth (3-4 levels)
- Ensure smooth visual transitions
- Consider adding sound effects for navigation

View File

@@ -1,55 +0,0 @@
# 07. Implement Enter/Escape Layer Navigation Controls
meta:
id: podtui-navigation-theming-improvements-07
feature: podtui-navigation-theming-improvements
priority: P1
depends_on: [podtui-navigation-theming-improvements-05]
tags: [implementation, navigation, keyboard]
objective:
- Enhance enter key to go down into a layer
- Enhance escape key to go up multiple layers at once
- Add visual feedback when entering/exiting layers
- Prevent invalid layer transitions
deliverables:
- Enhanced keyboard handler for enter/escape layer navigation
- Updated Navigation component with layer hints
- Visual feedback for layer navigation
- Layer boundary prevention logic
steps:
- Update useAppKeyboard hook to handle enter for going down
- Update useAppKeyboard hook to handle escape for going up multiple layers
- Add visual feedback when entering/exiting layers
- Prevent invalid layer transitions (can't go down from max depth)
- Update Navigation component to show layer navigation hints
- Add visual indicators for current layer position
- Test enter to go down, escape to go up
- Ensure proper layer nesting behavior
tests:
- Unit: Test keyboard handler with mocked key events
- Integration: Test enter/escape navigation between layers
acceptance_criteria:
- <enter> key goes down into a layer (prevents going below max depth)
- <escape> key goes up multiple layers at once
- Current layer is visually indicated
- Navigation hints are shown in Navigation component
- No keyboard conflicts with page-specific shortcuts
- Navigation works correctly at layer boundaries
validation:
- Run `bun run start` and test enter/escape navigation
- Verify current layer is highlighted
- Check that navigation hints are visible
- Test at layer boundaries (first/last layer)
- Verify no conflicts with page shortcuts
notes:
- Use existing useAppKeyboard hook as base
- Consider max layer depth (3-4 levels)
- Ensure smooth visual transitions
- Consider adding sound effects for navigation

View File

@@ -1,56 +0,0 @@
# 08. Design Active Layer Background Color System
meta:
id: podtui-navigation-theming-improvements-08
feature: podtui-navigation-theming-improvements
priority: P1
depends_on: [podtui-navigation-theming-improvements-05]
tags: [implementation, theming, navigation]
objective:
- Design active layer background colors for each depth level
- Define color palette for layer backgrounds
- Create theme-aware layer styling
- Implement visual hierarchy for layers
deliverables:
- Enhanced theme system with layer backgrounds
- Layer background colors for all themes
- Visual hierarchy implementation
- Theme tokens for layer backgrounds
steps:
- Review existing theme system in src/constants/themes.ts
- Design layer background colors for layer 0-3
- Define color palette that works with existing themes
- Add layerBackgrounds property to theme colors
- Implement layer background rendering in Layout component
- Add visual indicators for active/inactive layers
- Ensure colors work with system/light/dark modes
- Test layer color transitions
tests:
- Unit: Test theme layer backgrounds
- Integration: Test layer color rendering
acceptance_criteria:
- Layer background colors are defined for all themes
- Active layer is clearly visible with distinct background
- Inactive layers have subtle background variations
- Visual hierarchy is clear between layers
- Colors work with all theme modes
- Layer backgrounds are accessible and readable
validation:
- Run `bun run start` and test layer colors
- Verify layer backgrounds appear at different depths
- Check that active layer is clearly visible
- Test with different themes (catppuccin, gruvbox, etc.)
- Verify colors work in both light and dark modes
notes:
- Use existing theme colors for layer backgrounds
- Ensure high contrast for readability
- Colors should be subtle but clearly visible
- Consider terminal color limitations
- Design should be consistent with existing UI elements

View File

@@ -1,59 +0,0 @@
# 09. Create Theme Context Provider
meta:
id: podtui-navigation-theming-improvements-09
feature: podtui-navigation-theming-improvements
priority: P1
depends_on: []
tags: [theming, implementation, solid-js]
objective:
- Create theme context provider for global theme management
- Implement theme state management with signals
- Provide theme tokens to all components
- Add system theme detection and preference observer
deliverables:
- Theme context provider component
- Theme state management hooks
- Theme provider integration
- System theme detection logic
steps:
- Review existing theme system in src/stores/app.ts
- Create theme context using SolidJS createContext
- Design theme state structure (themeId, colorScheme, mode, etc.)
- Implement theme state management with signals
- Add theme selection and color scheme management functions
- Create ThemeProvider component
- Add theme persistence to localStorage
- Implement system theme detection
- Add theme change listeners
- Test theme context provider
tests:
- Unit: Test theme context provider with mocked state
- Integration: Test theme provider integration
acceptance_criteria:
- Theme context provider is created
- Theme state management works correctly
- Theme selection and color scheme management functions work
- Theme persistence to localStorage works
- System theme detection works
- Theme change listeners work
- Theme provider can be used to wrap App component
validation:
- Run `bun run start` and verify theme context provider works
- Test theme selection functionality
- Test color scheme switching
- Verify localStorage persistence
- Check system theme detection
notes:
- Use existing appStore as base for theme management
- Follow SolidJS context patterns
- Use createSignal for reactive theme state
- Ensure proper cleanup in onCleanup
- Test with different theme configurations

View File

@@ -1,57 +0,0 @@
# 10. Implement DesktopTheme Type and Structure
meta:
id: podtui-navigation-theming-improvements-10
feature: podtui-navigation-theming-improvements
priority: P2
depends_on: [podtui-navigation-theming-improvements-09]
tags: [theming, implementation, types]
objective:
- Implement DesktopTheme type and structure based on opencode
- Define theme data structure for light and dark variants
- Create theme token types
deliverables:
- DesktopTheme type definition
- Theme variant structure
- Token type definitions
- Example theme data
steps:
- Read opencode/packages/ui/src/theme/types.ts for reference
- Create DesktopTheme type interface
- Define ThemeVariant structure with seeds and overrides
- Define ThemeColor type
- Define ColorScheme type
- Define ResolvedTheme type
- Define ColorValue type
- Create theme constants file
- Add example theme data (system, catppuccin, gruvbox, tokyo, nord)
- Test type definitions
tests:
- Unit: Test type definitions with TypeScript compiler
- Integration: None (type definition task)
acceptance_criteria:
- DesktopTheme type is defined
- ThemeVariant structure is defined
- ThemeColor type is defined
- ColorScheme type is defined
- ResolvedTheme type is defined
- ColorValue type is defined
- Example theme data is provided
- All types are exported correctly
validation:
- Run `tsc --noEmit` to verify no TypeScript errors
- Test theme type usage in components
- Verify theme data structure is correct
notes:
- Use references/solid/REFERENCE.md for SolidJS patterns
- Follow opencode theming implementation patterns
- Ensure types are comprehensive and well-documented
- Add JSDoc comments for complex types
- Consider TypeScript strict mode compliance

View File

@@ -1,59 +0,0 @@
# 11. Implement Theme Resolution System
meta:
id: podtui-navigation-theming-improvements-11
feature: podtui-navigation-theming-improvements
priority: P2
depends_on: [podtui-navigation-theming-improvements-10]
tags: [theming, implementation, theme-resolution]
objective:
- Implement theme resolution system for light and dark variants
- Create theme token generation logic
- Define color scale generation functions
deliverables:
- Theme resolution functions
- Color scale generation functions
- Theme token generation logic
- Theme CSS variable generation
steps:
- Read opencode/packages/ui/src/theme/resolve.ts for reference
- Implement resolveThemeVariant function
- Implement generateNeutralScale function
- Implement generateScale function
- Implement hexToOklch and oklchToHex functions
- Implement withAlpha function
- Implement themeToCss function
- Create theme resolution constants
- Define color scale configurations
- Implement theme resolution for each theme
- Test theme resolution functions
tests:
- Unit: Test theme resolution with mocked themes
- Integration: Test theme resolution in context provider
acceptance_criteria:
- resolveThemeVariant function works correctly
- generateNeutralScale function works correctly
- generateScale function works correctly
- Color conversion functions work correctly
- themeToCss function generates valid CSS
- Theme resolution works for all themes
- Light and dark variants are resolved correctly
validation:
- Run `bun run start` and verify theme resolution works
- Test theme resolution for all themes
- Verify light/dark variants are correct
- Check CSS variable generation
- Test with different color schemes
notes:
- Use references/solid/REFERENCE.md for SolidJS patterns
- Follow opencode theming implementation patterns
- Ensure color scales are generated correctly
- Test color conversion functions
- Verify theme tokens match expected values

View File

@@ -1,57 +0,0 @@
# 12. Create CSS Variable Token System
meta:
id: podtui-navigation-theming-improvements-12
feature: podtui-navigation-theming-improvements
priority: P3
depends_on: [podtui-navigation-theming-improvements-11]
tags: [theming, implementation, css-variables]
objective:
- Create comprehensive CSS variable token system
- Define all theme tokens for OpenTUI components
- Generate CSS variables from theme tokens
deliverables:
- CSS variable token definitions
- Theme CSS generation functions
- CSS variable application utilities
steps:
- Read opencode/packages/ui/src/theme/resolve.ts for reference
- Review opencode theme token definitions
- Define theme tokens for OpenTUI components:
- Background tokens (background-base, background-weak, etc.)
- Surface tokens (surface-base, surface-raised, etc.)
- Text tokens (text-base, text-weak, etc.)
- Border tokens (border-base, border-hover, etc.)
- Interactive tokens (interactive-base, interactive-hover, etc.)
- Color tokens for specific states (success, warning, error, etc.)
- Layer navigation tokens (layer-active-bg, layer-inactive-bg, etc.)
- Implement themeToCss function to generate CSS variables
- Create utility to apply theme tokens to components
- Test CSS variable generation
tests:
- Unit: Test CSS variable generation
- Integration: Test CSS variable application in components
acceptance_criteria:
- All OpenTUI theme tokens are defined
- CSS variable generation works correctly
- Theme tokens can be applied to components
- Generated CSS is valid
- Tokens cover all component styling needs
validation:
- Run `bun run start` and verify CSS variables are applied
- Test theme token application in components
- Check CSS variable values
- Verify tokens work with existing components
notes:
- Use references/solid/REFERENCE.md for SolidJS patterns
- Follow opencode theming implementation patterns
- Ensure tokens are comprehensive and well-organized
- Test token application in various components
- Consider backward compatibility with existing hardcoded colors

View File

@@ -1,55 +0,0 @@
# 13. Implement System Theme Detection
meta:
id: podtui-navigation-theming-improvements-13
feature: podtui-navigation-theming-improvements
priority: P2
depends_on: [podtui-navigation-theming-improvements-12]
tags: [theming, implementation, system-theme]
objective:
- Implement system theme detection (prefers-color-scheme)
- Add theme mode management (light/dark/auto)
- Implement automatic theme switching based on system preferences
deliverables:
- System theme detection implementation
- Theme mode management functions
- Automatic theme switching logic
steps:
- Read opencode/packages/ui/src/theme/context.tsx for reference
- Implement getSystemMode function
- Add theme mode state to theme context
- Implement colorScheme state management
- Add window.matchMedia listener for system theme changes
- Implement automatic mode switching when colorScheme is "system"
- Add theme mode persistence to localStorage
- Test system theme detection
- Test automatic theme switching
tests:
- Unit: Test system theme detection with mocked media queries
- Integration: Test theme mode switching
acceptance_criteria:
- getSystemMode function works correctly
- Theme mode state is managed correctly
- Window.matchMedia listener works correctly
- Automatic theme switching works when colorScheme is "system"
- Theme mode persistence to localStorage works
- System theme changes are detected and applied
validation:
- Run `bun run start` and test system theme detection
- Test switching system between light/dark modes
- Verify automatic theme switching works
- Check localStorage persistence
- Test theme mode selection (system/light/dark)
notes:
- Use references/solid/REFERENCE.md for SolidJS patterns
- Follow opencode theming implementation patterns
- Ensure proper cleanup in onCleanup
- Test with different system theme settings
- Verify theme updates are reactive

View File

@@ -1,59 +0,0 @@
# 14. Integrate Theme Provider into App Component
meta:
id: podtui-navigation-theming-improvements-14
feature: podtui-navigation-theming-improvements
priority: P2
depends_on: [podtui-navigation-theming-improvements-09, podtui-navigation-theming-improvements-13]
tags: [integration, theming, app]
objective:
- Integrate theme provider into App component
- Apply theme tokens to all components
- Ensure theme changes are reactive
deliverables:
- Updated App.tsx with theme provider
- Theme application logic
- Theme integration tests
steps:
- Read references/solid/REFERENCE.md for SolidJS patterns
- Wrap App component with ThemeProvider
- Implement theme application in Layout component
- Apply theme tokens to all components:
- TabNavigation (active tab background)
- Navigation (footer navigation)
- DiscoverPage (background, borders, text colors)
- FeedList (background, borders, text colors)
- SettingsScreen (background, borders, text colors)
- SourceManager (background, borders, text colors)
- All other components
- Update theme tokens usage in src/constants/themes.ts
- Test theme application in all components
- Test theme changes are reactive
tests:
- Unit: Test theme provider integration
- Integration: Test theme changes in all components
acceptance_criteria:
- ThemeProvider is integrated into App component
- Theme tokens are applied to all components
- Theme changes are reactive
- All components render correctly with theme tokens
- No console errors related to theming
validation:
- Run `bun run start` and verify theme is applied
- Test theme selection and color scheme switching
- Test system theme detection
- Verify all components use theme tokens
- Check console for errors
notes:
- Use references/solid/REFERENCE.md for SolidJS patterns
- Follow opencode theming implementation patterns
- Ensure theme tokens are used consistently
- Test theme changes in all components
- Verify no hardcoded colors remain

View File

@@ -1,60 +0,0 @@
# 15. Update Components to Use Theme Tokens
meta:
id: podtui-navigation-theming-improvements-15
feature: podtui-navigation-theming-improvements
priority: P3
depends_on: [podtui-navigation-theming-improvements-14]
tags: [theming, implementation, component-updates]
objective:
- Replace all hardcoded colors with theme tokens
- Update all components to use theme context
- Ensure consistent theme application across all components
deliverables:
- Updated components using theme tokens
- Theme token usage guide
- List of components updated
steps:
- Review all components in src/components/
- Identify all hardcoded color values
- Replace hardcoded colors with theme tokens:
- Background colors (background-base, surface-base, etc.)
- Text colors (text-base, text-weak, etc.)
- Border colors (border-base, border-hover, etc.)
- Interactive colors (interactive-base, interactive-hover, etc.)
- Color tokens (success, warning, error, etc.)
- Layer navigation colors (layer-active-bg, layer-inactive-bg, etc.)
- Update src/constants/themes.ts to export theme tokens
- Update all components to use theme context
- Test theme tokens in all components
- Verify no hardcoded colors remain
- Create theme token usage guide
tests:
- Unit: Test theme tokens in individual components
- Integration: Test theme tokens in all components
acceptance_criteria:
- All hardcoded colors are replaced with theme tokens
- All components use theme tokens
- Theme tokens are exported from themes.ts
- Theme token usage guide is created
- No console errors related to theming
- All components render correctly with theme tokens
validation:
- Run `bun run start` and verify all components use theme tokens
- Test theme selection and color scheme switching
- Verify all components render correctly
- Check console for errors
- Verify no hardcoded colors remain
notes:
- Use references/solid/REFERENCE.md for SolidJS patterns
- Follow opencode theming implementation patterns
- Ensure consistent theme application across all components
- Test theme tokens in all components
- Verify backward compatibility if needed

View File

@@ -1,65 +0,0 @@
# 16. Test Navigation Flows and Layer Transitions
meta:
id: podtui-navigation-theming-improvements-16
feature: podtui-navigation-theming-improvements
priority: P2
depends_on: [podtui-navigation-theming-improvements-06, podtui-navigation-theming-improvements-07, podtui-navigation-theming-improvements-08]
tags: [testing, navigation, integration]
objective:
- Test all navigation flows and layer transitions
- Verify left/right navigation works correctly
- Verify enter/escape navigation works correctly
- Test layer transitions between different pages
deliverables:
- Navigation test results
- Test cases for navigation flows
- Bug reports for any issues
steps:
- Create test cases for navigation flows:
- Test left/right navigation between layers
- Test enter to go down into layers
- Test escape to go up from layers
- Test layer boundaries (first/last layer)
- Test layer nesting behavior
- Test navigation with different terminal sizes
- Run `bun run start` and perform all test cases
- Document any issues or bugs
- Test navigation in all pages:
- Discover tab
- My Feeds tab
- Search tab
- Player tab
- Settings tab
- Test keyboard shortcut conflicts
- Test visual feedback for navigation
- Test layer color visibility
tests:
- Unit: Test navigation logic with mocked state
- Integration: Test navigation flows in actual application
acceptance_criteria:
- All navigation flows work correctly
- Left/right navigation works between layers
- Enter/escape navigation works correctly
- Layer boundaries are handled correctly
- No keyboard shortcut conflicts
- Visual feedback is clear and accurate
- All pages work correctly with navigation
validation:
- Run `bun run start` and perform all test cases
- Document all test results
- Report any issues found
- Verify all navigation flows work
notes:
- Test with different terminal sizes
- Test with different layer depths
- Test keyboard shortcuts in all pages
- Verify visual feedback is clear
- Test edge cases and error conditions

View File

@@ -1,64 +0,0 @@
# 17. Test Tab Crash Fixes and Edge Cases
meta:
id: podtui-navigation-theming-improvements-17
feature: podtui-navigation-theming-improvements
priority: P1
depends_on: [podtui-navigation-theming-improvements-02, podtui-navigation-theming-improvements-03, podtui-navigation-theming-improvements-04]
tags: [testing, crash-fix, integration]
objective:
- Test all tab crash fixes
- Verify Discover, My Feeds, and Settings tabs load without errors
- Test edge cases and error conditions
deliverables:
- Crash fix test results
- Test cases for tab crashes
- Bug reports for any remaining issues
steps:
- Create test cases for tab crashes:
- Test Discover tab selection
- Test My Feeds tab selection
- Test Settings tab selection
- Test Settings/Sources sub-tab selection
- Test navigation between tabs
- Run `bun run start` and perform all test cases
- Test all keyboard shortcuts in each tab
- Test edge cases:
- Empty feed lists
- Missing data
- Network errors
- Invalid inputs
- Rapid tab switching
- Test error boundaries
- Document any remaining issues
- Verify no console errors
tests:
- Unit: Test components with mocked data
- Integration: Test all tabs and sub-tabs in actual application
acceptance_criteria:
- All tabs load without crashes
- No console errors when selecting tabs
- All keyboard shortcuts work correctly
- Edge cases are handled gracefully
- Error boundaries work correctly
- All test cases pass
validation:
- Run `bun run start` and perform all test cases
- Check console for errors
- Test all keyboard shortcuts
- Test edge cases
- Document all test results
- Report any remaining issues
notes:
- Test with different data states (empty, full, partial)
- Test network error scenarios
- Test rapid user interactions
- Verify error messages are clear
- Test with different terminal sizes

View File

@@ -1,72 +0,0 @@
# 18. Test Theming System with All Modes
meta:
id: podtui-navigation-theming-improvements-18
feature: podtui-navigation-theming-improvements
priority: P2
depends_on: [podtui-navigation-theming-improvements-15, podtui-navigation-theming-improvements-16, podtui-navigation-theming-improvements-17]
tags: [testing, theming, integration]
objective:
- Test theming system with all modes (system/light/dark)
- Verify theme tokens work correctly
- Ensure theming is consistent across all components
- Test theme persistence and system theme detection
deliverables:
- Theming system test results
- Theme token test cases
- Theme consistency report
steps:
- Create test cases for theming system:
- Test system theme mode
- Test light theme mode
- Test dark theme mode
- Test theme persistence
- Test system theme detection
- Test theme selection
- Test color scheme switching
- Run `bun run start` and perform all test cases
- Test theme tokens in all components:
- Background colors
- Text colors
- Border colors
- Interactive colors
- Color tokens (success, warning, error, etc.)
- Layer navigation colors
- Test theme changes are reactive
- Test theme tokens work in all terminals
- Verify theme consistency across all components
- Document any issues
tests:
- Unit: Test theme tokens with mocked themes
- Integration: Test theming in all components and modes
acceptance_criteria:
- Theming system works correctly in all modes
- Theme tokens work correctly in all components
- Theme changes are reactive
- Theme persistence works correctly
- System theme detection works correctly
- Theme consistency is maintained across all components
- All test cases pass
validation:
- Run `bun run start` and perform all test cases
- Test all theme modes (system/light/dark)
- Test theme selection and color scheme switching
- Verify theme persistence
- Test system theme detection
- Check console for errors
- Verify theme consistency across all components
- Document all test results
notes:
- Test with different terminal sizes
- Test with different color schemes
- Test rapid theme changes
- Verify theme updates are reactive
- Test all components with all themes
- Ensure accessibility standards are met

View File

@@ -1,50 +0,0 @@
# PodTUI Navigation and Theming Improvements
Objective: Implement layered navigation system, fix tab crashes, and integrate sophisticated theming based on opencode
Status legend: [ ] todo, [~] in-progress, [x] done
Tasks
- [x] 01 — Analyze current navigation and layer system → `01-analyze-navigation-system.md`
- [x] 02 — Fix Discover tab crash → `02-fix-discover-tab-crash.md`
- [x] 03 — Fix My Feeds tab crash → `03-fix-feeds-tab-crash.md`
- [x] 04 — Fix Settings/Sources sub-tab crash → `04-fix-settings-sources-crash.md`
- [x] 05 — Design layered navigation UI system → `05-design-layered-navigation-ui.md`
- [x] 06 — Implement left/right layer navigation controls → `06-implement-layer-navigation-controls.md`
- [x] 07 — Implement enter/escape layer navigation controls → `07-implement-enter-escape-controls.md`
- [x] 08 — Design active layer background color system → `08-design-active-layer-colors.md`
- [x] 09 — Create theme context provider → `09-create-theme-context-provider.md`
- [x] 10 — Implement DesktopTheme type and structure → `10-implement-desktop-theme-types.md`
- [x] 11 — Implement theme resolution system → `11-implement-theme-resolution.md`
- [x] 12 — Create CSS variable token system → `12-create-css-token-system.md`
- [x] 13 — Implement system theme detection → `13-implement-system-theme-detection.md`
- [x] 14 — Integrate theme provider into App component → `14-integrate-theme-provider.md`
- [x] 15 — Update components to use theme tokens → `15-update-components-to-use-themes.md`
- [ ] 16 — Test navigation flows and layer transitions → `16-test-navigation-flows.md`
- [ ] 17 — Test tab crash fixes and edge cases → `17-test-tab-crash-fixes.md`
- [ ] 18 — Test theming system with all modes → `18-test-theming-system.md`
Dependencies
- 01 depends on
- 02, 03, 04 depends on 01
- 05 depends on 02, 03, 04
- 06, 07, 08 depends on 05
- 16 depends on 06, 07, 08
- 09 depends on
- 10 depends on 09
- 11 depends on 10
- 12 depends on 11
- 13 depends on 12
- 14 depends on 13
- 15 depends on 14
- 18 depends on 15, 16, 17
Exit criteria
- Navigation is clearly visualized with layered backgrounds and active states
- Left/right keys navigate between layers, enter goes down, escape goes up
- All tabs (Discover, My Feeds, Settings) load without crashes
- Settings/Sources sub-tab loads without crashes
- Theming system works correctly with system/light/dark/auto modes
- All components use theme tokens consistently
- No hardcoded colors remain in components
- All tests pass and crashes are resolved

View File

@@ -1,63 +0,0 @@
# 01. Create JSON Theme Schema and File Structure
meta:
id: theme-refactoring-01
feature: theme-refactoring-json-format
priority: P0
depends_on: []
tags: [implementation, infrastructure]
objective:
- Create the JSON theme schema and establish the file structure for theme definitions
- Define the theme.json schema that opencode uses
- Create the directory structure for JSON theme files
deliverables:
- `src/types/theme-schema.ts` - JSON theme schema definition
- `src/themes/*.json` - Directory for theme JSON files
- `src/themes/schema.json` - Theme schema reference
steps:
- Step 1.1: Create `src/types/theme-schema.ts` with TypeScript interfaces matching the opencode theme JSON structure
- Define `ThemeJson` interface with `$schema`, `defs`, and `theme` properties
- Define `ColorValue` type supporting hex colors, color references, variants, and RGBA
- Define `Variant` type for light/dark mode color definitions
- Export interfaces for type checking
- Step 1.2: Create `src/themes/schema.json` with the opencode theme schema reference
- Add `$schema: "https://opencode.ai/theme.json"`
- Document the theme structure in comments
- Step 1.3: Create `src/themes/` directory
- Ensure directory exists for JSON theme files
- Step 1.4: Create a sample theme file in `src/themes/opencode.json`
- Use the opencode theme as reference
- Include proper `$schema` reference
- Define `defs` with all color references
- Define `theme` with semantic color mappings
tests:
- Unit:
- Test `ThemeJson` type definition matches opencode structure
- Test `ColorValue` type accepts hex colors, references, variants, and RGBA
- Test `Variant` type structure is correct
- Integration/e2e:
- Verify JSON file can be parsed without errors
- Validate schema reference is correct
acceptance_criteria:
- `src/types/theme-schema.ts` file exists with all required interfaces
- `src/themes/schema.json` contains valid schema reference
- `src/themes/` directory is created
- `src/themes/opencode.json` can be imported and parsed successfully
validation:
- Run: `bun run typecheck` - Should pass with no type errors
- Run: `cat src/themes/opencode.json | jq .` - Should be valid JSON
notes:
- Follow opencode's theme.json structure exactly
- Use TypeScript interfaces to ensure type safety
- Reference: `/home/mike/code/PodTui/opencode/packages/opencode/src/cli/cmd/tui/context/theme/`

View File

@@ -1,77 +0,0 @@
# 02. Convert Existing Themes to JSON Format
meta:
id: theme-refactoring-02
feature: theme-refactoring-json-format
priority: P0
depends_on: [theme-refactoring-01]
tags: [implementation, migration]
objective:
- Convert all existing PodTui themes (catppuccin, gruvbox, tokyo, nord) from TypeScript constants to JSON format
- Ensure each theme matches the opencode JSON structure
- Maintain color fidelity with original theme definitions
deliverables:
- `src/themes/catppuccin.json` - Catppuccin theme in JSON format
- `src/themes/gruvbox.json` - Gruvbox theme in JSON format
- `src/themes/tokyo.json` - Tokyo Night theme in JSON format
- `src/themes/nord.json` - Nord theme in JSON format
steps:
- Step 2.1: Convert `catppuccin` theme from `src/types/desktop-theme.ts` to JSON
- Extract all color definitions from `THEMES_DESKTOP.variants.find((v) => v.name === "catppuccin")!.colors`
- Create `defs` object with all color references (dark and light variants)
- Create `theme` object with semantic mappings
- Add `$schema` reference
- Verify color values match original definitions
- Step 2.2: Convert `gruvbox` theme from `src/types/desktop-theme.ts` to JSON
- Extract all color definitions from `THEMES_DESKTOP.variants.find((v) => v.name === "gruvbox")!.colors`
- Create `defs` object with all color references
- Create `theme` object with semantic mappings
- Add `$schema` reference
- Step 2.3: Convert `tokyo` theme from `src/types/desktop-theme.ts` to JSON
- Extract all color definitions from `THEMES_DESKTOP.variants.find((v) => v.name === "tokyo")!.colors`
- Create `defs` object with all color references
- Create `theme` object with semantic mappings
- Add `$schema` reference
- Step 2.4: Convert `nord` theme from `src/types/desktop-theme.ts` to JSON
- Extract all color definitions from `THEMES_DESKTOP.variants.find((v) => v.name === "nord")!.colors`
- Create `defs` object with all color references
- Create `theme` object with semantic mappings
- Add `$schema` reference
- Step 2.5: Verify all JSON files are valid
- Check syntax with `bun run typecheck`
- Ensure all imports work correctly
tests:
- Unit:
- Test each JSON file can be imported without errors
- Verify color values match original TypeScript definitions
- Integration/e2e:
- Load each theme and verify all colors are present
- Check that theme structure matches expected schema
acceptance_criteria:
- All four theme JSON files exist in `src/themes/`
- Each JSON file follows the theme schema
- Color values match original theme definitions exactly
- All JSON files are valid and can be parsed
validation:
- Run: `bun run typecheck` - Should pass
- Run: `cat src/themes/catppuccin.json | jq .` - Should be valid JSON
- Run: `cat src/themes/gruvbox.json | jq .` - Should be valid JSON
- Run: `cat src/themes/tokyo.json | jq .` - Should be valid JSON
- Run: `cat src/themes/nord.json | jq .` - Should be valid JSON
notes:
- Use opencode's theme structure as reference
- Maintain backward compatibility with existing color definitions
- Ensure both dark and light variants are included if available
- Reference: `/home/mike/code/PodTui/opencode/packages/opencode/src/cli/cmd/tui/context/theme/`

View File

@@ -1,70 +0,0 @@
# 03. Update Type Definitions for Color References and Variants
meta:
id: theme-refactoring-03
feature: theme-refactoring-json-format
priority: P0
depends_on: [theme-refactoring-01]
tags: [implementation, types]
objective:
- Update type definitions to support the new JSON theme structure
- Add support for color references, variants, and light/dark mode
- Maintain backward compatibility with existing code
deliverables:
- `src/types/theme-schema.ts` - Updated with new types
- `src/types/settings.ts` - Updated with color reference types
- `src/types/desktop-theme.ts` - Updated to support JSON themes
steps:
- Step 3.1: Update `src/types/theme-schema.ts`
- Export `ThemeJson` interface
- Export `ColorValue` type
- Export `Variant` type
- Add `ThemeColors` type for resolved theme colors
- Step 3.2: Update `src/types/settings.ts`
- Add `ThemeJson` import
- Add `ColorValue` type definition
- Add `Variant` type definition
- Update `ThemeColors` to support color references
- Add `ThemeJson` type for JSON theme files
- Step 3.3: Update `src/types/desktop-theme.ts`
- Add imports for `ThemeJson`, `ColorValue`, `Variant`
- Add `ThemeJson` type for JSON theme files
- Update existing types to support color references
- Add helper functions for JSON theme loading
- Step 3.4: Ensure backward compatibility
- Keep existing `ThemeColors` structure for resolved themes
- Ensure existing code can still use theme colors as strings
- Add type guards for color references
tests:
- Unit:
- Test `ThemeJson` type accepts valid JSON theme structure
- Test `ColorValue` type accepts hex colors, references, variants, and RGBA
- Test `Variant` type structure is correct
- Test existing `ThemeColors` type remains compatible
- Integration/e2e:
- Verify type imports work correctly
- Test type inference with JSON theme files
acceptance_criteria:
- All type definitions are updated and exported
- Backward compatibility maintained with existing code
- New types support color references and variants
- Type checking passes without errors
validation:
- Run: `bun run typecheck` - Should pass with no errors
- Verify existing components can still use theme colors
- Test type inference with new theme JSON files
notes:
- Use TypeScript's `with { type: "json" }` for JSON imports
- Ensure all types are properly exported for use across the codebase
- Reference: `/home/mike/code/PodTui/opencode/packages/opencode/src/cli/cmd/tui/context/theme.tsx`

View File

@@ -1,83 +0,0 @@
# 04. Create Theme Resolution Logic with Color Reference Lookup
meta:
id: theme-refactoring-04
feature: theme-refactoring-json-format
priority: P0
depends_on: [theme-refactoring-02, theme-refactoring-03]
tags: [implementation, logic]
objective:
- Implement theme resolution logic that handles color references and variants
- Create function to resolve colors from theme JSON files
- Support hex colors, color references, ANSI codes, and RGBA values
- Handle light/dark mode selection based on current mode
deliverables:
- `src/utils/theme-resolver.ts` - Theme resolution utility
- `src/utils/ansi-to-rgba.ts` - ANSI color conversion utility
steps:
- Step 4.1: Create `src/utils/ansi-to-rgba.ts`
- Implement `ansiToRgba(code: number): RGBA` function
- Handle standard ANSI colors (0-15)
- Handle 6x6x6 color cube (16-231)
- Handle grayscale ramp (232-255)
- Add type definitions for RGBA
- Step 4.2: Create `src/utils/theme-resolver.ts`
- Implement `resolveTheme(theme: ThemeJson, mode: "dark" | "light"): ThemeColors`
- Create `resolveColor(c: ColorValue): RGBA` helper function
- Handle RGBA objects directly
- Handle hex color strings
- Handle color references from `defs`
- Handle variants (dark/light mode)
- Handle ANSI codes
- Add error handling for invalid color references
- Step 4.3: Implement color reference resolution
- Create lookup logic for `defs` object
- Add fallback to theme colors if reference not in defs
- Throw descriptive errors for invalid references
- Step 4.4: Handle optional theme properties
- Support `selectedListItemText` property
- Support `backgroundMenu` property
- Support `thinkingOpacity` property
- Add default values for missing properties
tests:
- Unit:
- Test `ansiToRgba` with ANSI codes 0-15
- Test `ansiToRgba` with color cube codes 16-231
- Test `ansiToRgba` with grayscale codes 232-255
- Test `resolveColor` with hex colors
- Test `resolveColor` with color references
- Test `resolveColor` with light/dark variants
- Test `resolveColor` with RGBA objects
- Test `resolveTheme` with complete theme JSON
- Test `resolveTheme` with missing optional properties
- Test error handling for invalid color references
- Integration/e2e:
- Test resolving colors from actual theme JSON files
- Verify light/dark mode selection works correctly
acceptance_criteria:
- `src/utils/ansi-to-rgba.ts` file exists with conversion functions
- `src/utils/theme-resolver.ts` file exists with resolution logic
- All color value types are handled correctly
- Error messages are descriptive and helpful
- Theme resolution works with both light and dark modes
validation:
- Run: `bun run typecheck` - Should pass
- Run unit tests with `bun test src/utils/theme-resolver.test.ts`
- Test with sample theme JSON files
- Verify error messages are clear
notes:
- Use opencode's implementation as reference for color resolution
- Ensure RGBA values are normalized to 0-1 range
- Add comprehensive error handling for invalid inputs
- Reference: `/home/mike/code/PodTui/opencode/packages/opencode/src/cli/cmd/tui/context/theme.tsx` (lines 176-277)

View File

@@ -1,80 +0,0 @@
# 05. Migrate ThemeContext to SolidJS Pattern
meta:
id: theme-refactoring-05
feature: theme-refactoring-json-format
priority: P0
depends_on: [theme-refactoring-04]
tags: [implementation, context]
objective:
- Migrate ThemeContext from basic React context to SolidJS context pattern
- Implement reactive state management for theme switching
- Add persistence for theme selection and mode
- Support system theme detection
deliverables:
- `src/context/ThemeContext.tsx` - Updated with SolidJS pattern
- `src/utils/theme-context.ts` - Theme context utilities
steps:
- Step 5.1: Update `src/context/ThemeContext.tsx`
- Import SolidJS hooks: `createSignal`, `createEffect`, `createMemo`, `createStore`
- Replace React context with SolidJS `createSimpleContext` pattern (from opencode)
- Add theme loading logic
- Implement reactive theme state
- Add system theme detection
- Support theme persistence via localStorage
- Step 5.2: Implement theme state management
- Create store with `themes`, `mode`, `active`, `ready`
- Initialize with default theme (opencode)
- Load custom themes from JSON files
- Handle system theme detection
- Step 5.3: Add reactive theme resolution
- Create `createMemo` for resolved theme colors
- Create `createMemo` for syntax highlighting
- Create `createMemo` for subtle syntax
- Step 5.4: Implement theme switching
- Add `setTheme(theme: string)` method
- Add `setMode(mode: "dark" | "light")` method
- Persist theme and mode to localStorage
- Step 5.5: Add system theme detection
- Detect terminal background and foreground colors
- Generate system theme based on terminal palette
- Handle system theme preference changes
tests:
- Unit:
- Test theme state initialization
- Test theme switching logic
- Test mode switching logic
- Test system theme detection
- Test localStorage persistence
- Integration/e2e:
- Test theme context in component tree
- Test reactive theme updates
- Test system theme changes
acceptance_criteria:
- `src/context/ThemeContext.tsx` uses SolidJS pattern
- Theme context provides reactive theme state
- Theme switching works correctly
- System theme detection is functional
- Theme and mode are persisted to localStorage
validation:
- Run: `bun run typecheck` - Should pass
- Run: `bun test src/context/ThemeContext.test.ts`
- Test theme switching in application
- Test system theme detection
notes:
- Use opencode's ThemeProvider pattern as reference
- Follow SolidJS best practices for reactive state
- Ensure proper cleanup of effects and listeners
- Reference: `/home/mike/code/PodTui/opencode/packages/opencode/src/cli/cmd/tui/context/theme.tsx` (lines 279-392)

View File

@@ -1,79 +0,0 @@
# 06. Add Theme Loader for JSON Files and Custom Themes
meta:
id: theme-refactoring-06
feature: theme-refactoring-json-format
priority: P1
depends_on: [theme-refactoring-02, theme-refactoring-04]
tags: [implementation, loading]
objective:
- Create theme loader to load JSON theme files
- Support loading custom themes from multiple directories
- Provide API for theme discovery and loading
- Handle theme file validation
deliverables:
- `src/utils/theme-loader.ts` - Theme loader utilities
- `src/utils/custom-themes.ts` - Custom theme loading logic
steps:
- Step 6.1: Create `src/utils/theme-loader.ts`
- Implement `loadTheme(name: string): Promise<ThemeJson>`
- Implement `loadThemeFromPath(path: string): Promise<ThemeJson>`
- Implement `getAllThemes(): Promise<Record<string, ThemeJson>>`
- Add error handling for missing or invalid theme files
- Step 6.2: Create `src/utils/custom-themes.ts`
- Implement `getCustomThemes()` function
- Scan for theme files in multiple directories:
- `~/.config/podtui/themes/`
- `./.podtui/themes/`
- Project root `./themes/`
- Support custom theme files with `.json` extension
- Return merged theme registry
- Step 6.3: Add theme file validation
- Validate theme JSON structure
- Check required properties (`defs`, `theme`)
- Validate color references in `defs`
- Add warning for optional properties
- Step 6.4: Implement theme discovery
- List all available theme files
- Provide theme metadata (name, description)
- Support theme aliases (e.g., "catppuccin" -> "catppuccin.json")
tests:
- Unit:
- Test `loadTheme` with existing theme files
- Test `loadTheme` with missing theme files
- Test `loadThemeFromPath` with custom paths
- Test `getAllThemes` returns all available themes
- Test `getCustomThemes` scans multiple directories
- Test theme file validation
- Integration/e2e:
- Test loading all available themes
- Test custom theme loading from directories
- Verify theme discovery works correctly
acceptance_criteria:
- `src/utils/theme-loader.ts` file exists with loading functions
- `src/utils/custom-themes.ts` file exists with custom theme logic
- Custom themes can be loaded from multiple directories
- Theme validation prevents invalid files
- Theme discovery API is functional
validation:
- Run: `bun run typecheck` - Should pass
- Run: `bun test src/utils/theme-loader.test.ts`
- Run: `bun test src/utils/custom-themes.test.ts`
- Test loading all available themes manually
- Test custom theme loading from directories
notes:
- Use opencode's `getCustomThemes` pattern as reference
- Support both local and global theme directories
- Add comprehensive error messages for invalid theme files
- Reference: `/home/mike/code/PodTui/opencode/packages/opencode/src/cli/cmd/tui/context/theme.tsx` (lines 394-419)

View File

@@ -1,83 +0,0 @@
# 07. Implement System Theme Detection with Terminal Palette
meta:
id: theme-refactoring-07
feature: theme-refactoring-json-format
priority: P1
depends_on: [theme-refactoring-04, theme-refactoring-06]
tags: [implementation, detection]
objective:
- Implement system theme detection based on terminal palette
- Generate system theme from terminal colors
- Detect light/dark mode based on terminal background
- Handle terminal color palette limitations
deliverables:
- `src/utils/system-theme.ts` - System theme detection utilities
- `src/utils/color-generation.ts` - Color generation helpers
steps:
- Step 7.1: Create `src/utils/system-theme.ts`
- Implement `detectSystemTheme()` function
- Detect terminal background and foreground colors
- Determine light/dark mode based on luminance
- Return detected theme information
- Step 7.2: Implement terminal palette detection
- Detect terminal colors using `@opentui/core` renderer
- Get default background and foreground colors
- Extract color palette (16 standard colors)
- Handle missing or invalid palette data
- Step 7.3: Create `src/utils/color-generation.ts`
- Implement `generateGrayScale(bg: RGBA, isDark: boolean): Record<number, RGBA>`
- Implement `generateMutedTextColor(bg: RGBA, isDark: boolean): RGBA`
- Implement `tint(base: RGBA, overlay: RGBA, alpha: number): RGBA`
- Generate gray scale based on background luminance
- Generate muted text colors for readability
- Step 7.4: Create system theme JSON generator
- Implement `generateSystemTheme(colors: TerminalColors, mode: "dark" | "light"): ThemeJson`
- Use ANSI color references for primary colors
- Generate appropriate background colors
- Generate diff colors with alpha blending
- Generate markdown and syntax colors
- Step 7.5: Add system theme caching
- Cache terminal palette detection results
- Handle palette cache invalidation
- Support manual cache clear on SIGUSR2
tests:
- Unit:
- Test `detectSystemTheme` returns correct mode
- Test `generateGrayScale` produces correct grays
- Test `generateMutedTextColor` produces readable colors
- Test `tint` produces correct blended colors
- Test `generateSystemTheme` produces valid theme JSON
- Integration/e2e:
- Test system theme detection in terminal
- Test theme generation with actual terminal palette
- Verify light/dark mode detection is accurate
acceptance_criteria:
- `src/utils/system-theme.ts` file exists with detection functions
- `src/utils/color-generation.ts` file exists with generation helpers
- System theme detection works correctly
- Light/dark mode detection is accurate
- Theme generation produces valid JSON
validation:
- Run: `bun run typecheck` - Should pass
- Run: `bun test src/utils/system-theme.test.ts`
- Run: `bun test src/utils/color-generation.test.ts`
- Test system theme detection manually in terminal
- Verify theme colors are readable
notes:
- Use opencode's system theme detection as reference
- Handle terminal transparency gracefully
- Add fallback for terminals without palette support
- Reference: `/home/mike/code/PodTui/opencode/packages/opencode/src/cli/cmd/tui/context/theme.tsx` (lines 428-535)

View File

@@ -1,83 +0,0 @@
# 08. Add Syntax Highlighting Integration
meta:
id: theme-refactoring-08
feature: theme-refactoring-json-format
priority: P1
depends_on: [theme-refactoring-04, theme-refactoring-05]
tags: [implementation, syntax]
objective:
- Add syntax highlighting support using theme colors
- Generate syntax rules from theme definitions
- Support markdown syntax highlighting
- Integrate with OpenTUI syntax highlighting
deliverables:
- `src/utils/syntax-highlighter.ts` - Syntax highlighting utilities
- `src/utils/syntax-rules.ts` - Syntax rule generation
steps:
- Step 8.1: Create `src/utils/syntax-rules.ts`
- Implement `getSyntaxRules(theme: ThemeColors)` function
- Define syntax scopes and their mappings
- Map theme colors to syntax scopes:
- Default text
- Keywords (return, conditional, repeat, coroutine)
- Types (function, class, module)
- Variables (parameter, member, builtin)
- Strings, numbers, booleans
- Comments
- Operators and punctuation
- Markdown-specific scopes (headings, bold, italic, links)
- Diff scopes (added, removed, context)
- Add style properties (foreground, bold, italic, underline)
- Step 8.2: Create `src/utils/syntax-highlighter.ts`
- Implement `generateSyntax(theme: ThemeColors)` function
- Implement `generateSubtleSyntax(theme: ThemeColors)` function
- Apply opacity to syntax colors for subtle highlighting
- Use theme's `thinkingOpacity` property
- Step 8.3: Integrate with OpenTUI syntax highlighting
- Import `SyntaxStyle` from `@opentui/core`
- Use `SyntaxStyle.fromTheme()` to create syntax styles
- Apply syntax styles to code components
- Step 8.4: Add syntax scope mappings
- Map common programming language scopes
- Map markdown and markup scopes
- Map diff and git scopes
- Add scope aliases for common patterns
tests:
- Unit:
- Test `getSyntaxRules` generates correct rules
- Test `generateSyntax` creates valid syntax styles
- Test `generateSubtleSyntax` applies opacity correctly
- Test syntax rules cover all expected scopes
- Integration/e2e:
- Test syntax highlighting with different themes
- Verify syntax colors match theme definitions
- Test markdown highlighting
acceptance_criteria:
- `src/utils/syntax-rules.ts` file exists with rule generation
- `src/utils/syntax-highlighter.ts` file exists with style generation
- Syntax highlighting works with theme colors
- Markdown highlighting is supported
- Syntax rules cover common programming patterns
validation:
- Run: `bun run typecheck` - Should pass
- Run: `bun test src/utils/syntax-rules.test.ts`
- Run: `bun test src/utils/syntax-highlighter.test.ts`
- Test syntax highlighting in application
- Verify syntax colors are readable
notes:
- Use opencode's syntax rule generation as reference
- Include comprehensive scope mappings
- Support common programming languages
- Reference: `/home/mike/code/PodTui/opencode/packages/opencode/src/cli/cmd/tui/context/theme.tsx` (lines 622-1152)

View File

@@ -1,77 +0,0 @@
# 09. Update Theme Utilities and CSS Variable Application
meta:
id: theme-refactoring-09
feature: theme-refactoring-json-format
priority: P1
depends_on: [theme-refactoring-04]
tags: [implementation, utilities]
objective:
- Update existing theme utilities to work with JSON theme structure
- Refactor CSS variable application logic
- Add support for theme color references
- Ensure backward compatibility with existing components
deliverables:
- `src/utils/theme.ts` - Updated theme utilities
- `src/utils/theme-css.ts` - CSS variable application utilities
steps:
- Step 9.1: Update `src/utils/theme.ts`
- Refactor `applyTheme()` to accept `ThemeColors` type
- Keep existing function signature for backward compatibility
- Update to work with resolved theme colors
- Step 9.2: Create `src/utils/theme-css.ts`
- Implement `applyThemeToCSS(theme: ThemeColors)` function
- Apply theme colors as CSS custom properties
- Support all theme color properties
- Handle layer backgrounds if present
- Step 9.3: Update theme attribute handling
- Implement `setThemeAttribute(themeName: string)` function
- Update to use data-theme attribute
- Support system theme attribute
- Step 9.4: Add color reference support
- Implement `resolveColorReference(color: string): string` function
- Convert color references to CSS values
- Handle hex colors, color references, and RGBA
- Step 9.5: Add theme utility functions
- Implement `getThemeByName(name: string): ThemeJson | undefined`
- Implement `getDefaultTheme(): ThemeJson`
- Implement `getAllThemes(): ThemeJson[]`
tests:
- Unit:
- Test `applyThemeToCSS` applies all colors correctly
- Test `setThemeAttribute` sets attribute correctly
- Test `resolveColorReference` converts references correctly
- Test theme utility functions return correct results
- Integration/e2e:
- Test theme application in browser
- Verify CSS variables are updated correctly
- Test theme attribute changes
acceptance_criteria:
- `src/utils/theme.ts` is updated and backward compatible
- `src/utils/theme-css.ts` file exists with CSS utilities
- CSS variables are applied correctly
- Color references are resolved properly
- Theme utilities are functional
validation:
- Run: `bun run typecheck` - Should pass
- Run: `bun test src/utils/theme.test.ts`
- Run: `bun test src/utils/theme-css.test.ts`
- Test theme application in browser
- Verify CSS variables are applied correctly
notes:
- Maintain backward compatibility with existing code
- Use CSS custom properties for theming
- Ensure all theme colors are applied
- Reference: `/home/mike/code/PodTui/src/utils/theme.ts` (original file)

View File

@@ -1,80 +0,0 @@
# 10. Test Theme Switching and Light/Dark Mode
meta:
id: theme-refactoring-10
feature: theme-refactoring-json-format
priority: P0
depends_on: [theme-refactoring-05, theme-refactoring-09]
tags: [testing, verification]
objective:
- Comprehensive testing of theme switching functionality
- Verify light/dark mode switching works correctly
- Test all theme JSON files load and apply correctly
- Ensure theme persistence works across sessions
deliverables:
- `src/utils/theme.test.ts` - Theme utility tests
- `src/context/ThemeContext.test.ts` - Context tests
- Test results showing all themes work correctly
steps:
- Step 10.1: Create `src/utils/theme.test.ts`
- Test theme loading functions
- Test theme resolution logic
- Test color reference resolution
- Test ANSI color conversion
- Step 10.2: Create `src/context/ThemeContext.test.ts`
- Test theme context initialization
- Test theme switching
- Test mode switching
- Test system theme detection
- Test localStorage persistence
- Test reactive theme updates
- Step 10.3: Manual testing
- Test switching between all themes (catppuccin, gruvbox, tokyo, nord)
- Test light/dark mode switching
- Test system theme detection
- Test theme persistence (close and reopen app)
- Test custom theme loading
- Step 10.4: Visual verification
- Verify all theme colors are correct
- Check readability of text colors
- Verify background colors are appropriate
- Check that all UI elements use theme colors
tests:
- Unit:
- Run all theme utility tests
- Run all theme context tests
- Verify all tests pass
- Integration/e2e:
- Test theme switching in application
- Test light/dark mode switching
- Test theme persistence
- Test system theme detection
acceptance_criteria:
- All unit tests pass
- All integration tests pass
- Theme switching works correctly
- Light/dark mode switching works correctly
- All themes load and apply correctly
- Theme persistence works across sessions
validation:
- Run: `bun test src/utils/theme.test.ts`
- Run: `bun test src/context/ThemeContext.test.ts`
- Run: `bun test` - Run all tests
- Manual testing of all themes
- Visual verification of theme appearance
notes:
- Test with actual terminal to verify system theme detection
- Verify all theme colors are visually appealing
- Check for any color contrast issues
- Test edge cases (missing themes, invalid colors)

View File

@@ -1,77 +0,0 @@
# 11. Verify Custom Theme Loading and Persistence
meta:
id: theme-refactoring-11
feature: theme-refactoring-json-format
priority: P1
depends_on: [theme-refactoring-06, theme-refactoring-10]
tags: [testing, verification]
objective:
- Test custom theme loading from directories
- Verify theme persistence works correctly
- Test custom theme switching
- Ensure custom themes are loaded on app start
deliverables:
- Test results for custom theme loading
- Documentation for custom theme format
- Verification that custom themes work correctly
steps:
- Step 11.1: Create test theme files
- Create test theme in `~/.config/podtui/themes/`
- Create test theme in `./.podtui/themes/`
- Create test theme in `./themes/`
- Step 11.2: Test custom theme loading
- Start application and verify custom themes are loaded
- Switch to custom theme
- Verify custom theme applies correctly
- Step 11.3: Test theme persistence
- Set custom theme
- Close application
- Reopen application
- Verify custom theme is still selected
- Step 11.4: Test theme discovery
- List all available themes
- Verify custom themes appear in list
- Test switching to custom themes
- Step 11.5: Test invalid theme handling
- Create invalid theme JSON
- Verify error is handled gracefully
- Verify app doesn't crash
tests:
- Unit:
- Test custom theme loading functions
- Test theme discovery
- Test invalid theme handling
- Integration/e2e:
- Test custom theme loading from directories
- Test theme persistence
- Test theme discovery
- Test invalid theme handling
acceptance_criteria:
- Custom themes can be loaded from directories
- Custom themes persist across sessions
- Custom themes appear in theme list
- Invalid themes are handled gracefully
- Theme discovery works correctly
validation:
- Run: `bun test src/utils/custom-themes.test.ts`
- Run: `bun test` - Run all tests
- Manual testing of custom themes
- Verify themes persist after restart
notes:
- Create documentation for custom theme format
- Reference: `/home/mike/code/PodTui/opencode/packages/opencode/src/cli/cmd/tui/context/theme.tsx` (lines 394-419)
- Test with multiple custom themes
- Verify all custom themes work correctly