A keyboard-driven YouTube TUI. Search via yt-dlp, play via mpv, no Invidious, no comments.
- Vim-keyed list (
j/k,gg/G,Ctrl-d/Ctrl-u,/,?) - Cancellable in-flight searches (Esc kills the
yt-dlpprocess group) - Filter results live (
/) - Re-run, new search, audio-only mode
- Refuses to launch on upcoming livestreams
- Single static binary, ~1 MB
| Tool | Why | Install |
|---|---|---|
| yt-dlp | search + URL resolution | brew install yt-dlp |
| mpv | playback | brew install mpv |
yttui will refuse to start if either is missing and tell you how to
install it.
From source:
git clone https://github.com/jbenge1/yttui
cd yttui
cargo install --path .Or directly:
cargo install --git https://github.com/jbenge1/yttuiA Homebrew formula isn't published yet. Build from source for now.
yttui # opens an empty prompt
yttui factorio mega bus # immediate search
yttui --count 50 vim # 50 results (1..=100)
yttui --audio-only "lofi hip hop" # mpv with --no-video
yttui --help
yttui --versionInside the TUI:
| Key | Action |
|---|---|
j / ↓ |
Next result |
k / ↑ |
Previous result |
gg / G |
First / last result |
Ctrl-d / Ctrl-u |
Half-page down / up |
Enter |
Play selected video |
/ |
Filter current results (live) |
n |
New search |
r |
Re-run current search |
? |
Help overlay (also shown inline on the prompt) |
q / Esc |
Quit (or cancel current modal) |
During playback, mpv takes the terminal; close mpv to return to the
result list. Esc on the searching screen cancels and kills the
yt-dlp subprocess.
mpv handles both. To cap quality or auto-load subtitles, configure mpv
itself in ~/.config/mpv/mpv.conf:
ytdl-format=bestvideo[height<=1080]+bestaudio/best
ytdl-raw-options=write-subs=,write-auto-subs=,sub-lang="en"During playback: j / J cycle subtitle tracks, v toggles visibility.
yttui reads an optional TOML config from:
$XDG_CONFIG_HOME/yttui/config.tomlif$XDG_CONFIG_HOMEis set- otherwise
~/.config/yttui/config.toml(Linux and macOS)
A missing file is fine — defaults reproduce V1 behavior exactly.
Malformed TOML is reported at error level (filter-immune) and
yttui falls back to defaults so the TUI still launches.
User-visible knobs:
[player] args— extra args appended to thempvinvocation, after ytTUI's managed flags and before the URL. mpv resolves conflicting options last-wins, so anything here can override a ytTUI default of the same option — including audio-only mode (pass--video=autoto defeat--audio-only). The URL position is fixed; user args cannot displace it.[log] level— one ofoff | error | warn | info | debug | trace. Defaultwarn. Maps directly tolog::LevelFilter.[search] videos_only— drop channel and playlist entries from search results (they render with—in the duration column and pressing Enter on one plays an arbitrary video from the channel, which is confusing). Defaulttrue; setfalse(or pass--no-videos-only) to see the unfiltered batch.[search] rich_metadata— drop--flat-playlistfrom the yt-dlp invocation to fetch full per-entry metadata, including upload timestamps. Roughly 10-20× slower per search. Defaultfalse(V1 behavior). Enable to power the upload-age column. Mirrors--rich-metadata/--no-rich-metadata.[display] upload_age_column— show a right-aligned upload-age column between channel and duration. Hidden automatically when the terminal is narrower than 80 columns. Defaultfalse. Mirrors--upload-age/--no-upload-age. Requires[search] rich_metadata = trueto show real dates; otherwise shows—.[display] upload_age_format— one ofrelative("2d ago"),absolute("2026-05-20"), orhybrid(relative when < 1 year, absolute otherwise). Defaulthybrid. Mirrors--upload-age-format <relative|absolute|hybrid>.
Example:
[player]
args = ["--save-position-on-quit", "--no-osc"]
[log]
level = "info"Precedence is built-in default < config file < CLI flag. Pass
--config <PATH> to load from a non-default path; a missing or
malformed file at that path is a hard error (a typo at the prompt
shouldn't silently fall back to defaults). The default lookup path
keeps the "missing file is fine" behavior.
Every TOML knob that affects runtime behavior has a CLI override:
| TOML | CLI |
|---|---|
[search] count |
--count <N> |
[search] videos_only |
--videos-only / --no-videos-only |
[search] rich_metadata |
--rich-metadata / --no-rich-metadata |
[display] upload_age_column |
--upload-age / --no-upload-age |
[display] upload_age_format |
--upload-age-format <relative|absolute|hybrid> |
[playback] audio_only |
--audio-only / --no-audio-only |
[player] fullscreen |
--fullscreen / --no-fullscreen |
Paired flags are last-wins: yttui --audio-only --no-audio-only is
legal and ends with audio_only = false. Env-var overrides are not
yet supported.
| Path | What |
|---|---|
$XDG_CONFIG_HOME/yttui/config.toml or ~/.config/yttui/config.toml |
optional TOML config (see above) |
~/Library/Caches/yttui/yttui.log (macOS) |
log file (level configurable) |
~/.cache/yttui/yttui.log (Linux/XDG) |
log file (level configurable) |
Apps launched from Finder, Spotlight, or Raycast inherit a minimal
$PATH that omits /opt/homebrew/bin. If yttui reports
yt-dlp not found in PATH despite a working Homebrew install, launch
it from a real terminal — Ghostty, iTerm, Terminal.app — so the
shell's $PATH applies.
yttui runs mpv (and yt-dlp) in their own process groups so a
SIGINT/SIGTERM to yttui itself, or a Ctrl-C at the parent
terminal during playback, will tear the children down with it instead
of orphaning them. The signal-watcher thread runs from process start
to exit; manual verification:
# Variant A: signal yttui from another terminal.
# Terminal 1
yttui rust ratatui # search, then Enter on a result
# Terminal 2 (while mpv is playing)
kill -INT $(pgrep -x yttui) # or `kill -TERM …`
sleep 0.2 # let the watcher kill mpv asynchronously
pgrep mpv # should print nothing# Variant B: literal Ctrl-C at yttui's terminal during playback.
# This is the user-facing path the orphan-prevention guarantee
# exists for; Variant A is *almost* equivalent because yttui keeps
# foreground (it doesn't tcsetpgrp to mpv), but exercising the
# real Ctrl-C is the spec-cited shape.
yttui rust ratatui # search, then Enter on a result
# While mpv is playing, focus yttui's terminal and press Ctrl-C.
sleep 0.2
pgrep mpv # should print nothingA second Ctrl-C arriving while the watcher is still running its
cleanup escalates: the process exits immediately, matching POSIX
shell convention. SIGKILL (kill -9) is uninterceptable — it
bypasses the watcher and will leak mpv. That's a kernel
limitation, not something yttui can fix.
All state is local — no telemetry, no cloud sync. yt-dlp talks to
YouTube directly from your IP. Logs (level configurable, warn by
default) live in your platform cache dir; safe to delete.
V1.0. See roadmap.md for V2 (thumbnails, watch
history, config) and V3 (subscription feed + personal recommendation
engine).
MIT — see LICENSE.
