From 6134dea044fbf70981987cf96c69831103812ab9 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Mon, 10 Aug 2026 19:07:16 -0400 Subject: [PATCH] feat(settings): configurable selection marker, default off MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/components/TabPanel.tsx | 4 +++- src/hooks/useSelectionMarker.ts | 16 ++++++++++++++++ src/pages/Discover/DiscoverPage.tsx | 11 +++++------ src/pages/Feed/FeedPage.tsx | 8 ++++---- src/pages/MyShows/MyShowsPage.tsx | 11 +++++------ src/pages/Search/SearchPage.tsx | 8 ++++---- src/pages/Settings/PreferencesPanel.tsx | 17 +++++++++++++++-- src/pages/Settings/SettingsPage.tsx | 5 +++-- src/stores/app.ts | 1 + src/types/settings.ts | 2 ++ src/utils/app-persistence.ts | 1 + 11 files changed, 59 insertions(+), 25 deletions(-) create mode 100644 src/hooks/useSelectionMarker.ts diff --git a/src/components/TabPanel.tsx b/src/components/TabPanel.tsx index a8e26da..eabebdf 100644 --- a/src/components/TabPanel.tsx +++ b/src/components/TabPanel.tsx @@ -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) ─────────────────────────── */} - {isCursor() ? "❯" : " "} + {isCursor() ? marker() : " "} {nerd && ( {TAB_ICON[tab]} diff --git a/src/hooks/useSelectionMarker.ts b/src/hooks/useSelectionMarker.ts new file mode 100644 index 0000000..f59ecbd --- /dev/null +++ b/src/hooks/useSelectionMarker.ts @@ -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 ? "❯" : " "; +} diff --git a/src/pages/Discover/DiscoverPage.tsx b/src/pages/Discover/DiscoverPage.tsx index a090b52..d31bf32 100644 --- a/src/pages/Discover/DiscoverPage.tsx +++ b/src/pages/Discover/DiscoverPage.tsx @@ -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)} > - {index() === nav.depthFocus(0) ? "❯" : " "} + {index() === nav.depthFocus(0) ? marker() : " "} {nerd && ( @@ -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() { }} > - {index() === lf() ? "❯" : " "} + {index() === lf() ? marker() : " "} {nerd && ( @@ -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() { > - {index() === lf() ? "❯" : " "} + {index() === lf() ? marker() : " "} {podcast.title} diff --git a/src/pages/Feed/FeedPage.tsx b/src/pages/Feed/FeedPage.tsx index a8aaf7b..404ec36 100644 --- a/src/pages/Feed/FeedPage.tsx +++ b/src/pages/Feed/FeedPage.tsx @@ -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( @@ -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() { > - {index() === fi() ? "❯" : " "} + {index() === fi() ? marker() : " "} {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() { }} > - {focusedOnMore() ? "❯" : " "} + {focusedOnMore() ? marker() : " "} {nerd && ( diff --git a/src/pages/MyShows/MyShowsPage.tsx b/src/pages/MyShows/MyShowsPage.tsx index 7dd6e1b..a1dac5a 100644 --- a/src/pages/MyShows/MyShowsPage.tsx +++ b/src/pages/MyShows/MyShowsPage.tsx @@ -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)} > - {index() === lf() ? "❯" : " "} + {index() === lf() ? marker() : " "} {showTitle(feed)} ({feed.episodes.length}) @@ -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() { }} > - {index() === lf() ? "❯" : " "} + {index() === lf() ? marker() : " "} {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() { > - {index() === lf() ? "❯" : " "} + {index() === lf() ? marker() : " "} {ep.episodeNumber ? `#${ep.episodeNumber} ` : ""} diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 5b30d40..e6c1842 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -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() : " "} { @@ -375,7 +375,7 @@ function SearchPage() { > - {index() === fi() ? "❯" : " "} + {index() === fi() ? marker() : " "} {result.podcast.title} diff --git a/src/pages/Settings/PreferencesPanel.tsx b/src/pages/Settings/PreferencesPanel.tsx index a4e38a2..9528876 100644 --- a/src/pages/Settings/PreferencesPanel.tsx +++ b/src/pages/Settings/PreferencesPanel.tsx @@ -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); }} > - {focused() ? "❯" : " "} + {focused() ? marker() : " "} {inList(feed.id) ? "●" : "○"} {feed.customName || feed.podcast.title} diff --git a/src/pages/Settings/SettingsPage.tsx b/src/pages/Settings/SettingsPage.tsx index b76b1db..7fd243d 100644 --- a/src/pages/Settings/SettingsPage.tsx +++ b/src/pages/Settings/SettingsPage.tsx @@ -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 ( - {props.focused ? "❯" : " "} + {props.focused ? marker() : " "} {props.icon && nerd && {props.icon}} {props.label} diff --git a/src/stores/app.ts b/src/stores/app.ts index f56a130..32d1223 100644 --- a/src/stores/app.ts +++ b/src/stores/app.ts @@ -30,6 +30,7 @@ const defaultSettings: AppSettings = { playbackSpeed: 1, downloadPath: "", transparentBackground: false, + showSelectionMarker: false, visualizer: defaultVisualizerSettings, }; diff --git a/src/types/settings.ts b/src/types/settings.ts index ceae72b..fced4e2 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -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; }; diff --git a/src/utils/app-persistence.ts b/src/utils/app-persistence.ts index a469ea2..bd87ab9 100644 --- a/src/utils/app-persistence.ts +++ b/src/utils/app-persistence.ts @@ -33,6 +33,7 @@ const defaultSettings: AppSettings = { playbackSpeed: 1, downloadPath: "", transparentBackground: false, + showSelectionMarker: false, visualizer: defaultVisualizerSettings, };