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 { useNavigation } from "@/context/NavigationContext";
import { useScrollIntoView } from "@/hooks/useScrollIntoView";
import { useSelectionMarker } from "@/hooks/useSelectionMarker";
import { TABS } from "@/utils/navigation";
import { NF_ICONS, supportsNerdFonts } from "@/utils/nerd-fonts";
@@ -51,6 +52,7 @@ export function TabListPane(props: { muted?: boolean }) {
const nerd = supportsNerdFonts();
const { theme } = useTheme();
const nav = useNavigation();
const marker = useSelectionMarker();
const cursor = () => nav.tabCursor();
const activeTab = () => nav.activeTab();
@@ -104,7 +106,7 @@ export function TabListPane(props: { muted?: boolean }) {
}}
>
{/* ── selection marker (j/k cursor) ─────────────────────────── */}
<text fg={focusFg(tab)}>{isCursor() ? "" : " "}</text>
<text fg={focusFg(tab)}>{isCursor() ? marker() : " "}</text>
{nerd && (
<text fg={focusFg(tab)} paddingRight={1}>
{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 { LoadingIndicator } from "@/components/LoadingIndicator";
import { useScrollIntoView } from "@/hooks/useScrollIntoView";
import { useSelectionMarker } from "@/hooks/useSelectionMarker";
export const DiscoverPaneCount = 1;
@@ -43,6 +44,7 @@ function DiscoverPage() {
const { theme } = useTheme();
const muted = () => theme.muted || theme.text;
const nav = useNavigation();
const marker = useSelectionMarker();
const depth = nav.currentDepth;
const focus = (d: number = depth()) => nav.depthFocus(d);
@@ -182,12 +184,11 @@ function DiscoverPage() {
ref={ref}
flexDirection="row"
gap={1}
paddingLeft={1}
paddingRight={1}
backgroundColor={focusBg(index(), lf(), false)}
>
<text fg={focusFg(index(), nav.depthFocus(0), false)}>
{index() === nav.depthFocus(0) ? "" : " "}
{index() === nav.depthFocus(0) ? marker() : " "}
</text>
{nerd && (
<text fg={focusFg(index(), nav.depthFocus(0), false)}>
@@ -219,7 +220,6 @@ function DiscoverPage() {
ref={ref}
flexDirection="row"
gap={1}
paddingLeft={1}
paddingRight={1}
backgroundColor={focusBg(index(), lf(), isActive())}
onMouseDown={() => {
@@ -229,7 +229,7 @@ function DiscoverPage() {
}}
>
<text fg={focusFg(index(), lf(), isActive())}>
{index() === lf() ? "" : " "}
{index() === lf() ? marker() : " "}
</text>
{nerd && (
<text fg={focusFg(index(), lf(), isActive())}>
@@ -268,7 +268,6 @@ function DiscoverPage() {
ref={ref}
flexDirection="column"
gap={0}
paddingLeft={1}
paddingRight={1}
backgroundColor={focusBg(index(), lf(), isActive())}
onMouseDown={() => {
@@ -278,7 +277,7 @@ function DiscoverPage() {
>
<box flexDirection="row" gap={1}>
<text fg={focusFg(index(), lf(), isActive())}>
{index() === lf() ? "" : " "}
{index() === lf() ? marker() : " "}
</text>
<text fg={focusFg(index(), lf(), isActive())}>
{podcast.title}

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ import { useFeedStore } from "@/stores/feed";
import { useTheme } from "@/context/ThemeContext";
import { useInputFocusNav } from "@/hooks/useInputFocusNav";
import { useScrollIntoView } from "@/hooks/useScrollIntoView";
import { useSelectionMarker } from "@/hooks/useSelectionMarker";
import {
NavMode,
useNavigation,
@@ -88,6 +89,18 @@ export function usePreferencesItems(): SettingItem[] {
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",
label: "Font Size",
@@ -427,6 +440,7 @@ function WhitelistEditor() {
const focused = () =>
!nav.inputFocused() && index() === wlCursorClamped();
const ref = useScrollIntoView(focused);
const marker = useSelectionMarker();
const bg = () => (focused() ? theme.primary : undefined);
const fg = () => (focused() ? theme.surface : theme.text);
return (
@@ -434,7 +448,6 @@ function WhitelistEditor() {
ref={ref}
flexDirection="row"
gap={1}
paddingLeft={1}
paddingRight={1}
backgroundColor={bg()}
onMouseDown={() => {
@@ -445,7 +458,7 @@ function WhitelistEditor() {
wlToggle(feed.id);
}}
>
<text fg={fg()}>{focused() ? "" : " "}</text>
<text fg={fg()}>{focused() ? marker() : " "}</text>
<text fg={fg()}>{inList(feed.id) ? "●" : "○"}</text>
<text fg={fg()}>
{feed.customName || feed.podcast.title}

View File

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

View File

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

View File

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

View File

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