# ytsurf, YouTube in a terminal with a picker you choose

> ytsurf is a bash script that turns YouTube into a terminal workflow: it searches with yt-dlp, shows results in fzf, rofi, sentaku or tv, and plays through mpv. It is a thin, readable wrapper rather than an application, and it depends on eight other tools to do its job.

**Stan-breaks/ytsurf** — YouTube in your terminal. Clean and distraction-free.

- Repository: https://github.com/Stan-breaks/ytsurf
- Stars: 615 · Forks: 38
- Language: Shell
- License: GPL-3.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/stan-breaks-ytsurf

## A shell script wrapped around yt-dlp and mpv

ytsurf is described in one line as YouTube in your terminal, clean and distraction-free, and the project is a bash script. The repository top level is `ytsurf.sh` plus a Homebrew formula `ytsurf.rb`, a Nix flake with `flake.nix` and `flake.lock`, a `package.nix`, and the usual community files.

The design is easier to understand from the dependency list than from the feature list.

The required set is bash, yt-dlp, jq, curl, mpv, fzf, chafa and ffmpeg, and the optional set is rofi, sentaku and syncplay.

So the script does the searching and the orchestrating, yt-dlp does the actual extraction of video information and streams, mpv does the playing, jq handles the JSON that yt-dlp produces, and chafa renders images in the terminal. fzf provides the default interactive picker. curl and ffmpeg round out the required set.

That is the trade this kind of tool makes. You get a small, readable script instead of an application, and in exchange you inherit the behaviour, the update cadence and the failure modes of eight separate programs.

The audience is a terminal user on Linux or macOS who wants to search and watch without a browser, and who is comfortable with a shell script and a package manager. The stated features include syncplay support to watch videos together in sync, audio-only playback and downloads, downloading videos or audio, interactive format and quality selection, an external config file, playback history and quick replay, resuming playback where you left off because mpv and iina save position on quit, queues and saved playlists, an adjustable search result limit, a custom download directory, self-update for manual installations only, copying short YouTube URLs to clipboard, channel subscriptions with a personalized feed, and importing subscriptions from YouTube.

The last push to the repository was on 2026-10-04, and the recorded releases are v3.1.8 from 2026-07-21, v3.1.7 from 2026-05-06 and v3.1.6 from 2026-03-30.

## Four pickers, and the choice is the main design decision

The most interesting table in the project is the picker comparison, because it makes the one architectural decision visible.

The four rows of that table read like this. fzf is the default, terminal-based, with thumbnail previews, described as lightweight and best for most users because it is fast with previews. rofi is a GUI menu, keyboard-driven with a clean look, for users who prefer a graphical menu. sentaku is very minimal with no previews, for systems without Go or fzf support. tv is terminal-based, similar to telescope.nvim, for users who want a fancier terminal-based picker.

fzf is the default, and the reason is the previews. chafa is in the required dependency list for exactly this reason, and the fzf entry names thumbnail previews as its distinguishing feature. The tool renders the video thumbnail in the terminal next to the result, which is the difference between a list of titles and something you can actually choose from.

sentaku is the escape hatch for a machine that cannot run Go or does not have fzf, and the documentation notes that the `--sentaku` flag exists for systems that cannot compile go. tv is the option for people who want the telescope.nvim style picker, which is a fuzzy finder with a preview pane.

The rofi option is the interesting one for a different reason. rofi is a graphical menu launcher, so a user who wants a window rather than a terminal interface can get one without the script changing shape, and the `--rofi` flag selects it.

This is a better design than a script that hardcodes one picker, because the picker is the part users have the strongest opinions about and the part most likely to be replaced by a better tool later. The trade-off is that the four options are not equally capable: sentaku has no previews, and the preview quality depends on chafa and on your terminal's image support.

## Installing it four ways, including one that does not need a package manager

The installation section covers four routes, and they are not all equal in convenience.

The Arch Linux route is an AUR package.

```bash
yay -S ytsurf
# or
paru -S ytsurf
```

Homebrew is the route most macOS users will take, and it needs the tap first.

```bash
brew tap stan-breaks/ytsurf https://github.com/stan-breaks/ytsurf
brew install stan-breaks/ytsurf/ytsurf
```

NixOS is supported with a flake, and the documentation gives a complete system configuration in the `flake.nix` that pulls nixpkgs and ytsurf as inputs and applies the ytsurf overlay before adding the package to `environment.systemPackages`.

