import { Link } from 'react-router' import { Button } from '@/components/ui/button' import { ExtLink } from '@/components/ui/ext-link' import { Code } from '@/components/ui/inline-code' import { Cols, Eyebrow, PageHead, Prose, Section, SectionHead, } from '@/components/ui/layout' import { Callout, Panel } from '@/components/ui/panel' import { Terminal } from '@/components/ui/terminal' import { LINKS } from '@/lib/links' const GLOBAL_KEYS: readonly (readonly [string, string])[] = [ ['s', 'search the network'], ['Space', 'select a result'], ['Enter', 'download / send'], ['b', "browse the owner's files"], ['c', 'chat rooms'], ['m', 'compose a private message'], ['i', 'inbox (unread counter)'], ['/', 'filter the current list'], ['a / A', 'select all / none'], ['1–3', 'focus a pane'], ['q', 'quit'], ] const ROOM_KEYS: readonly (readonly [string, string])[] = [ ['Enter', 'join room / send message'], ['Tab · ⇧Tab', 'switch between open rooms'], ['x', 'leave the active room'], ['l', 'back to the room list'], ['↑ ↓', 'select a member'], ['b', "browse the member's files"], ['m', 'message the member'], ] const PANES: readonly { num?: number; title: string; body: string }[] = [ { num: 1, title: 'Searches', body: 'Your queries and how many results each returned. Press s to search the network; each search keeps its own result set.', }, { num: 2, title: 'Results', body: 'Files from the network: size, user, bitrate, speed, free slots. Space selects, Enter queues a download.', }, { num: 3, title: 'Downloads / Uploads', body: 'Live transfers in both directions with progress and speed: queued, active, complete, failed.', }, { title: 'Info', body: 'Everything about the selected file: user, size, path, bitrate, length, queue position, free slots.', }, ] const KeyRow = ({ combo, description }: { combo: string; description: string }) => (
cargo install soulseek-rs does the same.
On first run you get a login screen: sign in with an existing Soulseek account or register a new one, unless a daemon is already running, in which case the TUI attaches to it and asks for nothing. Your password goes to the OS keychain, not a plain-text file.
Downloads land in a conventional folder, and TUI state (searches, downloads, rooms) is restored across restarts.
1–3 to focus
one; the focused pane’s legend turns green.
{pane.body}
The command groups: search, get, download,{' '}
browse, wish, room, message,{' '}
serve, user, whoami, shares,{' '}
daemon, config, portmap,{' '}
skills, completions. Records are tab-separated fields,
one per line, or NDJSON with --json.
No TTY required. search --json pipes through jq into{' '}
download --stdin. Every ending has its own exit code: 4 found
nothing, 3 could not reach the server, 6 the transfer died, 7 another login took
the session away, which is the one a daemon spares you.
completions install gives bash, zsh and fish tab completion for all
of it, generated from the same definition the binary parses with.{' '}
completions uninstall takes it back out.
portmap tests whether your router will let peers connect back, worth
running once.{' '}
More on being reachable
soulseek-rs skills install writes a SKILL.md into every
coding agent on the machine: Claude Code and opencode today, --dir{' '}
for anything else. After it lands you describe the outcome instead of the
pipeline: “mirror that peer’s FLAC folder”, “watch this
room for an hour and save anything mentioning the label”.
It carries what --help cannot: the JSON keys of every record, and
what each exit code means. Rerun it after an upgrade;{' '}
skills uninstall takes it back out.
If both you and a peer are behind routers with no forwarded port, browsing that peer can’t work, that’s a fundamental Soulseek/peer-to-peer limitation, not a bug.
Commands find it themselves: it listens on a Unix socket only your user can
open, so there is no address to pass and no token to handle. Without one
running, everything behaves exactly as it did before, and{' '}
--no-daemon gets that back for a single command.{' '}
daemon status exits 3 when none is running, which is how a script
decides whether to start one. That socket is a Unix one: on Windows, start the
daemon with --bind 127.0.0.1:5030 and point commands at it with{' '}
config set daemon and config set daemon_token.
Every window is a view of the same session. Open a second TUI and it shows the searches and transfers already running; queue something in either and both see it. There is no login screen, because the daemon is already signed in, and the inbox opens on the private messages it collected while nobody was watching. The settings form edits the daemon’s download folder and share list, so the paths you type there name its filesystem rather than this machine’s. The status bar names the daemon, and closing a window leaves its downloads going, because they belong to the daemon rather than the window.
Attach one and commands report its world: whoami names its account
and server, download prints the path on its disk, and{' '}
serve --follow watches its uploads instead of starting a second
server. It holds the session rather than the wishlist: standing searches re-run
under serve --wishlist, or when you call wish run.
Why bother: the server permits a single session per account. Two one-off commands at once either cut each other off, or push you into a throwaway username per run, which is account churn on a network other people maintain. A daemon is one login for as long as you work, one share index instead of a rescan every invocation, and downloads that outlive the command that queued them. If you drive this from a script or an agent, start one.
Your laptop needs no Soulseek account of its own; it uses the box’s. Queue
what you want, then close the lid. The downloads keep going, because they
belong to the daemon rather than to the command that asked for them. Come back
tomorrow, run daemon status, and pick up where you left off. The
files are on the box, which is where you wanted them.
The two config set lines are the one-time version;{' '}
--daemon ADDR and SOULSEEK_DAEMON point a single
command at a different box. On the box itself,{' '}
daemon --upload-slots N sets how many uploads it serves at once.
There is no encryption on that port. On a home network that is usually fine.
Over the internet, reach it through SSH instead:{' '}
ssh -L 5030:localhost:5030 nas, or run the commands on the box.
config set download_dir and shares add reach the
running daemon and take effect at once, but the box reads its own{' '}
config.toml when it restarts, so make the lasting changes there.{' '}
shares add also checks the folder on the machine you type it on, so
a path that exists only on the box fails from your laptop: add shares on the box,
or in the attached TUI’s settings form, which sends paths through as typed.
It speaks newline-delimited JSON-RPC 2.0, described by an{' '}
rpc.discover, so a client can
ask a running daemon what it can do. The same document is checked in at{' '}
auth answers with the protocol version, 1 today, so a
client written against an older shape learns it at connect time.
OpenRPC is the JSON-RPC counterpart to OpenAPI: point a generator at it and you get typed bindings in whatever language you are working in. Want an Android or iOS app that queues downloads on your home server from the sofa, or a web page showing what is transferring? You have what you need and nobody to ask.
Searches, transfers, rooms, private messages and shares are all on the same
interface this CLI uses. There is no private back channel it keeps for itself,
and live updates are pushed to every connected client rather than polled for:{' '}
event.room, event.message, event.upload,{' '}
event.download_status, event.browse and{' '}
event.session_loss. One rule to write against: the download folder
belongs to the daemon, so download.set_dir moves it for every
transfer and a per-download destination is ignored.
The README on GitHub and the library API reference on docs.rs stay current with the code.