Project pageView on GitHub

needle

A terminal music player with a full-screen TUI, a no-TUI mode that keeps your shell, a background daemon and a JSON-lines interface for scripts.

Features · Install · Use it · Keys · Configuration · How it works · Limits · Contributing

License: MIT Go 1.27+ macOS · Linux Backend: mpv · afplay

needle's TUI: artists, albums and tracks in three columns, with the cover and a progress bar below

Run it locally, step by step

  1. Install mpv: brew install mpv on macOS or sudo apt install mpv on Debian and Ubuntu.

  2. Get the code:

    git clone https://github.com/Gitguy268/needle.git
    cd needle
    
  3. Install Go 1.27 or newer.

  4. Build it:

    go build -o needle .
    
  5. Play some music:

    ./needle play ~/Music/song.mp3
    

    Run ./needle on its own for the full-screen player. See Keys below for how to quit and navigate.

Features

Mode Command What you get
TUI needle Library in three columns (artist or genre, album, track), queue, live search, half-block cover art
Inline needle play <files> A few live lines under your prompt with single-key controls; the played tracks stay above as history
Plain needle play <files> | cat One line per track, no escape codes, for pipes and logs
Daemon needle daemon --background Plays with no UI; needle next, status, seek, vol and more control it
JSON lines needle --headless, needle --raw Commands on stdin and events on stdout, or events only
Library needle index, needle list A SQLite index of your tags with search and grouping

The animated demo browses to an album, plays a track, opens the queue and searches

Install

needle needs mpv. On macOS it can also play through afplay, which ships with the system, but with fewer controls (see Limits).

brew install mpv          # macOS
sudo apt install mpv      # Debian, Ubuntu

Then install needle with Go 1.27 or newer:

go install github.com/Gitguy268/needle@latest

Or build it from a clone:

git clone https://github.com/Gitguy268/needle.git
cd needle
go build -o needle .

The binary is pure Go (no CGO), so it cross-compiles with GOOS=linux or GOOS=darwin. Optional: install ffmpeg so needle index stores track durations up front. Otherwise they're filled in as tracks play.

Effects are optional and need Pedalboard, a Python package. needle still ships as one Go binary and starts Python only when effects are enabled:

python3 -m pip install pedalboard

Set NEEDLE_PYTHON to use a different interpreter, for example from a virtualenv. If effects are off, Python is never run. The effects tests and manual checks used Pedalboard 0.9.26.

Use it

Full-screen TUI

needle                  # your library; the first run scans ~/Music
needle ~/Downloads/album  # just these files, in the queue view

Pick an artist, an album or a track and press Enter to play it. 1 2 3 switch between Library, Queue and Search. ? shows every key.

Without the TUI

needle play ~/Music/Jazz
needle play song.flac https://radio.example/stream.mp3 mix.m3u
needle play --fx bass ~/Music/Jazz    # Pedalboard effects for local files

The inline player: each track's cover and title stays in the scrollback, with live status lines below

When stdout is a pipe or a file, needle play prints one plain line per track and exits when the queue ends:

needle play ~/Music/Ambient | tee played.txt

Daemon and control commands

needle daemon --background ~/Music/Ambient   # or run it in the foreground without --background
needle status
needle next
needle vol -20
needle seek 1:30          # also +10, -10, 90
needle shuffle on
needle repeat all         # off, all, one
needle add ~/Music/more   # load replaces the queue instead
needle queue
needle jump 4
needle watch              # print each new track as it starts
needle quit

A shell session: daemon, status, next, volume and a library search

These commands talk over a Unix socket, so they also control a TUI or inline player that is already running. That makes them easy to bind to media keys or a status bar. Add --json to status, queue and watch for machine-readable output.

JSON lines

--headless reads one JSON command per line on stdin and writes replies and events on stdout. --raw plays files and only writes events.

printf '%s\n' '{"cmd":"load","paths":["/music/a.flac"]}' '{"cmd":"seek","value":30}' | needle --headless
needle --raw ~/Music/song.mp3 | jq -r 'select(.event=="track") | .state.track.title'
Commands and events

Commands take cmd plus, depending on the command, index (queue position from 0), path or paths, value, relative and mode. An optional id is echoed in the reply.

cmd Fields Does
play index or paths (optional) resume, play a queue position, or load and play paths
pause, resume, toggle, stop playback state
next, prev prev restarts the track after 3 seconds
seek value, relative seconds
volume value, relative 0 to 100
shuffle mode: on, off, toggle
repeat mode: off, all, one, cycle
add, load path or paths files, folders, .m3u and URLs; load replaces the queue and plays
clear, remove index for remove
status, queue reply with state or queue
subscribe mode: positions (optional) stream events on this connection
quit stop the daemon or headless session

