docs: tell the whole daemon story, and stop denying it exists

An audit of the README and the site against the implementation found 38
places the docs were missing, stale, or wrong about daemon mode. Two were
outright false: the community page still answered "want a queue that
outlives the command?" with "slskd has that", and the home page promised
"no daemon, no API to stand up" fifty lines before the section that
describes both.

The download-box walkthrough now tells the truth about its sharp edges: a
pushed folder or share list is live-only on a remote daemon, whose own
config.toml wins at restart; `shares add` validates the path on the
machine you type it on; and a `get` that reaches its `--timeout` cancels
the daemon-side transfer. Its shell example also runs as written now — the
old block put a second command after a foreground daemon that never
returns.

The reference material catches up with the feature: `daemon` and its
subcommands join the command list, the config table gains the
daemon/daemon_token/--no-daemon rows, nine settings become eleven,
`daemon status` and `daemon token` get documented record shapes, exit 3
covers daemons, `queued` joins the serve statuses, the credential-free
count goes 13 to 19, and the action-command count 4 to 6.

And the surprises are written down rather than discovered: the daemon does
not sweep the wishlist on its own; Windows has no socket and so no
auto-attach; the attached TUI's settings form edits the daemon's folders
and rewrites this machine's config.toml; `serve` monitors a daemon's
uploads instead of starting a second server; `whoami` reports the
daemon's world, with --no-daemon as the way out. The OpenRPC section names
the six event methods and the session-wide download.set_dir rule.

