feat(settings): configurable selection marker, default off

Adds a Selection Marker toggle under Settings → Preferences that
controls the ❯ cursor glyph on the focused row of every list.
Default off; rows keep a leading space for alignment either way.
Every list pane (tab strip, feed, my shows, discover, search,
settings, whitelist editor) reads the glyph through the shared
useSelectionMarker hook.

Also aligns the lead column across panes: page list rows drop
their extra box paddingLeft so labels start at the same column
as the tab strip (2 cells without nerd icons, 4 with), keeping
episode sub-rows aligned to the title.
This commit is contained in:
2026-08-10 19:07:16 -04:00
parent 93d5925dfd
commit 6134dea044
11 changed files with 59 additions and 25 deletions

View File

@@ -19,6 +19,7 @@ import { For } from "solid-js";
import { useTheme } from "@/context/ThemeContext"; import { useTheme } from "@/context/ThemeContext";
import { useNavigation } from "@/context/NavigationContext"; import { useNavigation } from "@/context/NavigationContext";
import { useScrollIntoView } from "@/hooks/useScrollIntoView"; import { useScrollIntoView } from "@/hooks/useScrollIntoView";
import { useSelectionMarker } from "@/hooks/useSelectionMarker";
import { TABS } from "@/utils/navigation"; import { TABS } from "@/utils/navigation";
import { NF_ICONS, supportsNerdFonts } from "@/utils/nerd-fonts"; import { NF_ICONS, supportsNerdFonts } from "@/utils/nerd-fonts";
@@ -51,6 +52,7 @@ export function TabListPane(props: { muted?: boolean }) {
const nerd = supportsNerdFonts(); const nerd = supportsNerdFonts();
const { theme } = useTheme(); const { theme } = useTheme();
const nav = useNavigation(); const nav = useNavigation();
const marker = useSelectionMarker();
const cursor = () => nav.tabCursor(); const cursor = () => nav.tabCursor();
const activeTab = () => nav.activeTab(); const activeTab = () => nav.activeTab();
@@ -104,7 +106,7 @@ export function TabListPane(props: { muted?: boolean }) {
}} }}
> >
{/* ── selection marker (j/k cursor) ─────────────────────────── */} {/* ── selection marker (j/k cursor) ─────────────────────────── */}
<text fg={focusFg(tab)}>{isCursor() ? "" : " "}</text> <text fg={focusFg(tab)}>{isCursor() ? marker() : " "}</text>
{nerd && ( {nerd && (
<text fg={focusFg(tab)} paddingRight={1}> <text fg={focusFg(tab)} paddingRight={1}>
{TAB_ICON[tab]} {TAB_ICON[tab]}

View File

@@ -0,0 +1,16 @@
/**
* useSelectionMarker — reactive accessor for the row-selection marker glyph.
*
* When the `showSelectionMarker` setting is on, the focused row of every list
* renders ``; when off (the default), it renders a space so column alignment
* is preserved. Every list pane in the app reads the marker through this hook
* so the setting applies consistently everywhere.
*/
import { useAppStore } from "@/stores/app";
export function useSelectionMarker(): () => string {
const app = useAppStore();
return () =>
app.state().settings.showSelectionMarker ? "" : " ";
}

View File

@@ -33,6 +33,7 @@ import { PaneRow } from "@/components/PaneRow";
import { TabListPane } from "@/components/TabPanel"; import { TabListPane } from "@/components/TabPanel";
import { LoadingIndicator } from "@/components/LoadingIndicator"; import { LoadingIndicator } from "@/components/LoadingIndicator";
import { useScrollIntoView } from "@/hooks/useScrollIntoView"; import { useScrollIntoView } from "@/hooks/useScrollIntoView";
import { useSelectionMarker } from "@/hooks/useSelectionMarker";
export const DiscoverPaneCount = 1; export const DiscoverPaneCount = 1;
@@ -43,6 +44,7 @@ function DiscoverPage() {
const { theme } = useTheme(); const { theme } = useTheme();
const muted = () => theme.muted || theme.text; const muted = () => theme.muted || theme.text;
const nav = useNavigation(); const nav = useNavigation();
const marker = useSelectionMarker();
const depth = nav.currentDepth; const depth = nav.currentDepth;
const focus = (d: number = depth()) => nav.depthFocus(d); const focus = (d: number = depth()) => nav.depthFocus(d);
@@ -182,12 +184,11 @@ function DiscoverPage() {
ref={ref} ref={ref}
flexDirection="row" flexDirection="row"
gap={1} gap={1}
paddingLeft={1}
paddingRight={1} paddingRight={1}
backgroundColor={focusBg(index(), lf(), false)} backgroundColor={focusBg(index(), lf(), false)}
> >
<text fg={focusFg(index(), nav.depthFocus(0), false)}> <text fg={focusFg(index(), nav.depthFocus(0), false)}>
{index() === nav.depthFocus(0) ? "" : " "} {index() === nav.depthFocus(0) ? marker() : " "}
</text> </text>
{nerd && ( {nerd && (
<text fg={focusFg(index(), nav.depthFocus(0), false)}> <text fg={focusFg(index(), nav.depthFocus(0), false)}>
@@ -219,7 +220,6 @@ function DiscoverPage() {
ref={ref} ref={ref}
flexDirection="row" flexDirection="row"
gap={1} gap={1}
paddingLeft={1}
paddingRight={1} paddingRight={1}
backgroundColor={focusBg(index(), lf(), isActive())} backgroundColor={focusBg(index(), lf(), isActive())}
onMouseDown={() => { onMouseDown={() => {
@@ -229,7 +229,7 @@ function DiscoverPage() {
}} }}
> >
<text fg={focusFg(index(), lf(), isActive())}> <text fg={focusFg(index(), lf(), isActive())}>
{index() === lf() ? "" : " "} {index() === lf() ? marker() : " "}
</text> </text>
{nerd && ( {nerd && (
<text fg={focusFg(index(), lf(), isActive())}> <text fg={focusFg(index(), lf(), isActive())}>
@@ -268,7 +268,6 @@ function DiscoverPage() {
ref={ref} ref={ref}
flexDirection="column" flexDirection="column"
gap={0} gap={0}
paddingLeft={1}
paddingRight={1} paddingRight={1}
backgroundColor={focusBg(index(), lf(), isActive())} backgroundColor={focusBg(index(), lf(), isActive())}
onMouseDown={() => { onMouseDown={() => {
@@ -278,7 +277,7 @@ function DiscoverPage() {
> >
<box flexDirection="row" gap={1}> <box flexDirection="row" gap={1}>
<text fg={focusFg(index(), lf(), isActive())}> <text fg={focusFg(index(), lf(), isActive())}>
{index() === lf() ? "" : " "} {index() === lf() ? marker() : " "}
</text> </text>
<text fg={focusFg(index(), lf(), isActive())}> <text fg={focusFg(index(), lf(), isActive())}>
{podcast.title} {podcast.title}

View File

@@ -40,6 +40,7 @@ import { LoadingIndicator } from "@/components/LoadingIndicator";
import { PaneRow } from "@/components/PaneRow"; import { PaneRow } from "@/components/PaneRow";
import { TabListPane } from "@/components/TabPanel"; import { TabListPane } from "@/components/TabPanel";
import { useScrollIntoView } from "@/hooks/useScrollIntoView"; import { useScrollIntoView } from "@/hooks/useScrollIntoView";
import { useSelectionMarker } from "@/hooks/useSelectionMarker";
export const FeedPaneCount = 1; export const FeedPaneCount = 1;
@@ -55,6 +56,7 @@ function FeedPage() {
const { theme } = useTheme(); const { theme } = useTheme();
const muted = () => theme.muted || theme.text; const muted = () => theme.muted || theme.text;
const nav = useNavigation(); const nav = useNavigation();
const marker = useSelectionMarker();
// ── flat episode list (depth 0 — the only depth Feed has) ──────────────── // ── flat episode list (depth 0 — the only depth Feed has) ────────────────
const episodes = createMemo<EpItem[]>( const episodes = createMemo<EpItem[]>(
@@ -257,7 +259,6 @@ function FeedPage() {
ref={ref} ref={ref}
flexDirection="column" flexDirection="column"
gap={0} gap={0}
paddingLeft={1}
paddingRight={1} paddingRight={1}
backgroundColor={focusBg(index(), fi(), isActive())} backgroundColor={focusBg(index(), fi(), isActive())}
onMouseDown={() => { onMouseDown={() => {
@@ -267,7 +268,7 @@ function FeedPage() {
> >
<box flexDirection="row" gap={1}> <box flexDirection="row" gap={1}>
<text fg={focusFg(index(), fi(), isActive())}> <text fg={focusFg(index(), fi(), isActive())}>
{index() === fi() ? "" : " "} {index() === fi() ? marker() : " "}
</text> </text>
<text fg={focusFg(index(), fi(), isActive())}> <text fg={focusFg(index(), fi(), isActive())}>
{item.episode.episodeNumber {item.episode.episodeNumber
@@ -304,7 +305,6 @@ function FeedPage() {
ref={moreRef} ref={moreRef}
flexDirection="row" flexDirection="row"
gap={1} gap={1}
paddingLeft={1}
paddingRight={1} paddingRight={1}
backgroundColor={focusBg(episodes().length, focusedRow(), isActive())} backgroundColor={focusBg(episodes().length, focusedRow(), isActive())}
onMouseDown={() => { onMouseDown={() => {
@@ -313,7 +313,7 @@ function FeedPage() {
}} }}
> >
<text fg={focusFg(episodes().length, focusedRow(), isActive())}> <text fg={focusFg(episodes().length, focusedRow(), isActive())}>
{focusedOnMore() ? "" : " "} {focusedOnMore() ? marker() : " "}
</text> </text>
{nerd && ( {nerd && (
<text fg={focusFg(episodes().length, focusedRow(), isActive())}> <text fg={focusFg(episodes().length, focusedRow(), isActive())}>

View File

@@ -35,6 +35,7 @@ import { LoadingIndicator } from "@/components/LoadingIndicator";
import { PaneRow } from "@/components/PaneRow"; import { PaneRow } from "@/components/PaneRow";
import { TabListPane } from "@/components/TabPanel"; import { TabListPane } from "@/components/TabPanel";
import { useScrollIntoView } from "@/hooks/useScrollIntoView"; import { useScrollIntoView } from "@/hooks/useScrollIntoView";
import { useSelectionMarker } from "@/hooks/useSelectionMarker";
export const MyShowsPaneCount = 1; export const MyShowsPaneCount = 1;
@@ -47,6 +48,7 @@ export function MyShowsPage() {
const { theme } = useTheme(); const { theme } = useTheme();
const muted = () => theme.muted || theme.text; const muted = () => theme.muted || theme.text;
const nav = useNavigation(); const nav = useNavigation();
const marker = useSelectionMarker();
const stack = nav.depthStack; const stack = nav.depthStack;
const depth = nav.currentDepth; const depth = nav.currentDepth;
@@ -254,12 +256,11 @@ export function MyShowsPage() {
ref={ref} ref={ref}
flexDirection="row" flexDirection="row"
gap={1} gap={1}
paddingLeft={1}
paddingRight={1} paddingRight={1}
backgroundColor={focusBg(index(), lf(), false)} backgroundColor={focusBg(index(), lf(), false)}
> >
<text fg={focusFg(index(), lf(), false)}> <text fg={focusFg(index(), lf(), false)}>
{index() === lf() ? "" : " "} {index() === lf() ? marker() : " "}
</text> </text>
<text fg={focusFg(index(), lf(), false)}>{showTitle(feed)}</text> <text fg={focusFg(index(), lf(), false)}>{showTitle(feed)}</text>
<text fg={muted()}>({feed.episodes.length})</text> <text fg={muted()}>({feed.episodes.length})</text>
@@ -299,7 +300,6 @@ export function MyShowsPage() {
ref={ref} ref={ref}
flexDirection="row" flexDirection="row"
gap={1} gap={1}
paddingLeft={1}
paddingRight={1} paddingRight={1}
backgroundColor={focusBg(index(), lf(), isActive())} backgroundColor={focusBg(index(), lf(), isActive())}
onMouseDown={() => { onMouseDown={() => {
@@ -308,7 +308,7 @@ export function MyShowsPage() {
}} }}
> >
<text fg={focusFg(index(), lf(), isActive())}> <text fg={focusFg(index(), lf(), isActive())}>
{index() === lf() ? "" : " "} {index() === lf() ? marker() : " "}
</text> </text>
<text fg={focusFg(index(), lf(), isActive())}> <text fg={focusFg(index(), lf(), isActive())}>
{showTitle(feed)} {showTitle(feed)}
@@ -354,7 +354,6 @@ export function MyShowsPage() {
ref={ref} ref={ref}
flexDirection="column" flexDirection="column"
gap={0} gap={0}
paddingLeft={1}
paddingRight={1} paddingRight={1}
backgroundColor={focusBg(index(), lf(), isActive())} backgroundColor={focusBg(index(), lf(), isActive())}
onMouseDown={() => { onMouseDown={() => {
@@ -364,7 +363,7 @@ export function MyShowsPage() {
> >
<box flexDirection="row" gap={1}> <box flexDirection="row" gap={1}>
<text fg={focusFg(index(), lf(), isActive())}> <text fg={focusFg(index(), lf(), isActive())}>
{index() === lf() ? "" : " "} {index() === lf() ? marker() : " "}
</text> </text>
<text fg={focusFg(index(), lf(), isActive())}> <text fg={focusFg(index(), lf(), isActive())}>
{ep.episodeNumber ? `#${ep.episodeNumber} ` : ""} {ep.episodeNumber ? `#${ep.episodeNumber} ` : ""}

View File

@@ -42,6 +42,7 @@ import { PaneRow } from "@/components/PaneRow";
import { TabListPane } from "@/components/TabPanel"; import { TabListPane } from "@/components/TabPanel";
import { LoadingIndicator } from "@/components/LoadingIndicator"; import { LoadingIndicator } from "@/components/LoadingIndicator";
import { useScrollIntoView } from "@/hooks/useScrollIntoView"; import { useScrollIntoView } from "@/hooks/useScrollIntoView";
import { useSelectionMarker } from "@/hooks/useSelectionMarker";
export const SearchPaneCount = 1; export const SearchPaneCount = 1;
@@ -52,6 +53,7 @@ function SearchPage() {
const { theme } = useTheme(); const { theme } = useTheme();
const muted = () => theme.muted || theme.text; const muted = () => theme.muted || theme.text;
const nav = useNavigation(); const nav = useNavigation();
const marker = useSelectionMarker();
const stack = nav.depthStack; const stack = nav.depthStack;
const depth = nav.currentDepth; const depth = nav.currentDepth;
@@ -288,7 +290,6 @@ function SearchPage() {
ref={ref} ref={ref}
flexDirection="row" flexDirection="row"
gap={1} gap={1}
paddingLeft={1}
paddingRight={1} paddingRight={1}
backgroundColor={ backgroundColor={
typing() typing()
@@ -311,7 +312,7 @@ function SearchPage() {
: focusFg(index(), lf(), isActive()) : focusFg(index(), lf(), isActive())
} }
> >
{index() === lf() && !typing() ? "" : " "} {index() === lf() && !typing() ? marker() : " "}
</text> </text>
<text <text
fg={ fg={
@@ -365,7 +366,6 @@ function SearchPage() {
ref={ref} ref={ref}
flexDirection="column" flexDirection="column"
gap={0} gap={0}
paddingLeft={1}
paddingRight={1} paddingRight={1}
backgroundColor={focusBg(index(), fi(), isActive())} backgroundColor={focusBg(index(), fi(), isActive())}
onMouseDown={() => { onMouseDown={() => {
@@ -375,7 +375,7 @@ function SearchPage() {
> >
<box flexDirection="row" gap={1}> <box flexDirection="row" gap={1}>
<text fg={focusFg(index(), fi(), isActive())}> <text fg={focusFg(index(), fi(), isActive())}>
{index() === fi() ? "" : " "} {index() === fi() ? marker() : " "}
</text> </text>
<text fg={focusFg(index(), fi(), isActive())}> <text fg={focusFg(index(), fi(), isActive())}>
{result.podcast.title} {result.podcast.title}

View File

@@ -20,6 +20,7 @@ import { useFeedStore } from "@/stores/feed";
import { useTheme } from "@/context/ThemeContext"; import { useTheme } from "@/context/ThemeContext";
import { useInputFocusNav } from "@/hooks/useInputFocusNav"; import { useInputFocusNav } from "@/hooks/useInputFocusNav";
import { useScrollIntoView } from "@/hooks/useScrollIntoView"; import { useScrollIntoView } from "@/hooks/useScrollIntoView";
import { useSelectionMarker } from "@/hooks/useSelectionMarker";
import { import {
NavMode, NavMode,
useNavigation, useNavigation,
@@ -88,6 +89,18 @@ export function usePreferencesItems(): SettingItem[] {
transparentBackground: !settings().transparentBackground, transparentBackground: !settings().transparentBackground,
}), }),
}, },
{
id: "showSelectionMarker",
label: "Selection Marker",
kind: "toggle",
display: () => (settings().showSelectionMarker ? "On" : "Off"),
help: () =>
`Show the marker on the focused row of every list (tabs, shows, episodes, results).\nType: toggle\nDefault: off\nCurrent: ${settings().showSelectionMarker ? "On" : "Off"}\nSpace/Enter to toggle.`,
toggle: () =>
app.updateSettings({
showSelectionMarker: !settings().showSelectionMarker,
}),
},
{ {
id: "fontSize", id: "fontSize",
label: "Font Size", label: "Font Size",
@@ -427,6 +440,7 @@ function WhitelistEditor() {
const focused = () => const focused = () =>
!nav.inputFocused() && index() === wlCursorClamped(); !nav.inputFocused() && index() === wlCursorClamped();
const ref = useScrollIntoView(focused); const ref = useScrollIntoView(focused);
const marker = useSelectionMarker();
const bg = () => (focused() ? theme.primary : undefined); const bg = () => (focused() ? theme.primary : undefined);
const fg = () => (focused() ? theme.surface : theme.text); const fg = () => (focused() ? theme.surface : theme.text);
return ( return (
@@ -434,7 +448,6 @@ function WhitelistEditor() {
ref={ref} ref={ref}
flexDirection="row" flexDirection="row"
gap={1} gap={1}
paddingLeft={1}
paddingRight={1} paddingRight={1}
backgroundColor={bg()} backgroundColor={bg()}
onMouseDown={() => { onMouseDown={() => {
@@ -445,7 +458,7 @@ function WhitelistEditor() {
wlToggle(feed.id); wlToggle(feed.id);
}} }}
> >
<text fg={fg()}>{focused() ? "" : " "}</text> <text fg={fg()}>{focused() ? marker() : " "}</text>
<text fg={fg()}>{inList(feed.id) ? "●" : "○"}</text> <text fg={fg()}>{inList(feed.id) ? "●" : "○"}</text>
<text fg={fg()}> <text fg={fg()}>
{feed.customName || feed.podcast.title} {feed.customName || feed.podcast.title}

View File

@@ -38,6 +38,7 @@ import { useDownloadItems } from "./DownloadManager";
import { PaneRow } from "@/components/PaneRow"; import { PaneRow } from "@/components/PaneRow";
import { TabListPane } from "@/components/TabPanel"; import { TabListPane } from "@/components/TabPanel";
import { useScrollIntoView } from "@/hooks/useScrollIntoView"; import { useScrollIntoView } from "@/hooks/useScrollIntoView";
import { useSelectionMarker } from "@/hooks/useSelectionMarker";
export const SettingsPaneCount = 1; export const SettingsPaneCount = 1;
@@ -436,17 +437,17 @@ function Row(props: {
? theme.selectedListItemText ?? theme.text ? theme.selectedListItemText ?? theme.text
: theme.text; : theme.text;
const ref = useScrollIntoView(() => props.focused); const ref = useScrollIntoView(() => props.focused);
const marker = useSelectionMarker();
return ( return (
<box <box
ref={ref} ref={ref}
flexDirection="row" flexDirection="row"
gap={1} gap={1}
paddingLeft={1}
paddingRight={1} paddingRight={1}
backgroundColor={bg()} backgroundColor={bg()}
onMouseDown={props.onMouseDown} onMouseDown={props.onMouseDown}
> >
<text fg={fg()}>{props.focused ? "" : " "}</text> <text fg={fg()}>{props.focused ? marker() : " "}</text>
{props.icon && nerd && <text fg={fg()}>{props.icon}</text>} {props.icon && nerd && <text fg={fg()}>{props.icon}</text>}
<text fg={fg()}>{props.label}</text> <text fg={fg()}>{props.label}</text>
<Show when={props.value}> <Show when={props.value}>

View File

@@ -30,6 +30,7 @@ const defaultSettings: AppSettings = {
playbackSpeed: 1, playbackSpeed: 1,
downloadPath: "", downloadPath: "",
transparentBackground: false, transparentBackground: false,
showSelectionMarker: false,
visualizer: defaultVisualizerSettings, visualizer: defaultVisualizerSettings,
}; };

View File

@@ -81,6 +81,8 @@ export type AppSettings = {
downloadPath: string; downloadPath: string;
/** Render the app background transparent (let the terminal's own bg show). */ /** Render the app background transparent (let the terminal's own bg show). */
transparentBackground: boolean; transparentBackground: boolean;
/** Show the `` cursor marker on the focused row of every list (default: off). */
showSelectionMarker: boolean;
visualizer: VisualizerSettings; visualizer: VisualizerSettings;
}; };

View File

@@ -33,6 +33,7 @@ const defaultSettings: AppSettings = {
playbackSpeed: 1, playbackSpeed: 1,
downloadPath: "", downloadPath: "",
transparentBackground: false, transparentBackground: false,
showSelectionMarker: false,
visualizer: defaultVisualizerSettings, visualizer: defaultVisualizerSettings,
}; };