tubular
A modal terminal YouTube and Miruro browser built with Rust and Ratatui. Search for videos or anime episodes, organize YouTube channels and saved Miruro shows with overlapping tags, and open a selection in Terebi.
Requirements
- A recent stable Rust toolchain, including Cargo, to build and install.
yt-dlponPATHfor YouTube searches, feeds, and full descriptions; Miruro uses the native API client.curlonPATHfor YouTube thumbnails and Miruro cover art.- The Terebi CLI installed on
PATH, together with its app bundle and streaming dependencies, for playback. Single videos use--new-instance --url … --progress-file PATH; multiple videos require--new-instance --queue-file PATHsupport. Miruro also requires native episode-URL resolution and version-2 episode progress support.
The default playback setup requires macOS 15+, since Terebi is a native macOS player. Install Terebi separately and follow its dependency/setup instructions; installing Tubular does not install the player.
A terminal with native image support can display thumbnails and cover art using its graphics protocol. Other terminals fall back to colored half-block characters.
Installation
From a checkout of this repository:
cargo install --path . --locked
Make sure Cargo's install directory (normally ~/.cargo/bin) is on your PATH.
To run directly from source instead:
cargo run --locked -- "rust programming"
Usage
tubular # Choose YouTube or Miruro
tubular "rust programming" # Choose a provider, then search this query
tubular --miruro "apothecary" # Skip the chooser and search Miruro directly
tubular --player /path/to/terebi "japanese podcast"
tubular --help
Startup shows a centered YouTube / Miruro selector. Use j / k or arrows to select and Enter to open that provider's separate interface; Esc, q, or Ctrl-C quits without opening a browser. Navigation, confirmation and Esc use the configured selector.select.next, selector.select.previous, selector.open and selector.close bindings; quit/interrupt bindings also apply. An initial CLI query searches whichever provider you select. --miruro bypasses the chooser; --help and --default-config remain noninteractive.
After selection, the browser starts in normal mode. Press i to type a search; Enter submits a changed query and returns to normal mode. Esc returns without submitting.
In either provider's normal mode, press p (app.provider) to reopen the chooser with the current provider selected, then Enter to switch. Esc cancels and returns to browsing (unlike at startup); q or Ctrl-C quits. Both browser sessions stay in memory, preserving their queries, results, selection and navigation history when you switch back. Switching leaves running players unaffected and never reapplies the CLI query: it is used only for the initially opened provider. --miruro only skips the startup chooser; you can still switch providers with p.
Both providers use the same layout: a cyan view heading, bordered search and results, a bordered status line, and a dim one-line key hint bar. Entries use two lines: the title above dimmed metadata. YouTube shows the channel and publication date beneath each video title, without separate channel/date columns; tagged channels remain cyan. The selected row uses black text on cyan with a > marker; saved titles are green, watch-later titles magenta, and ✓ marks watched entries. Metadata and help open in centered cyan-bordered popups; bulk-removal confirmations use yellow borders. Miruro keeps its catalog navigation and title/episode metadata.
YouTube's dimmed metadata row includes duration as M:SS or H:MM:SS, or Unknown duration when unavailable. Duration is retained in saved collections; older entries without duration remain readable.
YouTube keeps fast flat listings, then resolves visible videos' original-preferred titles asynchronously, one metadata request at a time. Titles replace the listing text left to right at 20 ms per terminal column, using the smallest groups of whole user-perceived characters with matching column widths. Two narrow characters and one wide character therefore swap together after 40 ms, without shifting the untouched suffix or inserting temporary blanks. Only an exhausted shorter title is padded with trailing spaces; that padding disappears when the animation finishes. Offscreen changes apply immediately, and selection stays on the same video. Results are cached by video ID for the session; failures keep the listing title without automatic retries. This adds background yt-dlp requests, and opening video info can make a separate metadata request. “Original-preferred” uses the full extractor response, not a guarantee that YouTube will never return localized text.
Selecting a YouTube video launches:
terebi --new-instance --url 'https://www.youtube.com/watch?v=VIDEO_ID' \
--progress-file '/private/session/progress.jsonl'
Each submitted selection opens one independent player instance without replacing existing playback. Tubular creates a private reporting session per video and consumes Terebi's progress records independently of the launcher. --player overrides the Terebi CLI executable; it is not a generic mpv/IINA adapter. Tubular reports launcher failures, while later streaming/playback errors appear in Terebi. Quitting Tubular leaves launched players running.
Multiple video selection
In video and episode lists, Space checks/unchecks the hovered row; Ctrl-Space clears checks. Enter plays checked rows in their current displayed order, regardless of the order they were checked. With no checks, Enter plays the hovered row. These controls also work in collection views and metadata previews; Miruro catalog titles and saved shows cannot be checked.
Checked videos/episodes show a single *; unchecked rows leave that marker blank. Category and tag selectors keep their checkboxes.
Checks belong to each cached view/screen and follow stable video identities through pagination and reordering. Refresh/replacement queries clear them; collection filtering/removal prunes checks for hidden/removed rows. Successful launcher creation clears checks, while immediate launch failures retain them. Saving, removing and marking watch status still affect the hovered video, not every checked row. Configure video.select.toggle and video.select.clear; terminals must transmit Ctrl-Space, or bind another chord.
Two or more checked videos launch terebi --new-instance --queue-file '/private/session/queue.json'. The private version-1 manifest contains ordered canonical episode/video page URLs and one independent progress_file per item. Terebi reuses its queue navigation and resolves each item when selected, not in advance; no expiring playback sources are stored. Each item's watching/watched state and resume identity remain independent, including after Tubular restarts. Single-video launches retain the command above. See the queue contract.
Miruro browser
Choose Miruro at startup or use --miruro to start directly. Its interface is catalog → series → episodes, with a separate saved-show library organized by overlapping category tags, without YouTube's Search/Feed tabs or channel controls.
Catalog autocomplete returns at most eight anime titles, without pagination. Titles show the available format, status, season/year and planned/listed episode counts; press d for cover art, a bold title, dimmed metadata and a synopsis excerpt, using the same info layout as YouTube. Press d again for the full scrollable synopsis and returned facts; another d returns to info, and Esc closes either mode. The series screen lists only actual returned episodes—not planned or absolute episode numbers—with titles, dates and durations when supplied. Selected episode details appear first, above the series synopsis and beside the series cover art in info mode. Listed counts do not guarantee a playable source; Terebi resolves playback.
| Key | Miruro action |
|---|---|
i / / |
Edit the catalog query; Enter searches, Esc cancels |
p |
Reopen the provider chooser in normal mode |
j / k, ↓ / ↑; g / G, Home / End |
Select a title or episode |
| Space / Ctrl-Space | Check/uncheck the hovered episode / clear episode checks |
| Enter | Open a series, or play checked episodes in displayed order (hovered episode when none are checked) |
- / + |
Back / forward through cached screens, preserving query and selection |
w / v |
Save the selected episode to watch later / saved episodes |
s |
Edit the selected show's category tags; Enter saves the show, including with no tags |
c |
Choose All saved shows or a category, then Enter opens a saved show’s episodes |
m / M |
Mark the selected episode as watching / watched (also in metadata) |
W / V / C |
Open Miruro's offline watch-later / saved / currently-watching episodes |
f |
Cycle All / Unwatched / Watched in watch later |
x / X |
Remove the selected / confirm removal of visible episodes from the active collection; x also removes a saved show from the library |
d |
Open info with cover art → full synopsis/metadata → info; ↓ / ↑, Ctrl-j / Ctrl-k and page keys scroll the full description; j / k change selection |
| Esc | Cancel input or close help/metadata and return to the episode/title list |
r |
Refresh the active screen and reload configured keys |
? / q |
Keyboard help / quit |
These actions reuse the existing [keys] configuration; live hints and help omit unsupported YouTube actions. Show saving/tagging uses channel.tags, and the library picker uses category.browse. From catalog results, a series screen, or the saved-show library, press s to edit the show's tags: j / k selects, Space checks/unchecks, i filters, and n enters a new tag name. Enter stages a new tag; Enter again from the tag list saves the show and all checked tags. Esc cancels without saving. A show can belong to multiple categories or remain saved without tags. The c picker starts in normal mode; i filters, Enter browses, and Esc leaves filtering before closing. Saved-show lists are alphabetical and stored offline; Enter fetches current episodes using the saved catalog ID and title. x removes only the show and its tags, leaving episode collections and watch status unchanged. Categories disappear when no saved shows use them. Miruro's show categories are independent of YouTube's channel tags.
Space checks categories for combined browsing; Enter opens their deduplicated union even when the filter hides them. With no checks, Enter opens the hovered category or All saved shows.
Only episodes can have their watch status edited. Reopening a saved episode passes its stable identity to Terebi for fresh resolution; identity is the opaque Miruro catalog ID plus its returned episode number. Each interface shows only its provider's collections, while playback, watched state and storage remain shared. Episode markers show saved/watch-later/currently-watching membership and watched status; Terebi owns resume positions.
Catalog results, series screens and saved-show lists use the returned cover_url; missing artwork leaves a text-only preview, and image failures do not hide metadata. Offline episode collections retain their stored text-only details. JPEG, PNG and WebP covers load, decode and resize off-thread, with native terminal graphics or proportional half-block rendering. The session image cache retains eight inactive images plus the current selection; a successful r refresh clears it.
Tubular owns catalog browsing, selection, collections and watched decisions. Terebi owns source resolution, stream/subtitle headers, subtitles, playback titles and optional timeline metadata. Miruro launches use https://barelystarted.miruro.tv/watch/OPAQUE_ID/episode?ep=RETURNED_EPISODE; episode is a non-identifying placeholder slug, not a title. No source file or collection migration is required. Current Terebi displays the generic title Miruro episode N; title enrichment and intro/outro bands in the controls progress bar remain Terebi work. No automatic skipping or skip buttons are requested. See the Terebi URL-playback handoff for canonical-route handling and acceptance scenarios; this is not a claim of implemented timeline UI.
YouTube controls
These are the default bindings. Overrides in [keys] replace an action's bindings; the live hints and ? reference show the configured keys.
Normal mode
| Key | Action |
|---|---|
j / k, ↓ / ↑ |
Select the next / previous video |
| Space / Ctrl-Space | Check/uncheck the hovered video / clear checks |
| Enter | Play checked videos in displayed order, or the hovered video when none are checked |
p |
Reopen the provider chooser |
o |
Open the selected YouTube video in the default browser (also works in previews; configurable as video.open) |
g / Home, G / End |
Select the first / last loaded video |
i |
Edit the current category/combined feed search; other views use general YouTube Search |
/ |
Go directly to general YouTube Search |
F (Shift-F) |
Go directly to the subscription Feed |
d |
Open video info; press again for the full description, again to return to info |
| Esc | Close the preview; otherwise return to the previous view |
- |
Return to the previous view |
+ |
Go forward after navigating back |
c |
Browse saved categories |
u |
Browse an individual saved channel |
U (Shift-U) |
Open the selected video's channel |
s |
Edit the selected channel's tags |
S |
Edit tags for all saved channels in the current category |
R (Shift-R) |
Rename the current category, including category-scoped collection views |
P (Shift-P) |
Save the current combined-category feed as a new category |
w |
Save the selected video to watch later (also works in previews) |
W |
Choose combined or category watch later from any video view or preview |
Alt-w |
Open the current category's watch later |
f |
In watch later, cycle All → Unwatched → Watched |
x |
In watch-later, saved-video, or currently-watching lists, remove the selected video |
X (Shift-X) |
In collection lists, confirm bulk removal of the current list/category/filter subset |
v |
Save the selected video to saved videos (also works in previews) |
V |
Choose combined or category saved videos from any video view or preview |
Alt-v |
Open the current category's saved videos |
C (Shift-C) |
Open the combined Currently watching list |
m / M |
Mark the selected video as watching / watched (also works in previews) |
| Tab / Shift-Tab | Switch between visible views |
r |
Reload configuration and refresh the current view |
? |
Open / close the scrollable keybinding reference |
q |
Quit |
Ctrl-C quits from any mode or popup.
The bottom bar shows essential actions for the current context. Press ? in normal mode—including selectors and previews—for the full reference. Scroll help with j / k, arrows, Ctrl-J / Ctrl-K, or PageDown / PageUp; ? or Esc closes it without changing the underlying view or dialog. In text fields, ? remains ordinary input.
The overview groups shortcut/action rows under bold section headers such as VIEWS AND VIDEOS, with an empty line beneath each header. Shortcuts and action descriptions remain normal weight; the rows reflect configured bindings rather than showing internal action paths.
Back and forward navigation restore cached videos, selection, and pagination, including replaced temporary tabs. Removed views are skipped; empty history leaves the current view unchanged. Opening a different view after going back discards the old forward branch. Esc first closes a dialog or leaves insert mode without navigating. Minus and plus remain ordinary text inside input fields.
The full-description view scrolls with ↓ / ↑, Ctrl-J / Ctrl-K, or PageDown / PageUp. Plain j / k still changes the selected video. The bold title, dimmed author/publication row, and description scroll together.
Text input
These shortcuts work in search, category/channel/tag-filter, new-tag, and category-rename fields:
| Key | Action |
|---|---|
| Enter | Submit the search, browse the selected category/channel, leave tag-filter mode, stage a new tag, or commit a rename |
| Esc | Leave insert mode or abandon the new-tag / rename prompt |
| Backspace | Delete one character |
| Option/Alt-Backspace or Ctrl-W | Delete the previous whitespace-delimited word |
| Cmd/Super-Backspace or Ctrl-U | Clear the line |
If the terminal intercepts modified Backspace rather than transmitting it, use Ctrl-W / Ctrl-U or configure equivalent terminal key mappings. Input fields show a block cursor.
In terminals supporting bracketed paste, clipboard text is inserted in one operation into the active search, filter, or new-tag field; filters update once for the whole paste. Pasting never submits input or triggers shortcuts. Line breaks and tabs become spaces, other control characters are removed, and pastes outside text-entry mode are ignored. Bracketed-paste mode is disabled again when Tubular exits. Terminals without bracketed-paste support still deliver pasted text as ordinary keystrokes.
Category browser (c)
- Normal:
j/kor arrow keys navigate; Space checks/unchecks a category;ienters filter mode; Enter browses checked categories, or the hovered category if none are checked;ptoggles its persisted pin;Rrenames it; Esc closes. - Insert: type a case-insensitive substring to filter; arrow keys or Ctrl-J / Ctrl-K navigate matches; Enter browses the chosen categories; Esc returns to normal mode without clearing the filter. Space is ordinary input here.
- Checkboxes select categories for browsing; the dimmed
psuffix marks pins independently. Hidden checks survive filtering. Enter still browses checked categories when there are no visible matches; without checks, an empty result makes browsing a no-op. Pinning, renaming, and toggling require a visible selected row.
Category search
Press i in a category or combined-category feed to search only its channels' histories—not just already loaded videos. Enter submits a changed query and returns to normal mode; an unchanged query keeps cached results and pagination. Clear the field and submit to restore the ordinary channel catalog. Esc leaves the draft unsubmitted. Scoped queries still use YouTube's channel-search endpoint, which can omit music release tracks even when unfiltered browsing includes them.
Each category and combined feed retains its own draft, submitted query, results, and pagination independently of general Search. Subsequent pages use the same scoped query and shared 60-video budget. Empty categories and failed scoped requests never fall back to global search. Search input in other views still opens general YouTube Search.
The direct Search and Feed jumps use the configurable view.search and view.feed actions. They preserve cached views and history, and do not run inside text inputs or selectors.
Combined category feeds
Check multiple categories in c, then press Enter to open their combined feed. One checked category opens its ordinary view; no checks retain the usual single-category shortcut. Esc discards the selector's staged choices. Reopening c from a combined feed prechecks its member categories.
The combined feed uses the shared temporary tab, replacing the prior unpinned category/channel/collection view. Channels and video IDs are deduplicated, and videos use the same newest-first ordering and infinite scrolling as other feeds. Back/forward navigation restores its query, cached rows, selection, and pagination.
Press P to save the viewed union of channels under a new category name. Enter commits; Esc cancels; duplicate or invalid names leave the prompt open without overwriting anything. The new flat category becomes active without automatic pinning or re-fetching matching cached pages. Its memberships are an independent copy, not links to the original categories.
Renaming a member category updates the combined view and its history. Membership changes recompute the channel union, preserving cached rows when the resolved union is unchanged; deleting a member invalidates the combined view rather than silently widening it. Explicit refresh still reloads the active view from page one.
Combined feeds are not individual categories: S, R, and Alt-w / Alt-v require a single category. Save with P first to use those actions. Configure multi-selection with selector.toggle and saving with category.save; name entry uses the shared input.* controls.
Saved channels (u)
- Normal:
j/kor arrow keys navigate;ienters filter mode; Enter opens the selected channel's videos; Esc closes. - Insert: type a case-insensitive substring to filter; arrow keys or Ctrl-J / Ctrl-K navigate matches; Enter browses; Esc returns to normal mode without clearing the filter.
- The list includes registered channels, including untagged ones, and channels saved only in categories. Aliases of the same channel appear once, using a mapped handle when available; otherwise the saved channel identity is shown.
- Channels and unpinned categories share one temporary browsing tab. Choosing either replaces the previous temporary view; pinned category tabs stay visible. Revisiting or navigating back restores cached videos, selection, and pagination.
- Channel selection is single-choice and does not pin channels or change subscriptions.
sstill edits the selected video's channel tags; bulkSremains category-only.
Press U (Shift-U) on a video to browse its creator directly, including channels you have not saved. This replaces the current temporary browsing tab without adding a subscription. Channel IDs are preferred; a valid handle is used when an ID is unavailable. Missing channel metadata leaves the current view unchanged. Unsaved channel views survive configuration changes for the session; explicitly removing a saved subscription still removes its channel view.
Channel tags (s, or S for bulk editing)
When search results omit a channel handle, s reuses its saved mapping or looks up the handle from the channel's canonical ID before opening the editor. Esc cancels a pending lookup without saving. Failed lookups or mismatched channel metadata leave the config unchanged.
Tubular validates handles as channel addresses, not against YouTube's current handle-creation guidelines. Existing handles such as @hontame_ keep their leading/trailing separators and round-trip through saved mappings and categories. Whitespace inside handles, control characters, slashes, query strings, and fragments remain rejected.
- Normal:
j/kor arrow keys navigate; Space toggles a tag;iedits the filter;Rrenames the selected tag. - Insert: type a case-insensitive substring to filter live; arrow keys or Ctrl-J / Ctrl-K navigate matches. Enter or Esc returns to normal mode without saving or clearing the filter.
- Filtering only hides rows: checked and unchecked staged memberships outside the filter are retained. With no matches, navigation and toggling do nothing. Reopening the dialog clears its filter.
nprompts for a new tag name. Enter stages it checked, clears the filter, and selects the new tag; Esc abandons the name.- Enter in the normal-mode tag list saves all staged memberships, including hidden tags. Esc cancels those staged changes; it does not undo a committed rename of an existing category. These controls apply to both single-channel and bulk tagging.
- Filter actions are configurable as
tags.edit,tags.input.next, andtags.input.previous; text editing uses the sharedinput.*actions.
Bulk editing applies the exact checked tag set to every saved channel in the current category, not only channels with loaded videos. Tags shared by every target start checked; mixed memberships start unchecked. Review the checked set before saving: unchecked tags are removed from all targets. Other channels, saved registrations, empty categories, pins, and comments are preserved.
Renaming categories (R)
In normal mode, press R (Shift-R) to rename the active category, or the hovered category in a category/collection selector or single-channel/bulk tag list. Combined collection entries and channel rows cannot be renamed.
The floating prompt starts with the current name. Edit it with the shared text controls (Ctrl-U clears it), then press Enter to commit or Esc to cancel. Names are trimmed and must be nonempty, free of control characters, and distinct from existing or staged category names. Errors leave the prompt open without changing the file. If the source category changed externally, close dialogs and reload config with r before retrying.
Renaming an existing category saves its identity immediately while preserving channel memberships, pins, comments, cached videos, selection, pagination, watch filters, and back/forward history. In tag dialogs, checkbox changes remain staged until the main tag-list Enter; cancelling them does not undo the rename. A newly staged, unsaved tag stays local when renamed and reaches disk only when the tags are saved.
Configure the shortcut with category.rename; the prompt uses the shared input.* actions. R remains ordinary text while editing search or filters.
Subscriptions and feeds
Configuration lives at:
$XDG_CONFIG_HOME/tubular/config.toml
If XDG_CONFIG_HOME is unset, empty, or relative, the fallback is ~/.config/tubular/config.toml. Edit the file directly and press r to reload, or save memberships from the tag dialog.
[settings]
pinned_categories = ["Favorites"]
[categories]
Creators = ["@jawed"]
Favorites = ["@jawed"]
[channels]
"@jawed" = "https://www.youtube.com/channel/UC4QobU6STFB0P71PMvOGN5A"
[categories] contains flat, overlapping tags. A channel can belong to several categories; there is no hierarchy or inheritance. [channels] maps readable handles to stable channel URLs or IDs. Saving a channel through the UI records its handle and mapping when metadata is available.
[settings] holds global options such as pinned_categories. The [settings], [channels], [categories], and [keys] sections can appear in any order; pin/unpin and tag edits preserve the existing section order, key overrides, and other preferences. For older configs, add [settings] immediately above pinned_categories. A top-level declaration is no longer accepted.
Category entries accept @handles, HTTPS YouTube channel URLs, or canonical UC… channel IDs. Mapped handles use their stable identity; unmapped handles work directly.
Video tables color channel names cyan when the creator belongs to at least one category. Untagged registrations keep the default text color; selected rows retain their black-on-cyan highlight. Membership is matched by channel ID or handle, not the displayed name, and updates after saving tags or reloading config.
- Search and Feed are permanent YouTube tabs. Feed combines all YouTube
[channels]registrations, including untagged channels, plus channels entered directly in categories, without duplicates. - Browsing an unpinned category, combined-category feed, channel, watch-later list, or saved-video list uses one shared temporary tab; choosing another replaces it. Pinned categories keep their own tabs, marked with a dimmed
p. - Removing a channel from a category does not unsubscribe it from Feed. To unsubscribe, remove its
[channels]mappings and category references, then pressr. - Temporary tabs do not survive restarting; saved categories and pins do. Hidden views retain loaded rows, selection, and pagination during the session.
Bulk editing refuses to clear all tags when a target has no [channels] registration, preventing accidental unsubscription of category-only channels. If source memberships change while a bulk dialog is open, close and reopen it before saving.
Watch later
Watch later is one stored collection, deduplicated by YouTube video ID or stable Miruro episode identity, with provider-specific views. In YouTube, category lists are derived from saved videos' channel identities and the current subscription categories: adding or removing a channel from a category changes its watch-later list after saving tags or reloading config with r. Its combined list contains only YouTube videos. Miruro uses the flat episode lists described above. No category memberships are stored on individual videos.
In YouTube, W opens a searchable collection selector from any video view or preview. The All categories (combined) entry comes first, followed by configured categories; it remains available even when there are no categories. Navigate with j / k or arrows in normal mode, or press i to filter by name and use arrows / Ctrl-j / Ctrl-k while typing. Enter browses the selected match; with no matches it does nothing. Esc returns to normal mode, then closes the selector without navigating. Reopening clears the search. Alt-w still opens the current category's watch later directly.
Moving between watch-later scopes carries the current All/Unwatched/Watched filter to the destination, including cached lists; back navigation restores the originating category list. Choosing the current list does not add history. The border title shows the active filter. Lists use saved title, creator, and publication metadata without fetching video lists, so they also work offline.
Watch-later-only titles are magenta in all video tables; categorized channel names remain cyan. A ✓ marks watched videos, including outside watch-later views. The selected row retains its usual highlight.
Watch status
Terebi reports acknowledged playback positions. A video becomes watched when an eligible position reaches 95% of a known positive duration. Seeking near the end counts; live/non-seekable, unknown-duration, and audio-only inputs do not. Launcher exit or window closure alone never marks a video watched. Watched status is global across categories and remains after removing a video from watch later.
Press m to mark the selected video/episode watching: clear its watched flag and add it to Currently watching with offline metadata. Press M to mark it watched: set the watched flag and remove it from Currently watching. Both work in lists and previews/metadata, persist across restarts, and immediately update watch-later filters and markers without changing watch-later or saved membership. Miruro catalog titles are not editable. Configure video.status.watching and video.status.watched to remap or disable these actions.
Manual edits take precedence over reports from existing bound playback sessions, including reports recovered after restart. Start a new playback session to resume automatic status updates. Editing watch status does not change Terebi's resume position.
Storage
Files are siblings of config.toml:
watch-later.toml: versioned saved-video metadata.saved-videos.toml: independent versioned saved-video metadata.currently-watching.toml: versioned metadata for unfinished videos, added by playback or marked manually.watch-state.toml: versioned global watched video IDs.miruro-library.toml: versioned saved-show catalog metadata and overlapping category tags, independent of episode collections.playback-sessions/: private per-video progress streams, expected identities, metadata snapshots, durable admission/manual-edit markers, and orderedqueue.jsonmanifests for multi-video launches.
State writes are atomic and owner-only. Invalid state files or detected external edits are reported rather than overwritten; repair the file or restart before retrying. Each state file is limited to 8 MiB.
Progress sessions survive Tubular exit so later player reports can be recovered on the next launch. Session directories are 0700; snapshots, identity markers and Terebi-created reports are owner-only. New Miruro launches contain no resolved-source documents or CDN credentials. Existing sessions from earlier versions remain readable and are not automatically removed. Sessions are retained because CLI exit does not prove the independent GUI has finished; up to 4,096 are monitored. Close the corresponding players before removing their inactive session directories, then restart Tubular. Never remove an active player's directory.
Saved videos
Saved videos is a separate stored collection from watch later, deduplicated by YouTube video ID or stable Miruro episode identity, with provider-specific views. It uses offline saved metadata; YouTube entries follow current channel category memberships, not categories stored on individual videos. Its All categories (combined) list contains only YouTube videos; Miruro opens a flat saved-episode list.
In YouTube, V opens the same searchable selector for saved videos, with All categories (combined) first and configured categories beneath it. Alt-v opens the current category's saved videos directly. Alt-w / Alt-v retain the explicit category when switching between category-scoped collection lists; without a current category, they leave the view unchanged. All four shortcuts work in normal mode and previews, not text-entry fields or modal selectors. Both collections share the existing temporary-tab and back/forward history behavior.
Saved lists have no watched filter. Individual and bulk removal use the shared collection-list controls below.
Saved titles are green, taking precedence over watch-later magenta when a video belongs to both collections. Removing it from saved videos restores magenta if it remains in watch later. Channel styling, selected-row highlighting, and watched markers are unchanged.
The collection is persisted independently in saved-videos.toml beside config.toml, using the same atomic, owner-only writes and validation as watch later. Saving repeatedly does not add duplicates. Configure saving and the collection selector with video.saved.save and view.saved.browse; direct category browsing uses view.saved.category. The corresponding watch-later navigation actions are view.watch_later.browse and view.watch_later.category. Selectors use the shared selector.* and input.* bindings. The live hints and ? reference reflect overrides. Alt shortcuts require the terminal to transmit Alt/Meta; if macOS Option inserts symbols, enable Option-as-Meta or remap the actions.
Currently watching
C opens an offline list of the current provider's unfinished videos/episodes, started through playback or marked watching with m. YouTube uses its shared temporary tab; Miruro uses a cached episode-list screen. Both support back/forward navigation; configure the shortcut with view.watching.browse.
A validated, acknowledged positive playback position automatically adds the video—not merely launching Terebi or receiving position zero. Reaching the eligible 95% watched threshold, or marking it watched with M, removes it. Already-watched videos do not enter automatically; m explicitly clears watched status and adds them. Unknown-duration or ineligible playback can appear but stays unfinished unless a later eligible report marks it watched, you mark it watched, or you remove it.
Entries and their title/creator/publication metadata survive restarts. Reports written while Tubular is closed are recovered on the next launch. Older sessions without video snapshots can populate the list when matching watch-later/saved metadata exists; otherwise they continue updating watched status without inventing missing metadata.
Use x for individual removal or X for bulk confirmation. Removal neither marks the video watched nor changes watch later/saved videos. Continued reports from existing sessions cannot re-add a removed entry, including after restart; deliberately starting a new session can re-add it if still unfinished.
Collection removal
Episode removal controls apply only in watch-later, saved-video, and currently-watching views, including previews opened from those lists. The Miruro saved-show library additionally supports x to remove a show and its tags, without removing episodes or watch status. Search, Feed, Miruro catalog/episode browsing, ordinary YouTube channel/category views, and their previews do not expose removal actions or hints. Text fields and selectors retain their normal controls.
x immediately removes only the selected video/episode from the active collection. X snapshots the current provider's visible list and opens bulk confirmation: YouTube watch later uses the current category and All/Unwatched/Watched filter, and saved videos uses the current category's entire saved list; Miruro uses its flat list and any active watch-later filter. Currently watching uses the current provider's entire list. An empty list does not open confirmation. Neither interface's removal controls touch the other provider's entries.
The YouTube prompt names the collection, category, count and applicable filter; Miruro names the collection and episode count, with the active filter in the screen header. y confirms; n or Esc cancels. Miruro also accepts Enter to confirm. Only the snapshotted IDs are removed, even if membership or watched status changes while confirmation is open.
Removal changes only the active collection. Matching views update, while other collections and global watched status remain untouched.
The old collection-specific removal settings are no longer accepted. Replace them with:
[keys]
view.collection.remove.selected = "x"
view.collection.remove.bulk = "X"
Configurable keybindings
Print a complete default configuration, including every action and its default bindings:
tubular --default-config
This prints TOML to stdout and exits without reading or writing config, starting the TUI, or needing yt-dlp, Terebi, or a terminal. Copy the settings you want into your config; r reloads them.
[keys]
video.info.overview = "I"
video.info.description = "d"
video.info.hide = ["escape", "backspace"]
view.history.previous = ["-", "escape"]
view.history.next = "+"
video.play = ["enter", "ctrl+p"]
Each string or array replaces that action's default aliases; omitted actions keep their defaults, and [] disables an action. Dotted assignments, corresponding subtables such as [keys.video.info], and quoted action names are supported. Unknown actions, invalid chords, duplicate action declarations, and ambiguous bindings in the same context are rejected before saving.
Characters are case-sensitive (i differs from I). Named keys are case-insensitive: escape, enter, tab, space, arrows, home, end, pageup, pagedown, backspace, delete, and f1–f24. Combine modifiers with +, for example ctrl+j, alt+backspace, cmd+backspace, or shift+tab. control, option, and super are accepted aliases; + itself, or ctrl++, can be bound. The terminal must transmit the chord for it to work.
Using the same binding for overview and description preserves the default cycle. Distinct bindings, as above, open either view directly. Preview close takes precedence over history back; description scrolling takes precedence over video selection for shared bindings. View commands do not run inside text inputs or selectors; those have their own actions listed by --default-config.
Loading and previews
General Search loads up to 60 results per page. Feed and category views, including scoped category searches, share a 60-video total request budget per load, distributed fairly across active channels—not 60 per channel. Each channel retains its own offset; exhausted channels stop receiving requests, and feeds with more than 60 channels rotate through them across loads. Matching upload/search ranges are batched across at most four concurrent yt-dlp processes, with further batches queued. Release discovery and album extraction use the same four-process limit. Individual channel views can use the full 60-row budget.
Unfiltered channel, Feed, and category browsing traverses each channel's uploads first, then the tracks in its Releases albums. Album tracks—not album links—consume the video budget; repeated tracks are deduplicated in the loaded results without refunding their raw-row quota. This includes artist catalogs whose music appears in general Search but not the channel's Videos tab. Channels without a Releases tab keep their uploads normally; other discovery or album failures remain page errors. Later pages re-traverse earlier releases to recover their track offsets, so large artist archives can take longer to load.
In normal mode, forward video navigation authorizes prefetch when 20 loaded rows remain. Each successful page clears that intent, so short, duplicate-only, or unavailable batches do not immediately chain additional loads at startup. Only one page request runs at a time; insert mode and open popups pause automatic prefetch. A feed page is accepted only after every batch succeeds. Switching views cancels running and queued batches; returning resumes the same unaccepted per-channel ranges. Deep channel archives can get slower because yt-dlp offsets traverse earlier pages again.
Malformed individual entries—including invalid video IDs, missing required fields, and wrong field types—are discarded without losing valid videos from the same page. Raw entries, including skipped and duplicate rows, consume the request budget and determine each channel's exhaustion; a full unavailable batch therefore does not hide later results. Invalid JSON, malformed playlist-level metadata, missing/extra playlists, oversized responses, and extractor failures still fail the page without advancing channel offsets.
YouTube Search, feeds, category searches, and channel browsing hide videos that yt-dlp marks subscriber_only (members-only). Public, unlisted, and unknown-availability videos remain visible; titles or channel membership promotions are not used to infer restrictions. Filtered rows still count toward pagination, and offline saved collections are unchanged.
Feed views sort all loaded videos newest-first using available upload dates, with undated videos last. The budget limits what has been inspected: later loads may discover newer videos and insert them above older rows, while selection and pending description requests stay attached to the same video ID. Publication labels use YYYY-MM-DD; ~ marks an estimated UTC date, and Unknown means unavailable metadata.
Pane titles show animated spinners while content loads. Refresh keeps the current rows and selection visible until a valid first page replaces them, including when the new result is empty. Errors retain loaded rows and stop automatic retries; r refreshes the current view from page one.
Video info shows a thumbnail, title, author/publication details, and a clipped description excerpt. Press d again to read the full description in the same popup. Thumbnails and full descriptions are cached in memory across selections and views: 64 full descriptions and eight inactive decoded thumbnails, plus the selected image. Caches last for the session; a successful refresh clears them. Preview loading, decoding, and image resizing run asynchronously.
Development
Miruro API notes document the native catalog-only tubular::miruro wrapper, stable episode/progress contracts and historical source/CDN research. The Terebi URL-playback handoff defines the playback interface and remaining title/timeline work.
Unit tests live in tests/unit/, grouped by source module and loaded with #[cfg(test)] and #[path] so they retain access to private implementation details. Cargo runs them with the commands below; they are not separate integration-test crates.
cargo fmt --check
cargo test --locked
cargo clippy --locked --all-targets -- -D warnings
Read-only native Miruro smoke (search → detail/episodes):
cargo run --locked --example miruro_probe
This uses the documented research title and stops on errors/protection pages. The sample can become unavailable; passing it proves catalog retrieval, not Terebi playback. Earlier source/HLS/subtitle probe results remain historical evidence in the API notes.