152 lines
7.7 KiB
Markdown
152 lines
7.7 KiB
Markdown
# tubular
|
|
|
|
A modal terminal YouTube browser built with Rust and [Ratatui](https://ratatui.rs/). Search for videos, organize subscribed channels with overlapping tags, browse their feeds, and open a selected video in Terebi.
|
|
|
|
## Requirements
|
|
|
|
- A recent stable Rust toolchain, including Cargo, to build and install.
|
|
- [`yt-dlp`](https://github.com/yt-dlp/yt-dlp) on `PATH` for searches, feeds, and full descriptions.
|
|
- `curl` on `PATH` for thumbnails.
|
|
- The **Terebi CLI** installed on `PATH`, together with its app bundle and streaming dependencies, for playback.
|
|
|
|
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 using its graphics protocol. Other terminals fall back to colored half-block characters.
|
|
|
|
## Installation
|
|
|
|
From a checkout of this repository:
|
|
|
|
```sh
|
|
cargo install --path . --locked
|
|
```
|
|
|
|
Make sure Cargo's install directory (normally `~/.cargo/bin`) is on your `PATH`.
|
|
|
|
To run directly from source instead:
|
|
|
|
```sh
|
|
cargo run --locked -- "rust programming"
|
|
```
|
|
|
|
## Usage
|
|
|
|
```sh
|
|
tubular # Start without a query
|
|
tubular "rust programming" # Search immediately
|
|
tubular --player /path/to/terebi "japanese podcast"
|
|
tubular --help
|
|
```
|
|
|
|
Tubular starts in **normal mode**, with or without a command-line query. Press `i` to type a search; Enter submits a changed query and returns to normal mode. Esc returns without submitting.
|
|
|
|
Selecting a video launches:
|
|
|
|
```sh
|
|
terebi --new-instance --url 'https://www.youtube.com/watch?v=VIDEO_ID'
|
|
```
|
|
|
|
Each selection opens an independent player instance without replacing existing playback. `--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.
|
|
|
|
## Controls
|
|
|
|
### Normal mode
|
|
|
|
| Key | Action |
|
|
| --- | --- |
|
|
| `j` / `k`, ↓ / ↑ | Select the next / previous video |
|
|
| Enter | Open the selected video in Terebi |
|
|
| `g` / Home, `G` / End | Select the first / last loaded video |
|
|
| `i` | Edit the search query |
|
|
| `d` | Open video info; press again for the full description, again to return to info |
|
|
| Esc | Close the preview |
|
|
| `c` | Browse saved categories |
|
|
| `s` | Edit the selected channel's tags |
|
|
| `S` | Edit tags for all saved channels in the current category |
|
|
| Tab / Shift-Tab | Switch between visible views |
|
|
| `r` | Reload configuration and refresh the current view |
|
|
| `q` | Quit |
|
|
|
|
**Ctrl-C quits from any mode or popup.**
|
|
|
|
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-filter, and new-tag fields:
|
|
|
|
| Key | Action |
|
|
| --- | --- |
|
|
| Enter | Submit the search, browse the selected category, or stage the new tag |
|
|
| Esc | Leave insert mode or abandon the new-tag name |
|
|
| 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.
|
|
|
|
### Category browser (`c`)
|
|
|
|
- **Normal:** `j` / `k` or arrow keys navigate; `i` enters filter mode; Enter browses the selected category; `p` toggles its persisted pin; 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.
|
|
- Reopening the browser clears its filter. No matches makes browsing and pinning a no-op.
|
|
|
|
### Channel tags (`s`, or `S` for bulk editing)
|
|
|
|
- `j` / `k` or arrow keys navigate; Space toggles a tag.
|
|
- `n` prompts for a new tag name. Enter stages it checked and returns to the list.
|
|
- Enter **in the main tag list** saves the staged memberships. Esc cancels without writing.
|
|
|
|
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.
|
|
|
|
## Subscriptions and feeds
|
|
|
|
Configuration lives at:
|
|
|
|
```text
|
|
$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.
|
|
|
|
```toml
|
|
# Top-level setting: place this before the table headers.
|
|
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.
|
|
|
|
Category entries accept `@handles`, HTTPS YouTube channel URLs, or canonical `UC…` channel IDs. Mapped handles use their stable identity; unmapped handles work directly.
|
|
|
|
- **Search** and **Feed** are permanent tabs. Feed combines all `[channels]` registrations, including untagged channels, plus channels entered directly in categories, without duplicates.
|
|
- Browsing an unpinned category opens one 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 press `r`.
|
|
- 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.
|
|
|
|
## Loading and previews
|
|
|
|
Search loads 20 results per page; Feed and category views request 20 uploads per channel per page. Moving near the loaded end in normal mode fetches more asynchronously, preserving selection and existing rows. Insert mode and open popups pause automatic prefetch. Deep channel archives can get slower because yt-dlp offsets traverse earlier pages again.
|
|
|
|
Feed views merge newest-first using available upload dates. Older uploads wait while newer channel pages catch up; undated uploads appear when the feed is exhausted. Publication labels use `YYYY-MM-DD`; `~` marks an estimated UTC date, and `Unknown` means unavailable metadata.
|
|
|
|
Pane titles show animated spinners while content loads. Errors retain loaded rows and stop automatic retries; `r` refreshes the current view.
|
|
|
|
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
|
|
|
|
```sh
|
|
cargo fmt --check
|
|
cargo test --locked
|
|
cargo clippy --locked --all-targets -- -D warnings
|
|
```
|