The babel-preset-solid JSX transform HTML-escapes static string
children (< > → < >), which opentui renders verbatim — so the
"< > seek" hint displayed its entities. Pass the string as the
content prop instead, which bypasses the transform.
Previously the marquee looped continuously, cycling back to the
start the moment the text finished. Now it holds at the start for
SCROLL_HOLD_MS (10s), scrolls one pass at SCROLL_STEP_MS per char,
then holds again.
Feed and My Shows rows could grow to 4+ lines when a long title was
shrunk by the current pane: flexible text wrapped instead of
truncating, shifting every row below while scrolling. Add
wrapMode=none + truncate to flexible text and flexShrink=0 to
fixed-width cells so rows stay one line tall. Feed rows also move
the podcast name onto its own line. Adds a rendered-layout
regression test at 70 columns (35-col current pane).
Two fixes to refresh order stability (My Shows / Feed sort by
lastUpdated):
- A refresh that fetches identical episodes no longer bumps
lastUpdated (id-set comparison via sameEpisodes), so unchanged
feeds keep their position instead of reordering every cycle.
- refreshAllFeeds now fetches in parallel and applies ONE atomic
update instead of a per-feed setFeeds, which re-sorted the list
once per completion and made order flap until the batch finished.
Adds feed-refresh regression tests with mocked clock.
Change the parent|current|preview split from 1:2:2 (20/40/40) to
2:5:3 (20/50/30) so the focused list gets more room. 2-pane tabs
now give current the combined 80%. Updates ratio comments and the
PaneRow test expectations.
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.
The decode head lagged the player by a constant ε (ffmpeg startup
latency) because ffmpeg paced at -readrate <speed> started behind mpv
and, advancing at the same rate, never caught up — bars were a few
seconds behind for the entire playback.
Add -readrate_initial_burst LEAD_SECONDS so ffmpeg emits 3s of audio
immediately on start, then paces at realtime after. The decode head
leads the player by a stable ~3s from the very first frame; read()
samples at the exact player position and always finds fresh samples.
Add a sustained render-loop test that simulates ~5s of real playback,
asserting ffmpeg stays alive, the decode head maintains a positive
lead, and read() returns full windows. Uses real wall-clock time
(documented exception) since ffmpeg's decode pacing can't be tested
deterministically.
The cover was previously only wired through the UI hook; --play skipped it.
Also: Bun's fetch hangs in compiled binaries (Bun 1.3.8) — every cover
download silently failed in shipped builds, timing out against any host.
Cover staging now shells out to curl (present on macOS/Linux, 8s bound),
shared via src/utils/cover-art.ts so the UI and CLI paths stage the same
temp file and pass it to mpv as --cover-art-files (albumart track).
mediaremoted resolves the Now Playing client from the registering process's
code-signing identifier, not its bundle. codesign derives the identifier of
a plist-less executable from its basename, so the bundled mpv was stamped
'mpv' regardless of PodTui.app — the audio center kept showing a blank
placeholder. Signing mpv last with --identifier com.mikefreno.podtui (after
the bundle deep-seal, which would otherwise re-derive the basename) makes
the session register as PodTui: icon + name. Identity overridable via
PODTUI_CODESIGN_IDENTITY for Developer ID release builds.
mpv owns the macOS Now Playing session (it plays the audio), and an
unbundled binary renders as a blank placeholder for the source-app icon.
macOS has no public API for a third party to claim session ownership
(MPNowPlayingSession is iOS-only; the private MRMediaRemoteSetNowPlayingApplication
was removed from the shared cache), so instead we make the OWNING process
carry our bundle: the darwin tarball now includes PodTui.app with mpv copied
into Contents/MacOS. AudioPlayer resolves the sibling mpv first (falling
back to PATH), LaunchServices attributes the process to com.mikefreno.podtui,
and Control Center shows the PodTui icon + name with podcast cover art.
AppIcon.icns generated from the Xcode icon-composer exports.
MouseEvent coordinates are terminal-absolute, so the bar's seek handler
must subtract its own absolute left edge (renderable ref) instead of
treating the mouse x as bar-local. Clicking was off by the width of the
parent/Up pane (~20%) plus chrome. Also drop the empty played-text node
at position 0, which rendered a phantom space column and shifted the
drawn bar one char when playback started.
- release tarballs now ship podtui.png (assets/App Icon/App Icon.png, 512px)
on every platform; Linux tarballs additionally carry podtui.desktop
(Terminal=true so launchers open the TUI in a terminal)
- AUR PKGBUILD installs both: icon to hicolor 512x512, entry to applications/
- build.ts bundles the icon/desktop into dist before tarballing
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).
- mpv: --force-media-title '<podcast> — <episode>' so macOS Now Playing shows
the podcast name instead of the download-hash filename (mediaTitle PlayOption)
- media registry: artist is now the human podcast title (customName ||
podcast.title), falling back to podcastId
- downloads: write the podcast cover as a <base>.jpg sibling so mpv's
cover-art-auto=exact picks it up for artwork; delete it with the download
The bottom-bar now-playing segment shows '<podcast> — <episode>' (custom name
when set, feed resolved like advanceEpisode), takes the full remaining status-bar
width, and marquee-scrolls on a 300ms timer when the text overflows instead of
truncating at 40 chars. Static when it fits; no interval runs on fit/narrow bars.
- ProgressBar: full-width played/remaining bar in the player pane, click-to-seek;
waveform no longer handles seeking or the played/future color split (pure visual)
- bars: 2 terminal rows per bar (16 levels) via barChars, partial block in the
top row so the column renders continuously
- fix bars maxing at audio start: disable cava autosens (silence gain-ramp),
pre-warm the malloc'd FFT window with zeros, skip partial FFT windows
- createBarScaler peak follower + power curve replaces cava autosens for
level-to-height mapping (src/utils/bar-mapping.ts, unit-tested)
Whitelist editor (Preferences > Auto Download Whitelist):
- input focus driven by nav.inputFocused() (SearchPage pattern) so Esc
deterministically defocuses and j/k/Space/Enter browse the suggestions
- suggestions react to mouse: click focuses the row and toggles membership
- no accent bg / no marker on any suggestion while the input is focused
- s re-enters typing mode; suggestion rows scroll into view
Search page: recent-searches rows get no bg and no marker while the query
input is focused (previously the focused row fell into the inactive branch
and rendered the border color as a background).
My Shows: w toggles the focused show in/out of the auto-download whitelist
at the show list (depth 0), with a whitelist marker on the row and a hint in
the preview pane (scope: whitelist).
Loading indicators: the braille spinner now carries a contextual label
(Refreshing…, Fetching…, Loading more…, Discovering…, Searching…) and is
shown in every loading state that previously rendered nothing — Discover
results, Search results fallback, and the empty Feed list.
Feed pagination: a focusable "[Fetch More]" row at the bottom of the flat
feed list advances every feed's loaded window by 50 episodes via the new
loadMoreAllFeeds/hasMoreAcrossAll store API. Behavior is a setting
(Fetch More: manual|auto, default manual) persisted in config.json; auto
fetches when focus reaches the bottom row. The button row is excluded
from episode focus so no episode is double-highlighted while it is active.
Set autoloadBunfig: false in build.ts so the compiled runtime ignores any
bunfig.toml in the launching directory, preventing startup failures from a
CWD preload the standalone cannot resolve. Update release.yml, Makefile,
bunfig.toml, CONTRIBUTING.md, and README.md to match.
- Current pane gets muted left/right borders only (no full box, no accent
ring); border colors are passed only when a border is requested, since
opentui flips borderless boxes to bordered when borderColor is supplied.
- Remove the Up / <current tab> / Detail titles above the panes; the current
pane's title now renders once, top-left in the parent column's header slot.
- Drop the parentLabel/previewLabel props from PaneRow and all callers.
- Remove the tab/depth indicator from the bottom-left of the status bar.
- Tests measure column widths from the border glyphs and assert the
left/right edges render muted regardless of focus.
Make parent|current|preview fully borderless: no scrollbox borders and no
accent border highlight on the current column. focused still gates
scroll-following but never surfaces a separator. Update tests to measure
column widths from the header-label row and assert no border glyphs render.