lofi-player

command module
v0.1.12 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 18, 2026 License: MIT Imports: 14 Imported by: 0

README

lofi-player

English · Русский

A keyboard-driven TUI for lofi, chillhop and ambient internet radio — built to live in a tmux pane while you work.

ci release go version license

lofi-player demo

What it is

A small Go TUI that wraps mpv and turns it into a comfortable lofi station picker. Pick a stream, hit space, leave it running. Layer in some rain or a crackling fire from the ambient mixer. Drop the window into a tmux pane and forget it's there.

It is not a music library manager, not a downloader, not a Spotify client. The only thing it plays is internet radio (Icecast/Shoutcast, direct HTTP streams, and YouTube live/videos via yt-dlp).

Features

  • Internet radio — any URL mpv can open: Icecast, Shoutcast, raw HTTP/HTTPS streams. SomaFM and Radio Paradise ship in the defaults.
  • YouTube streams — Lofi Girl 24/7 and friends, routed through mpv's ytdl_hook (needs yt-dlp).
  • Ambient mixer — five looped beds (rain, fire, cafe, white noise, thunder), CC0 sources, embedded in the binary. Each channel has its own volume; mixes layer underneath the main station.
  • Eight themes — dark classics plus light palettes for bright rooms. Open the theme picker with t.
  • Mini mode — collapses to ~6 lines for a tmux pane (m).
  • macOS media key — the hardware Play/Pause key toggles the current station while lofi-player is running.
  • Tmux statuslinelofi-player --statusline prints one colored line for status-right.
  • State persistence — last station, volume, theme, ambient levels survive restarts (XDG state dir).
  • Manage stations from the TUIa adds, e edits, d deletes (with confirmation). Station kind is auto-detected from the URL.
  • ICY metadata — title/artist updates from the stream are surfaced in the now-playing card.
  • Stream info row — bitrate, codec, sample rate, session uptime, and a buffer-health bar live under the now-playing card. Toggle visibility with i; the choice persists across sessions.
  • Friendly first run — missing mpv shows a styled install hint instead of a raw error; missing yt-dlp degrades to a startup warning and an unavailable tag on YouTube stations, so the rest of your library keeps playing.

Install

Supported platforms: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64. Windows is not supported.

brew install iRootPro/tap/lofi-player

The formula pulls in mpv automatically and recommends yt-dlp for YouTube playback — one command and you're done.

One-line installer (macOS, Linux)
curl -fsSL https://raw.githubusercontent.com/iRootPro/lofi-player/main/scripts/install.sh | sh

Auto-detects OS/arch, pulls the matching tarball from the latest release, drops the binary into ~/.local/bin. Override with INSTALL_DIR=/usr/local/bin or pin to a tag with VERSION=v0.1.4.

You'll also need mpv (and optionally yt-dlp) — see runtime dependencies below. The installer prints a hint if mpv isn't on $PATH.

From source (Go 1.26+)
go install github.com/iRootPro/lofi-player@latest
Pre-built binaries

Grab the archive for your OS/arch from the releases page, extract, drop lofi-player somewhere on $PATH.

Runtime dependencies

Homebrew handles these for you. The other install paths require you to install them separately.

dependency required for install
mpv all playback brew install mpv · apt install mpv · pacman -S mpv · dnf install mpv
yt-dlp YouTube stations only brew install yt-dlp · pip install yt-dlp
Nerd Font section/volume/mixer icons JetBrains Mono or FiraCode Nerd Font

If mpv isn't on $PATH, the app prints a styled "can't start" card with platform-specific install commands and exits — there's nothing to play without the engine. If yt-dlp is missing but your config has YouTube stations, the app starts normally with a warning toast, marks YouTube rows as unavailable, and refuses to autoplay them; direct streams keep working. Without a Nerd Font, the icons render as tofu boxes; the rest of the UI keeps working.

Quick start

lofi-player

