feat(ui): nerd font icons on hard-defined list rows, graceful degrade

Tabs, Discover categories (replacing unused placeholder glyphs), Settings
sections, and the Feed 'Fetch More' row get Nerd Font glyphs (Font Awesome PUA
codepoints). When the terminal font isn't Nerd Font capable the glyphs render
nothing at all — no tofu, no layout gaps — via supportsNerdFonts() (env
allowlist + PODTUI_NERD_FONTS=1/0 override). Documented in README (Configuration
-> Fonts).
This commit is contained in:
2026-08-10 16:50:21 -04:00
parent 5dce21c038
commit b280af484c
8 changed files with 184 additions and 12 deletions

View File

@@ -27,6 +27,7 @@ import {
type DepthFrame,
} from "@/context/NavigationContext";
import { on, off } from "@/utils/event-bus";
import { supportsNerdFonts } from "@/utils/nerd-fonts";
import type { KeybindActionName } from "@/context/KeybindContext";
import { PaneRow } from "@/components/PaneRow";
import { TabListPane } from "@/components/TabPanel";
@@ -36,6 +37,8 @@ import { useScrollIntoView } from "@/hooks/useScrollIntoView";
export const DiscoverPaneCount = 1;
function DiscoverPage() {
// Static: detection never changes mid-session.
const nerd = supportsNerdFonts();
const discoverStore = useDiscoverStore();
const { theme } = useTheme();
const muted = () => theme.muted || theme.text;
@@ -186,6 +189,11 @@ function DiscoverPage() {
<text fg={focusFg(index(), nav.depthFocus(0), false)}>
{index() === nav.depthFocus(0) ? "" : " "}
</text>
{nerd && (
<text fg={focusFg(index(), nav.depthFocus(0), false)}>
{cat.icon}
</text>
)}
<text fg={focusFg(index(), nav.depthFocus(0), false)}>
{cat.name}
</text>
@@ -223,6 +231,11 @@ function DiscoverPage() {
<text fg={focusFg(index(), lf(), isActive())}>
{index() === lf() ? "" : " "}
</text>
{nerd && (
<text fg={focusFg(index(), lf(), isActive())}>
{cat.icon}
</text>
)}
<text fg={focusFg(index(), lf(), isActive())}>{cat.name}</text>
</box>
);

View File

@@ -32,6 +32,7 @@ import {
} from "@/context/NavigationContext";
import { useAudio } from "@/hooks/useAudio";
import { on, off } from "@/utils/event-bus";
import { NF_ICONS, supportsNerdFonts } from "@/utils/nerd-fonts";
import type { KeybindActionName } from "@/context/KeybindContext";
import type { Episode } from "@/types/episode";
import type { Feed } from "@/types/feed";
@@ -45,6 +46,8 @@ export const FeedPaneCount = 1;
type EpItem = { episode: Episode; feed: Feed };
function FeedPage() {
// Static: detection never changes mid-session.
const nerd = supportsNerdFonts();
const feedStore = useFeedStore();
const downloadStore = useDownloadStore();
const audioNav = useAudioNavStore();
@@ -312,6 +315,11 @@ function FeedPage() {
<text fg={focusFg(episodes().length, focusedRow(), isActive())}>
{focusedOnMore() ? "" : " "}
</text>
{nerd && (
<text fg={focusFg(episodes().length, focusedRow(), isActive())}>
{NF_ICONS.more}
</text>
)}
<Show
when={!feedStore.isLoadingMore()}
fallback={<LoadingIndicator label="Fetching…" />}

View File

@@ -27,6 +27,7 @@ import {
type PaneId,
} from "@/context/NavigationContext";
import { on, off } from "@/utils/event-bus";
import { NF_ICONS, supportsNerdFonts } from "@/utils/nerd-fonts";
import type { KeybindActionName } from "@/context/KeybindContext";
import type { SettingItem, SettingsSectionDef } from "./types";
import { usePreferencesItems } from "./PreferencesPanel";
@@ -45,29 +46,38 @@ const SECTIONS: SettingsSectionDef[] = [
id: 0,
label: "Sync",
description: "Import/export subscriptions and sync status.",
icon: NF_ICONS.sync,
},
{
id: 1,
label: "Sources",
description: "Podcast search/RSS sources — add, enable, remove.",
icon: NF_ICONS.sources,
},
{
id: 2,
label: "Preferences",
description: "Theme, font, playback speed, explicit/auto-download.",
icon: NF_ICONS.preferences,
},
{
id: 3,
label: "Visualizer",
description: "Audio visualizer: bars, sensitivity, cutoffs.",
icon: NF_ICONS.visualizer,
},
{
id: 4,
label: "Downloads",
description: "Manage downloaded episodes — delete by show or individually.",
icon: NF_ICONS.downloads,
},
];
// Static: detection never changes mid-session. Module-level because the Row
// component below (a sibling module function) needs it too.
const nerd = supportsNerdFonts();
/** Resolve the items for a section id at render time. */
function sectionItems(sectionId: number): SettingItem[] {
switch (sectionId) {
@@ -286,6 +296,7 @@ export function SettingsPage() {
{(section, index) => (
<Row
label={section.label}
icon={section.icon}
focused={index() === focusedSectionIdx()}
active={false}
/>
@@ -315,6 +326,7 @@ export function SettingsPage() {
{(section, index) => (
<Row
label={section.label}
icon={section.icon}
focused={index() === focusedSectionIdx()}
active={isActive()}
onMouseDown={() => {
@@ -407,6 +419,7 @@ function Row(props: {
focused: boolean;
active: boolean;
hint?: string;
icon?: string;
onMouseDown?: () => void;
}) {
const { theme } = useTheme();
@@ -434,6 +447,7 @@ function Row(props: {
onMouseDown={props.onMouseDown}
>
<text fg={fg()}>{props.focused ? "" : " "}</text>
{props.icon && nerd && <text fg={fg()}>{props.icon}</text>}
<text fg={fg()}>{props.label}</text>
<Show when={props.value}>
<box flexGrow={1} />

View File

@@ -41,5 +41,7 @@ export interface SettingsSectionDef {
id: number;
label: string;
description: string;
/** Nerd Font glyph for the section row (rendered only when supported). */
icon: string;
items?: () => SettingItem[];
}