The manual route is the interesting one, because it is the route that reveals what this project actually is. There is no binary to download and no build step.

```bash
mkdir -p ~/.local/bin
curl -o ~/.local/bin/ytsurf https://raw.githubusercontent.com/Stan-breaks/ytsurf/main/ytsurf.sh
chmod +x ~/.local/bin/ytsurf
```

You create a directory, curl one file, and make it executable. The `~/.local/bin` directory then has to be on your PATH, which the documentation calls out explicitly.

That single-file install is why the self-update feature is described as being for manual installations only. The `--update` flag can rewrite the script in place, which only makes sense if the script is the thing you installed and there is no package manager to coordinate with. For the AUR, Homebrew and Nix routes, your package manager owns the version.

The dependencies still have to be installed separately in every case. On Arch the documentation gives the command.

```bash
sudo pacman -S yt-dlp jq curl mpv fzf chafa rofi ffmpeg
```

So the honest summary is that the install of ytsurf itself is one file, and the install of everything ytsurf needs is eight packages, and yt-dlp in particular is the one that has to stay current for the site to keep working.

## The option surface, and a configuration file

The usage help in the documentation is the complete option list, and it reads like the tool's feature set in one place.

```bash
USAGE:
  ytsurf [OPTIONS] [QUERY]

OPTIONS:
  --audio         Play/download audio-only version
  --download      Download instead of playing
  --format        Interactively choose format/resolution
  --rofi          Use rofi instead of fzf for menus
  --queue, -q     Use it to add or play queues
  --playlist      Play your saved playlist
  --syncplay      Watch youtube with friend from the terminal
  --subscribe, -s Add a channel to subscriptions locally
  --unsubscribe   Remove a channel to subscriptions locally
  --import-subs   Import subscriptions from youtube
  --feed,-F       View videos from your feed
  --sentaku       Use sentaku instead of fzf or rofi(for system that can't compile go)
  --history       Show and replay from viewing history
  --limit <N>     Limit number of search results (default: $DEFAULT_LIMIT)
  --edit, -e      edit the configuration file
  --help, -h      Show this help message
  --version       Show version info
  --copy-url      Copy or display the video link
  --watch, -w     Watch videos directly without any options after choosing a video
  --update, -u    Update a manual install
```

Run `ytsurf` with no arguments and you enter interactive mode, and the examples show the shape of a query, such as `ytsurf lo-fi study mix`, with the `--audio` and `--download --format` variants for the other modes.

Two details in that list are worth pausing on. The `--limit` option's default is shown as `$DEFAULT_LIMIT`, which points at a shell variable rather than a number, so the default is configurable in the environment as well as in the config file. And `--subscribe` is described as adding a channel to subscriptions locally, while `--import-subs` imports subscriptions from YouTube, so the local subscription list and the imported one are two different things that the script keeps.

The configuration file lives at `~/.config/ytsurf/config`, and the rule is that CLI flags always override config values.

```bash
# ~/.config/ytsurf/config

# Set a higher default search limit
limit=25

# Always use audio-only mode
audio_only=true

# Set a custom download directory
download_dir="$HOME/Videos/YouTube"
```

It is a plain key=value file rather than YAML or JSON, which fits a shell script and means the `--edit` flag can open it in your editor.

## The mpv and youtube-dl conflict, and why the fix is a rename

The troubleshooting section documents one specific failure, and it is a good illustration of how a wrapper inherits the behaviour of its parts.

The symptom: MPV doesn't play the selected video. Some systems have mpv configured to prefer youtube-dl instead of yt-dlp, which causes ytsurf to show "playing..." without actually starting playback. The script hands the URL to mpv, mpv calls whatever downloader it prefers, and the older youtube-dl no longer handles the site, so nothing plays and the interface says it is working.

The fix offered is to rename your yt-dlp binary so that MPV uses it automatically.

```bash
sudo mv /usr/bin/yt-dlp /usr/bin/youtube-dl
```

The documentation then offers a symlink instead, and calls it safer.

```bash
sudo ln -s /usr/bin/yt-dlp /usr/local/bin/youtube-dl
```

This is a workaround in the literal sense. It does not configure mpv to prefer yt-dlp, it makes the old name point at the new binary so that whichever one mpv asks for resolves to a working program. The symlink is the better of the two because it is reversible and does not move a package-managed file out of the package manager's view.