Replies look like {"ok":true,"state":{...}} or {"ok":false,"error":"..."}. Events look like {"event":"track","state":{...}}, where event is one of track, state, position (once a second), queue, error or finished. The state object holds status (stopped, playing, paused or loading), track, index, length, position, duration, volume, shuffle, repeat, backend and caps (what the backend can do).

The same protocol runs on the daemon socket, so socat - UNIX-CONNECT:$HOME/.cache/needle/needle.sock works as a client too.

Library

needle index                       # scan the folders in config.toml (default ~/Music)
needle index ~/Music ~/Downloads   # or these
needle list jazz                   # words match title, artist, album and genre
needle list --artist "Mira Vale" --album Glasshouse
needle list --group artists        # or albums, genres
needle list --search rain --paths > rain.m3u && needle play rain.m3u
needle list --json

Rescans only read files whose size or modification time changed. In the TUI, u rescans.

Cover art

needle art song.flac prints a cover once. It uses Kitty graphics in Kitty and Ghostty, iTerm2 images in iTerm2 and WezTerm, Sixel in foot and mlterm, and half blocks everywhere else (--mode picks one). The TUI and the inline player always draw half blocks, which survive redraws in every terminal with colour.

Keys

TUI
Key Action
Space play / pause
n p next / previous
[ ] · { } seek 5 s · 30 s
+ - volume
s r S shuffle · repeat mode · stop
1 2 3 or Tab Library · Queue · Search
/ search
↑ ↓ j k, PgUp PgDn, g G move
← → h l library column
Enter play the selection (on a track: the whole list, from that track)
a · A add to the queue · add all search results
b browse by genre instead of artist
d D c queue: remove · clear · go to current
u rescan the music folders
? · q help · quit
Inline player
Key Action
Space play / pause
n p next / previous
← → · Shift+← → seek 5 s · 30 s
+ - volume
s r shuffle · repeat mode
q quit

Configuration

Everything works without a config file. To change something, copy config.toml.example to ~/.config/needle/config.toml:

music_dirs = ["~/Music", "/Volumes/NAS/Music"]
backend = "auto"     # auto, mpv or afplay
volume = 80
art = "auto"         # auto, halfblock, kitty, iterm, sixel or off

[effects]
enabled = false      # off unless you ask for it
preset = "bass"      # bass, vocal, warm or loud
cache_mb = 1024      # processed WAV cache under ~/.cache/needle/effects
# [[effects.chain]]  # a custom chain replaces the preset
# type = "PeakFilter"
# cutoff_frequency_hz = 250
# gain_db = -2.5
# q = 1.0

[theme]
accent = "#e0af68"   # "#rrggbb" or an ANSI number such as "13"
dim = "8"
select = "#7aa2f7"

Two themes are built in, Nujabes and Ye: Graduation. Set name = "nujabes" or name = "graduation" under [theme], and remove the colour lines, because colours set in the file override the preset. The TUI recolours the terminal where the terminal allows it, and both the TUI and the inline player show a line of text from the theme. Both themes are unofficial tributes; see NOTICE.

Flags override it for one run: --backend, --volume, --shuffle, --repeat, --art, --fx, --theme. --theme takes default, nujabes or graduation. --fx takes off, bass, vocal, warm, loud or custom (the chain from config).

File Default Override
Config ~/.config/needle/config.toml $XDG_CONFIG_HOME
Library index ~/.local/share/needle/library.db $XDG_DATA_HOME
Daemon log ~/.local/share/needle/daemon.log $XDG_DATA_HOME
Control socket ~/.cache/needle/needle.sock $XDG_RUNTIME_DIR, $NEEDLE_SOCKET
Effects cache ~/.cache/needle/effects $XDG_CACHE_HOME
Extra mpv options $NEEDLE_MPV_ARGS, for example --ao=null or --audio-device=...
Python for effects python3 on PATH $NEEDLE_PYTHON

How it works

Design

Limits

Contributing

Bug reports, ideas and pull requests are welcome. See CONTRIBUTING.md for building and the checks, SECURITY.md for reporting security problems privately, and the Code of Conduct.

Credits

needle's design borrows ideas, not code, from cliamp, musikcube, listnr, Kronos and haskell-player. NOTICE says which idea came from where, and lists the libraries needle is built with.

License

MIT © 2026 Lucas Van De Weerd. Third-party licences are listed in NOTICE. Pedalboard is GPL-3.0 and is used only as a separate, optional Python program; it is not linked into needle.