Site typechecks, lints, and builds through prerender; every claim was
verified against the cited code before it was written.
This commit is contained in:
Michel de Graaf
2026-07-29 09:22:02 +02:00
parent f1b667f7f9
commit ab5cef8cfe
5 changed files with 221 additions and 69 deletions
+120 -48
View File
@@ -46,6 +46,8 @@ does, how to install it, and every `config.toml` setting.
- **TUI and CLI**: a full terminal interface, plus one-shot commands for
scripts and agents: every feature reachable without a terminal, stdout
carrying records, the exit code saying what happened
- **Daemon mode**: one login several clients share, on this machine or over
the network, so a download outlives the command that queued it
## Project Goals
@@ -56,10 +58,6 @@ programming and reverse engineering.
The library stays lean on dependencies and has none today; the client takes
them freely.
## Planned Features
- [x] Headless daemon mode with remote control
## Project Structure
A Cargo workspace with two crates:
@@ -137,11 +135,19 @@ attached. If you drive this from a script or an agent, start one.
Commands find it by themselves. It listens on a Unix socket only your user can
open, so there is nothing to configure and no secret to pass around. Docker and
ssh-agent make the same arrangement. With no daemon running, everything
behaves exactly as it did before; `--no-daemon` forces that for one run.
ssh-agent make the same arrangement. The socket is `daemon.sock` in the state
directory (`~/.local/share/soulseek-rs/state`, or wherever `SOULSEEK_STATE_DIR`
points), with the token remote clients need beside it in `daemon.token`. With
no daemon running, everything behaves exactly as it did before; `--no-daemon`
forces that for one run.
Windows has no Unix socket. A daemon there needs `--bind ADDR`, and every
client needs `--daemon ADDR` and the token even on the same machine.
From a script, start it and *wait* for it: it takes a second or two to log in,
and a command that arrives early opens a session of its own instead.
and the socket is there before the login finishes. A command that arrives early
attaches to it and then waits on the handshake, up to 30 seconds. Poll
`daemon status` until it answers first.
```bash
soulseek-rs daemon status --json >/dev/null 2>&1 || {
@@ -163,15 +169,29 @@ second TUI and it shows the searches and transfers already running, including
ones the first started; queue something in either and both see it. The status
bar names the daemon it is attached to, so it is obvious which you are looking
at. Closing a window leaves its downloads running, because they belong to the
daemon.
daemon. The settings popup edits the daemon's folders while you are attached:
the paths are the daemon's, they are validated there, and nothing is created on
this machine. Applying them also writes those paths into this machine's
`config.toml`, which becomes your local download folder the next time you run
without a daemon.
Everything that touches the network goes through it. `config`, `skills`,
`completions` and `portmap` stay local, because they are about this machine
rather than a session. `shares add`/`remove` and `config set download_dir` do
both: they write the config file *and* update a running daemon, so a folder is
served — or downloaded into — straight away rather than at the daemon's next
start. Files land on the *daemon's* filesystem, and `daemon status` reports
where.
rather than a session. `shares add`/`remove`, `config set download_dir` and
`config set shared_dirs` do both: they write the config file *and* update a
running daemon, so a folder is served — or downloaded into — straight away
rather than at the daemon's next start. Files land on the *daemon's*
filesystem, and `daemon status` reports where.
It does not sweep the wishlist. Standing searches re-run inside
`serve --wishlist` or a one-off `wish run`, both of which use the daemon's
session when one is up, so put either on a schedule if you want the list worked
through. With a remote daemon the list itself lives in the client's
`config.toml`, so `wish run` from the laptop drives the daemon against the
laptop's wishes.
`soulseek-rs daemon --upload-slots N` sizes the upload queue for the session it
holds: 1 to 1000, where `serve` stops at 64.
#### A download box you drive from your laptop
@@ -184,11 +204,17 @@ network:
```bash
soulseek-rs daemon --bind 0.0.0.0:5030
soulseek-rs daemon token # copy this, you need it once
```
It stays in the foreground and prints the token on stderr as it starts, with
the two `config set` lines to paste on the other machine. `daemon token` prints
it again, and always *this* machine's token, so run it on the box rather than
on the laptop.
It needs no terminal and no one logged in; hand it to systemd or launchd and
forget about it.
forget about it. Run the unit as the user who types the commands, or export the
same `SOULSEEK_STATE_DIR` in both, because a client looks for the socket under
its own state directory and finds nothing under someone else's.
**On your laptop**, say once where it lives:
@@ -206,12 +232,26 @@ soulseek-rs get 'selected ambient works'
soulseek-rs daemon status # what it's doing right now
```
`shares add` resolves the folder against the filesystem of the machine you type
it on, so `shares add /srv/music` from the laptop exits 2 unless the laptop has
that path too. Add the box's folders on the box, or over `ssh`.
A share list or download folder pushed to a remote daemon changes its live
session, and the config file the command wrote is the laptop's. The daemon
reads its own `config.toml` at the next start, so put the change there as well,
or push it again after a restart.
Then close the lid. **The downloads keep going**, because they belong to the
daemon, not to the command that asked for them or to the laptop that ran it.
Come back tomorrow, run `soulseek-rs daemon status` again, and pick up where
you left off. The files are on the download box, which is where you wanted
them.
A transfer outlives the command being killed and the laptop going away, but not
the command's own deadline. A `download` or `get` that reaches `--timeout`
(300s by default) tells the daemon to drop the transfer before it exits 5.
Raise `--timeout` for a big file, or queue it from the attached TUI and quit.
When you are finished with it:
```bash
@@ -241,6 +281,11 @@ 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.
The handshake refuses a client and a daemon on different protocol versions, so
keep both ends on the same release. A destination is session-wide:
`download.set_dir` moves it for every transfer, and `download.start` ignores
any directory the caller names.
[`docs/daemon-protocol.md`](docs/daemon-protocol.md) covers the parts a schema
cannot state: the framing, the handshake, and how pushed events behave.
@@ -254,16 +299,16 @@ The one-shot commands follow three rules, so they compose with other tools:
records are tab-separated fields with no header and no decoration.
- **The exit code is the verdict.** No output plus exit 0 never means failure.
| Code | Meaning |
| ---- | ---------------------------------------------------------- |
| 0 | success |
| 1 | unexpected error |
| 2 | bad arguments, missing credentials, unusable configuration |
| 3 | could not reach the server, or the login was rejected |
| 4 | the command worked but found nothing |
| 5 | timed out waiting for a response or a transfer |
| 6 | a transfer started but did not finish |
| 7 | the session ended mid-command; nothing it saw is reliable |
| Code | Meaning |
| ---- | ----------------------------------------------------------------- |
| 0 | success |
| 1 | unexpected error |
| 2 | bad arguments, missing credentials, unusable configuration |
| 3 | could not reach the server or a daemon, or the login was rejected |
| 4 | the command worked but found nothing |
| 5 | timed out waiting for a response or a transfer |
| 6 | a transfer started but did not finish |
| 7 | the session ended mid-command; nothing it saw is reliable |
#### Commands
@@ -282,6 +327,8 @@ soulseek-rs message send <USER> <TEXT> # send a private message
soulseek-rs message read # stream incoming private messages
soulseek-rs room users <ROOM> # who is in a room
soulseek-rs serve [--follow] # stay online sharing, stream uploads
soulseek-rs daemon [--bind ADDR] # run as a service others share
soulseek-rs daemon token|status|stop # control a running one
soulseek-rs whoami # confirm credentials and connection
soulseek-rs user <NAME> # a peer's status and share counts
soulseek-rs shares list|add|remove|status|reindex
@@ -295,17 +342,27 @@ soulseek-rs completions install|uninstall # tab completion for bash/zsh/fish
first: the config commands need no account at all, and `whoami` answers
"are these credentials good and what am I offering" in one call.
Thirteen of these need no credentials, because they never touch the network:
`config path|list|get|set`, `shares list|add|remove`, `portmap`,
`skills install|uninstall|list` and `completions install|uninstall`.
`shares status` and `shares reindex` are not among them: they report what the
network will see, which means logging in and scanning the folders.
With a daemon attached, every command reports the daemon's world. The run
borrows its session, so `--username` and `--password` go unread and `whoami`
names the daemon's account, server and download folder; pass `--no-daemon` when
you want to test this machine's own login. `shares list` stays local either
way, because it reads the config file, while `shares status` and
`shares reindex` ask the session and so report what peers can see.
Every command emits records except the four that perform an action.
`room say`, `message send`, `shares add` and `shares remove` say nothing on
stdout and answer with their exit code alone. `room listen`, `message read`
and `serve` stream records as they arrive, until `--duration` seconds pass or,
with `--follow`, until they are interrupted.
Nineteen of these need no credentials: `config path|list|get|set`,
`shares list|add|remove`, `wish add|remove|list`, `portmap`,
`skills install|uninstall|list`, `completions install|uninstall` and
`daemon token|status|stop`. Most never touch the network at all;
`daemon status` and `daemon stop` talk to a daemon that is already logged in,
so a machine with no Soulseek account of its own can still ask whether one is
running. `shares status` and `shares reindex` are not among them: they report
what the network will see, which means logging in and scanning the folders.
Every command emits records except the six that perform an action.
`room say`, `message send`, `shares add`, `shares remove`, `wish remove` and
`daemon stop` say nothing on stdout and answer with their exit code alone.
`room listen`, `message read` and `serve` stream records as they arrive, until
`--duration` seconds pass or, with `--follow`, until they are interrupted.
#### Record shapes
@@ -322,6 +379,8 @@ room users user
room listen type room user message
message read timestamp user message
serve status user sent/size path
daemon status user server clients uptime-secs
daemon token token
user user status average-speed shared-files
whoami user server shared-folders shared-files
shares list ok|missing directory
@@ -331,18 +390,21 @@ portmap ok|failed backend external
```
`room listen`'s `type` is `message`, `join` or `leave`; a join or leave leaves
the message field empty. `serve`'s `status` is `uploading`, `completed`,
`cancelled` or `failed`. `user`'s `status` is `online`, `away` or `offline`,
and any field the server did not answer prints as `-`.
the message field empty. `serve`'s `status` is `queued`, `uploading`,
`completed`, `cancelled` or `failed`. `user`'s `status` is `online`, `away` or
`offline`, and any field the server did not answer prints as `-`.
`--json` carries more than text does: `search` adds `duration`, `slots`,
`speed` and `free_slot`; `browse` adds `directory`; `download` adds the `user`,
remote `path` and `size` alongside the local `file`; `whoami` adds `listening`,
`listen_port` and `download_dir`; `user` adds `privileged` and
`shared_folders`; `shares status` adds the `directories` array; `serve` adds
`bytes_sent`, `size`, `speed` and a `reason` on failure; `portmap` adds
`port`. Fields the server never answered are `null` rather than absent, so a
missing reply cannot be misread as a zero.
`listen_port`, `download_dir` and `privilege_seconds`; `user` adds `privileged`
and `shared_folders`; `shares status` adds the `directories` array; `serve`
adds `bytes_sent`, `size`, `speed` and a `reason` carrying the queue place or
the failure; `daemon status` adds `version`, `protocol`, `listening`,
`listen_port`, `shared_folders`, `shared_files`, `download_dir` and
`session_loss` (`displaced`, `disconnected`, or `null` while the session
holds); `portmap` adds `port`. Fields the server never answered are `null`
rather than absent, so a missing reply cannot be misread as a zero.
`download --stdin` reads back either shape it emits: a JSON object per line,
or tab-separated text whose first field is the user and last field is the
@@ -455,8 +517,9 @@ to act on one shell rather than the ones detected.
`serve` is the mode that makes this client a source: it stays logged in, keeps
the listener and the share index alive, answers searches and browse requests
from the network, and prints one record per upload as it changes state
(`uploading`, `completed`, `cancelled`, `failed`). It ends after `--duration`
seconds (an hour by default), or runs until interrupted with `--follow`.
(`queued`, `uploading`, `completed`, `cancelled`, `failed`). It ends after
`--duration` seconds (an hour by default), or runs until interrupted with
`--follow`.
```bash
soulseek-rs shares add ~/Music # remembered in config.toml
@@ -470,6 +533,11 @@ short-lived with nothing to serve, so there is no `uploads list`. Managing
transfers across commands (pause, resume, a queue that outlives one
invocation) is what [daemon mode](#daemon-mode) is for.
With a daemon running, `serve` starts no second server. It attaches and streams
the daemon's uploads, `--upload-slots` re-sizes the daemon's queue, and the
local "nothing to share" and "needs the listener" checks are skipped, because
the daemon's shares and listener are the ones peers reach.
Downloads run under a deadline (`--timeout`, 300s by default) and
`--max-concurrent-downloads` at a time, so an unattended run always ends.
@@ -496,6 +564,9 @@ file, in that order of precedence. Flags work before or after the subcommand.
| `--no-listener` / `--listener` | `SOULSEEK_NO_LISTENER` | `disable_listener` | listener on |
| `--max-concurrent-downloads` | `SOULSEEK_MAX_CONCURRENT_DOWNLOADS` | `max_concurrent_downloads` | `20` |
| `--search-timeout` | `SOULSEEK_SEARCH_TIMEOUT` | `search_timeout` | `10` |
| `--daemon ADDR` | `SOULSEEK_DAEMON` | `daemon` | local socket if one is up |
| `--daemon-token` | `SOULSEEK_DAEMON_TOKEN` | `daemon_token` | unset (TCP only) |
| `--no-daemon` | — | — | attach if one is running |
| `--log-file` | `SOULSEEK_LOG_FILE` | not a config key | stderr |
Boolean environment variables accept the usual `1`/`0`/`true`/`false`/`yes`/`no`.
@@ -507,10 +578,11 @@ config and state directories wholesale. Every variable above is also read from
a `.env` file in the working directory, which is often the tidiest way to hand
a container its credentials.
`config get` and `config set` cover the nine settings the file can hold
`config get` and `config set` cover the eleven settings the file can hold
(`username`, `server`, `listener_port`, `disable_listener`, `download_dir`,
`shared_dirs`, `max_concurrent_downloads`, `search_timeout`, `password_cmd`),
and name the lot back at you when you ask for something else. Setting a key to
`shared_dirs`, `max_concurrent_downloads`, `search_timeout`, `password_cmd`,
`daemon`, `daemon_token`), and name the lot back at you when you ask for
something else. Setting a key to
an empty string clears it, and `shared_dirs` takes a comma-separated list.
Every wait expressed in seconds (`--search-timeout`, each `--timeout`, each
`--duration`) is bounded to one day, so a mistyped flag is rejected rather than
+3 -2
View File
@@ -36,8 +36,9 @@ const ALTERNATIVES: readonly Alternative[] = [
question: 'Want a queue that outlives the command?',
answer: (
<>
Every command runs one-shot and exits: JSON on stdout, an exit code, nothing left
running. No resident queue to pause, resume, or drive remotely. slskd has that.
<Code>soulseek-rs daemon</Code> holds one: pause, resume and remove act on it from
the TUI or over JSON-RPC, and <Code>--bind</Code> lets another machine drive it.
slskd still has the web interface and accounts for several people.
</>
),
links: [{ label: 'slskd', href: LINKS.slskd }],
+63 -12
View File
@@ -89,7 +89,9 @@ const FirstRun = () => (
<Prose>
<p>
On first run you get a login screen: sign in with an existing Soulseek account or
register a new one. Your password goes to the OS keychain, not a plain-text file.
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.
</p>
<p>
Downloads land in a conventional folder, and TUI state (searches, downloads,
@@ -158,6 +160,8 @@ const FromTheShell = () => (
{ t: 'cm', text: '# search, pick the best, download it' },
{ t: 'cmd', text: 'soulseek-rs search "netlabel 2004" --min-bitrate 320' },
{ t: 'cmd', text: 'soulseek-rs browse <username> --json' },
{ t: 'cmd', text: 'soulseek-rs wish add "rare pressing"' },
{ t: 'cm', text: '# a standing search, re-run by serve --wishlist' },
{ t: 'cmd', text: 'soulseek-rs room list' },
{ t: 'cm', text: '# public rooms, busiest first' },
{ t: 'cmd', text: 'soulseek-rs room say <room> "hello room"' },
@@ -171,16 +175,17 @@ const FromTheShell = () => (
<Prose>
<p>
The command groups: <Code>search</Code>, <Code>get</Code>, <Code>download</Code>,{' '}
<Code>browse</Code>, <Code>room</Code>, <Code>message</Code>, <Code>serve</Code>,{' '}
<Code>user</Code>, <Code>whoami</Code>, <Code>shares</Code>, <Code>daemon</Code>,{' '}
<Code>config</Code>, <Code>portmap</Code>, <Code>skills</Code>,{' '}
<Code>completions</Code>. Records are
tab-separated fields, one per line, or NDJSON with <Code>--json</Code>.
<Code>browse</Code>, <Code>wish</Code>, <Code>room</Code>, <Code>message</Code>,{' '}
<Code>serve</Code>, <Code>user</Code>, <Code>whoami</Code>, <Code>shares</Code>,{' '}
<Code>daemon</Code>, <Code>config</Code>, <Code>portmap</Code>,{' '}
<Code>skills</Code>, <Code>completions</Code>. Records are tab-separated fields,
one per line, or NDJSON with <Code>--json</Code>.
</p>
<p>
No TTY required. <Code>search --json</Code> pipes through <Code>jq</Code> into{' '}
<Code>download --stdin</Code>. Every ending has its own exit code: 4 found
nothing, 3 could not reach the server, 6 the transfer died.
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.
</p>
<p>
<Code>completions install</Code> gives bash, zsh and fish tab completion for all
@@ -238,6 +243,8 @@ const Daemon = () => (
{ t: 'cm', text: '# once; it holds the session' },
{ t: 'cmd', text: 'soulseek-rs search "netlabel 2004" --json' },
{ t: 'cm', text: '# no login, no flags; it finds the daemon' },
{ t: 'cmd', text: 'soulseek-rs search "netlabel 2004" --no-daemon' },
{ t: 'cm', text: "# opt out; open this run's own session" },
{ t: 'cmd', text: 'soulseek-rs' },
{ t: 'cm', text: '# the TUI attaches to the same session' },
{ t: 'cmd', text: 'soulseek-rs daemon status' },
@@ -248,13 +255,29 @@ const Daemon = () => (
<p>
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.
running, everything behaves exactly as it did before, and{' '}
<Code>--no-daemon</Code> gets that back for a single command.{' '}
<Code>daemon status</Code> 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 <Code>--bind 127.0.0.1:5030</Code> and point commands at it with{' '}
<Code>config set daemon</Code> and <Code>config set daemon_token</Code>.
</p>
<p>
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. The status bar names the daemon, and closing a window leaves its downloads
going, because they belong to the daemon rather than the window.
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&rsquo;s download folder and share list, so the
paths you type there name its filesystem rather than this machine&rsquo;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.
</p>
<p>
Attach one and commands report its world: <Code>whoami</Code> names its account
and server, <Code>download</Code> prints the path on its disk, and{' '}
<Code>serve --follow</Code> watches its uploads instead of starting a second
server. It holds the session rather than the wishlist: standing searches re-run
under <Code>serve --wishlist</Code>, or when you call <Code>wish run</Code>.
</p>
<p>
Why bother: the server permits a single session per account. Two one-off
@@ -305,6 +328,12 @@ const DownloadBox = () => (
tomorrow, run <Code>daemon status</Code>, and pick up where you left off. The
files are on the box, which is where you wanted them.
</p>
<p>
The two <Code>config set</Code> lines are the one-time version;{' '}
<Code>--daemon ADDR</Code> and <Code>SOULSEEK_DAEMON</Code> point a single
command at a different box. On the box itself,{' '}
<Code>daemon --upload-slots N</Code> sets how many uploads it serves at once.
</p>
<p>
There is no encryption on that port. On a home network that is usually fine.
Over the internet, reach it through SSH instead:{' '}
@@ -312,6 +341,18 @@ const DownloadBox = () => (
</p>
</Prose>
</Cols>
<div className="mt-6">
<Callout tone="warn" title="pushed settings are live only">
<p>
<Code>config set download_dir</Code> and <Code>shares add</Code> reach the
running daemon and take effect at once, but the box reads its own{' '}
<Code>config.toml</Code> when it restarts, so make the lasting changes there.{' '}
<Code>shares add</Code> 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&rsquo;s settings form, which sends paths through as typed.
</p>
</Callout>
</div>
</Section>
)
@@ -327,7 +368,12 @@ const OpenSchema = () => (
It speaks newline-delimited JSON-RPC 2.0, described by an{' '}
<ExtLink href="https://open-rpc.org">OpenRPC</ExtLink> document. The daemon
serves that document itself through <Code>rpc.discover</Code>, so a client can
ask a running daemon what it can do.
ask a running daemon what it can do. The same document is checked in at{' '}
<ExtLink href={`${LINKS.gh}/blob/master/docs/openrpc.json`}>
docs/openrpc.json
</ExtLink>{' '}
on GitHub. <Code>auth</Code> answers with the protocol version, 1 today, so a
client written against an older shape learns it at connect time.
</p>
<p>
OpenRPC is the JSON-RPC counterpart to OpenAPI: point a generator at it and you
@@ -338,7 +384,12 @@ const OpenSchema = () => (
<p>
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.
and live updates are pushed to every connected client rather than polled for:{' '}
<Code>event.room</Code>, <Code>event.message</Code>, <Code>event.upload</Code>,{' '}
<Code>event.download_status</Code>, <Code>event.browse</Code> and{' '}
<Code>event.session_loss</Code>. One rule to write against: the download folder
belongs to the daemon, so <Code>download.set_dir</Code> moves it for every
transfer and a per-download destination is ignored.
</p>
</Prose>
<Terminal
+4 -2
View File
@@ -232,8 +232,10 @@ const About = () => (
<Callout title="agents and automation">
<p>
Every command runs as a one-off from the shell. Point cron, a script, or an
agent at them and read JSON records back off stdout. No daemon, no API to stand
up. <Code>skills install</Code> teaches the agent the surface for you.{' '}
agent at them and read JSON records back off stdout. Nothing to stand up first;{' '}
<Code>soulseek-rs daemon</Code> is one command when a batch of them should share
a single login, and the agent skill starts one for you.{' '}
<Code>skills install</Code> teaches the agent the surface.{' '}
<Link to="/docs" className="text-link hover:text-link-hover">
See the commands
</Link>
+31 -5
View File
@@ -125,7 +125,7 @@ const PLATFORMS: readonly PlatformNote[] = [
config: '%APPDATA%\\soulseek-rs\\config\\config.toml',
state: '%APPDATA%\\soulseek-rs\\data\\state',
downloads: '%USERPROFILE%\\Downloads\\Soulseek',
note: 'Uses the standard known-folder locations for roaming app data.',
note: 'Uses the standard known-folder locations for roaming app data. Without Unix sockets, a daemon here needs --bind 127.0.0.1:5030, and clients reach it with config set daemon plus config set daemon_token; that token file carries no permissions of its own.',
},
]
@@ -207,6 +207,26 @@ const SETTINGS: readonly Setting[] = [
</>
),
},
{
key: 'daemon',
fallback: 'the local socket, if one is running',
body: (
<>
A daemon to drive instead of logging in here, as <code>host:port</code> or a Unix
socket path.
</>
),
},
{
key: 'daemon_token',
fallback: 'unset',
body: (
<>
The secret a remote daemon asks for, printed by <code>daemon token</code> on the
machine running it. A local socket needs none.
</>
),
},
]
const CONFIG_EXAMPLE: TermLine[] = [
@@ -431,14 +451,17 @@ const Platforms = () => (
<code>config.toml</code> is layered: flags beat environment variables, which
beat the file, which beats the defaults. State (searches, downloads, rooms) is
versioned JSON and restores on restart; soulseek-rs sets anything unreadable
aside as <code>.bak</code> rather than overwriting it.
aside as <code>.bak</code> rather than overwriting it. Attached to a daemon, that
state is the daemon&rsquo;s, and the state folder also holds its{' '}
<code>daemon.sock</code> and <code>daemon.token</code>, both 0600 on macOS and
Linux.
</FeatureCard>
</Cols>
</Section>
)
const Config = () => (
<Section id="config">
<SectionHead eyebrow="config.toml" title="Nine settings, all optional.">
<SectionHead eyebrow="config.toml" title="Eleven settings, all optional.">
Every one has a working default, so an empty file, or no file at all, is valid.{' '}
<Code>config list</Code> prints the effective value of each.
</SectionHead>
@@ -471,7 +494,9 @@ const Config = () => (
reads one setting and <Code>config set</Code> writes one. It rejects
unknown keys, so a typo fails instead of doing nothing.{' '}
<Code>--no-config</Code> ignores the file entirely and{' '}
<Code>--config FILE</Code> points at another one.
<Code>--config FILE</Code> points at another one. <Code>wishlist</Code> is in
the file too, written by <Code>wish add</Code> rather than{' '}
<Code>config set</Code>, since a standing search may itself contain a comma.
</p>
</Callout>
</div>
@@ -497,7 +522,8 @@ const Uninstall = () => (
{ t: 'cmd', text: 'cargo uninstall soulseek-rs' },
{ t: 'cm', text: '# brew only: drop the tap it came from' },
{ t: 'cmd', text: 'brew untap michel/tap' },
{ t: 'cm', text: '# config and state: searches, downloads, rooms, messages' },
{ t: 'cm', text: '# config and state: searches, downloads, rooms,' },
{ t: 'cm', text: '# messages, the daemon token' },
{
t: 'cmd',
text: 'rm -rf ~/.config/soulseek-rs ~/.local/share/soulseek-rs',