On first run, a default config with four SomaFM/Radio Paradise stations is written to ~/.config/lofi-player/config.yaml. Pick a station with j/k, hit space, drop the window into a tmux pane, get back to work.

Quit with q or ctrl+c.

Keybindings

Global
key action
j / move cursor down
k / move cursor up
space play / pause selected station
macOS Play/Pause pause / resume the current station
+ / = volume up (5%)
- / _ volume down (5%)
t open theme picker
m toggle mini mode
a add station (modal)
e edit selected station (modal)
d delete selected station (with confirmation)
s share selected station as a YAML snippet
p import station snippet from clipboard
x open ambient mixer (modal)
o open settings (network buffer)
i toggle stream-info row
? toggle full help card
q / ctrl+c quit

The macOS hardware Play/Pause key controls the station already loaded in mpv; if no station has been started yet, it is ignored.

Settings (after o)
key action
j / · k / select setting
h / · l / adjust by 5 seconds (initial buffer is capped at 10s)
0 turn selected setting off
enter save
esc cancel

Network-buffer changes are persisted for you; no manual config editing needed. Restart the app to apply them to the underlying mpv process.

Ambient mixer (after x)
key action
j / · k / select channel
h / · l / volume ±5% (fine)
H · L volume ±25% (coarse)
0 mute channel
1 channel to 100%
esc / x close mixer (state autosaves)
Add / edit station (after a or e)
key action
tab / shift+tab next / previous field
enter save (writes to config.yaml)
esc cancel

e pre-fills the form with the selected station's name and URL; enter updates it in-place. kind is auto-detected from the URL: youtube.com / youtu.beyoutube, anything else → stream.

Delete confirmation (after d)
key action
y / enter confirm delete
n / esc cancel

Deleting the currently-playing station pauses playback and clears the now-playing card. The change is written to config.yaml immediately.

Sharing stations

lofi-player shares stations as small YAML snippets you can paste into Telegram, Slack, Discord, a README, or a gist — no accounts or backend.

Press s on a station to open a share card, then enter to copy:

stations:
  - name: SomaFM Groove Salad
    url: https://ice1.somafm.com/groovesalad-256-mp3

The receiver copies that snippet and presses p in the TUI. The app previews new stations, skips duplicates by URL, and appends the rest to config.yaml after confirmation. Plain copied https://... stream URLs work too; the hostname is used as the initial station name. Clipboard support depends on the host (pbcopy on macOS; wl-copy, xclip, or xsel on Linux), so the share card always shows the text for manual copying too.

CLI users can do the same without starting mpv:

lofi-player --export-station "SomaFM Groove Salad"
lofi-player --export-all > stations.yaml
lofi-player --import stations.yaml
lofi-player --import - < stations.yaml

Configuration

Lives at $XDG_CONFIG_HOME/lofi-player/config.yaml — i.e. ~/.config/lofi-player/config.yaml on both Linux and macOS. Created on first run with sensible defaults; a documented example sits at configs/lofi-player.example.yaml.

theme: tokyo-night        # see Themes below for all built-in theme ids
volume: 60                # initial volume, 0–100
buffer_seconds: 30        # network read-ahead; try 60–120 on flaky Wi-Fi
initial_buffer_seconds: 0 # wait before start/resume; capped at 10s for live radio

stations:
  - name: SomaFM Groove Salad
    url: https://ice1.somafm.com/groovesalad-256-mp3

  - name: Lofi Girl 24/7
    url: https://www.youtube.com/watch?v=jfKfPfyJRdk
    kind: youtube         # only needed for YouTube URLs

Want more stations out of the box? A bundled preset of ~200 chillout / jazz / classical / trance streams from radiopotok.ru lives at configs/radiopotok.yaml — copy the entries you like into your own config.yaml. Regenerate from upstream with ./scripts/fetch-radiopotok.py. ~20% of these third-party streams are flaky at any moment; pick another if one fails.

Themes

