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:
@@ -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]}
|
||||
|
||||
16
src/hooks/useSelectionMarker.ts
Normal file
16
src/hooks/useSelectionMarker.ts
Normal 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 ? "❯" : " ";
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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())}>
|
||||
|
||||
@@ -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} ` : ""}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -30,6 +30,7 @@ const defaultSettings: AppSettings = {
|
||||
playbackSpeed: 1,
|
||||
downloadPath: "",
|
||||
transparentBackground: false,
|
||||
showSelectionMarker: false,
|
||||
visualizer: defaultVisualizerSettings,
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ const defaultSettings: AppSettings = {
|
||||
playbackSpeed: 1,
|
||||
downloadPath: "",
|
||||
transparentBackground: false,
|
||||
showSelectionMarker: false,
|
||||
visualizer: defaultVisualizerSettings,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user