It is also a decent illustration of the fragility this class of tool has. The failure is silent, the fix is a filesystem trick, and the root cause is a configuration in a program ytsurf does not control. If you are the kind of user for whom that trade is acceptable, the whole tool is a pleasant script. If you are not, the same job is better done with a media player you configure deliberately.

## What you get, what you depend on, and the licence

Two things are worth separating: what the script does and what it assumes.

What it does is orchestrate. It searches, it presents a list, it hands a chosen stream to mpv, it keeps a history and a queue, and it writes a config file and a subscription list. The heavy lifting, the actual downloading of media and the actual decoding and playback, belong to yt-dlp and mpv.

What it assumes is a working Unix-like environment with a terminal that can render images if you want the previews, a `PATH` containing the tools, and an up-to-date yt-dlp. The project handles the picker variety and the config, but it does not handle the case where yt-dlp's site support has drifted, which is the failure that will actually happen to you eventually.

The feature list has one entry that is a bit of a category error. Resumes playback where you left off, with the note that mpv and iina save position on quit, is not a feature of ytsurf at all. It is a statement about the tools it drives. The same applies to the syncplay support, which requires the optional syncplay dependency to be installed.

For the licence, the project is released under the GNU General Public License v3.0, with a LICENSE file at the top of the repository. That matters if you fork it, because the copyleft terms attach to a distributed derivative. For personal use and for a local fork it is not a constraint. This is not legal advice, it is the reason to read the file.

The contribution model is documented, with a CONTRIBUTING.md and a FUTURE_FEATURES.md for the ideas that have not been built yet. The project also has an XMPP community channel, which is an unusual choice and a sign of who the audience is.

If you are evaluating ytsurf, the decisive question is not what it can do but whether you want a terminal-first YouTube client that depends on eight moving parts, or whether you want a browser and a video player. The script wins on speed and keyboard-only workflow. It loses on predictability.

## Conclusion

ytsurf suits a Linux or macOS user who lives in the terminal and wants to search, watch and download YouTube without opening a browser, and it suits people who want to swap the picker without changing anything else. It does not suit a Windows user without WSL, and it does not suit a team that needs a maintained, packaged application, since this is a single shell script whose behaviour depends on eight external tools. Verify first that your terminal can render the thumbnail previews the default picker relies on, that yt-dlp is recent enough for the site to work with, and that you understand the GPLv3 terms before you fork it into something you distribute.

## FAQ

### What is ytsurf and what does it need to run?

ytsurf is a bash script that puts YouTube in the terminal, searching with yt-dlp and playing with mpv. The required dependencies are bash, yt-dlp, jq, curl, mpv, fzf, chafa and ffmpeg, with rofi, sentaku and syncplay optional.

### How do I install ytsurf manually?

Create the directory, curl the script and make it executable, with mkdir -p ~/.local/bin, then curl -o ~/.local/bin/ytsurf https://raw.githubusercontent.com/Stan-breaks/ytsurf/main/ytsurf.sh and chmod +x ~/.local/bin/ytsurf. Make sure ~/.local/bin is on your PATH. It is also packaged for Arch through the AUR, for macOS through Homebrew, and for NixOS through a flake.

### Can I use a different menu picker than fzf with ytsurf?

Yes. The project documents four selectors: fzf as the default with thumbnail previews, rofi as a GUI menu, sentaku as a very minimal option with no previews for systems without Go or fzf support, and tv as a terminal-based picker similar to telescope.nvim. The --rofi and --sentaku flags select the alternatives.

### Why does ytsurf show playing but no video appears?

The documentation explains that some systems have mpv configured to prefer youtube-dl over yt-dlp, which makes ytsurf display playing without starting playback. The fix is to point the youtube-dl name at yt-dlp, either by renaming the binary or, as the documentation suggests is safer, by creating a symlink.

### Where does ytsurf keep its configuration?

Defaults are read from ~/.config/ytsurf/config, which is a plain key=value file, and command line flags always override the config values. The --edit flag opens that file for editing, and the example shows limit, audio_only and download_dir settings.

## Sources

- [Issues](https://github.com/Stan-breaks/ytsurf/issues)
- [License: GPL-3.0](https://github.com/Stan-breaks/ytsurf/blob/main/LICENSE)
- [README](https://github.com/Stan-breaks/ytsurf/blob/main/README.md)
- [Releases](https://github.com/Stan-breaks/ytsurf/releases)
- [Stan-breaks/ytsurf on GitHub](https://github.com/Stan-breaks/ytsurf)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/stan-breaks-ytsurf