Eight palettes ship in the binary:

  • Tokyo Night (default, tokyo-night) — cool, neon-on-deep-blue.
  • Catppuccin Mocha (catppuccin-mocha) — pastel-on-warm-charcoal.
  • Gruvbox Dark (gruvbox-dark) — earthy, high-contrast.
  • Rose Pine (rose-pine) — muted, soft mauve.
  • Catppuccin Latte (catppuccin-latte) — airy pastel light theme.
  • Rose Pine Dawn (rose-pine-dawn) — warm sunrise paper with soft mauve.
  • Solarized Light (solarized-light) — classic low-contrast terminal daylight.
  • Paper (paper) — warm ink on cream for bright rooms.

Open the live picker with t, preview palettes with ↑/↓ or j/k, then press enter to select or esc to cancel. The choice is persisted to state and reapplied on the next launch.

Mini mode and tmux

m collapses the UI to the now-playing card alone — about six lines. Drop the window into a small tmux pane and you have a permanent "what's playing" surface.

For an even smaller footprint, the --statusline mode prints a single colored line and exits, suitable for status-right:

lofi-player --statusline
# ♪ SomaFM Drone Zone  ▰▰▰▱▱▱  60%
set -g status-interval 5
set -g status-right '#(lofi-player --statusline)'

State

$XDG_STATE_HOME/lofi-player/state.json~/.local/state/lofi-player/state.json on both Linux and macOS. Stores last theme, volume, station name, and per-channel ambient volumes. Best-effort: a write failure logs to stderr but never aborts shutdown.

Project layout

main.go                      entry: load config + state, start mpv, run TUI
internal/
  audio/                     mpv subprocess + JSON-IPC client + ambient mixer
  config/                    YAML config + XDG paths + defaults
  share/                     station snippet import/export helpers
  state/                     state.json — last-session persistence
  theme/                     color palettes
  tui/                       Bubble Tea model / update / view / keys / styles
configs/
  lofi-player.example.yaml   documented example config
  radiopotok.yaml            ~200-station preset
scripts/
  fetch-radiopotok.py        regenerates configs/radiopotok.yaml
demo/
  lofi-player.tape           vhs script for the README GIF
plans/
  lofi-player-plan.md        roadmap (single source of truth)

Building and testing

go build -o lofi-player .
go test  ./...
go vet   ./...

The Makefile-free workflow on purpose. Releases are cut locally: git tag -a vX.Y.Z -m "..." then goreleaser release --clean — goreleaser uploads the binaries to GitHub Releases and pushes the updated formula to iRootPro/homebrew-tap in one shot. CI on main runs vet + test + build on every push.

Credits

Ambient samples are CC0 (public domain) — credited here as a courtesy and so the originals can be re-found.

channel source author
rain freesound.org/s/525046 speakwithanimals
fire freesound.org/s/760474 True_Killian
white noise freesound.org/s/132275 assett1
cafe freesound.org/s/32910 ToddBradley
thunder freesound.org/s/717890 TRP

Built on Bubble Tea, Lipgloss, Bubbles, and mpv.

License

MIT — see LICENSE.

Documentation

Overview

Command lofi-player is a TUI player for lofi/chillhop/ambient internet radio streams. See the project plan in plans/lofi-player-plan.md.

Directories

Path Synopsis
internal
config
Package config loads and persists the user's YAML configuration.
Package config loads and persists the user's YAML configuration.
share
Package share serializes and parses small station snippets that users can paste into chats, READMEs, gists, or back into lofi-player.
Package share serializes and parses small station snippets that users can paste into chats, READMEs, gists, or back into lofi-player.
state
Package state persists the user's last session — last station, volume, theme, plus a forward-compatible Pomodoro blob filled in Phase 3.
Package state persists the user's last session — last station, volume, theme, plus a forward-compatible Pomodoro blob filled in Phase 3.
theme
Package theme defines the color palettes used by the renderer.
Package theme defines the color palettes used by the renderer.
tui

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL