Commit Graph
75 Commits
Author SHA1 Message Date
Michel de Graaf b6b4ec04c1 docs: install by system, stable and nightly 2026-09-21 12:58:09 +02:00
Michel de Graaf 1b898adeeb docs: stop pinning a library version the instructions cannot keep current
Four places told people to write `soulseek-rs-lib = "8"` (RELEASE.md said "6"),
and every one of them had been left behind by ten majors or more. The failure is
silent rather than loud: 6.x and 8.0.0 are both real published versions, so cargo
resolves the snippet happily and hands over a library from August instead of
refusing it, and one of the four is soulseek-rs-lib's own README, which is the
page crates.io renders.

`cargo add` writes whatever version is current at the time it runs, so there is
no longer a number in the docs to go stale. The manifest pin in
soulseek-rs/Cargo.toml stays as it is — release-plz owns that one and keeps it
in step with the release.
2026-09-10 12:08:00 +02:00
Michel de Graaf d6d870b39f feat(tui): account settings — stats, password change, and logout
The settings popup opens with an Account block: who the session is logged in
as, how much it shares, the port peers reach it on, and the daemon it borrows
when the window is attached to one. Two rows follow, both confirmed before
they act. A password is typed twice, because the server sends no verdict on a
change and a typo entered once would leave the account with a password nobody
knows. Logging out closes the window and comes back to the login screen with
the stored password deleted, so the next start asks rather than walking
straight back in. A window attached to a daemon offers no logout: the login is
the daemon's.

"Change it on the server, then store it where the login lives" is one function
over the session and the secret store, so the window, the command and the
daemon share the rule rather than each spelling out its own idea of which
process owns the password.

Headless the same two acts are `account password` and `account logout`, and
the daemon answers `account.password` so a routed run is not a dead end —
`whoami` already reported who is logged in and what they share.
2026-09-10 11:10:58 +02:00
Michel de Graaf cd580e5173 docs: lead with the protocol coverage, and drop a stale upload assertion
The message-set coverage is the crate's strongest claim, so say it where
people look first: the README features list and About, the website home
and community pages, each pointing at the coverage table. The table now
carries a section naming what the message set does not reach, so the
claim has something behind it rather than standing on its own.

The upload streaming test still asserted the uploader was blocked
waiting for the downloader's close — the wait that came out when it
started costing the upload slot. It passed only while the thread had yet
to be scheduled, and failed whenever it had.
2026-09-09 19:45:06 +00:00
Michel de Graaf bf3e3ec4ad docs(readme): document the accept_children setting 2026-09-09 16:48:58 +00:00
Michel de Graaf a3105fb060 fix(docs): use deploy-independent nightly installer 2026-09-08 14:34:10 +02:00
Michel de Graaf 108c897fdf Merge pull request #92 from michel/nightly
feat(ci): publish installable nightly builds
2026-09-08 14:07:35 +02:00
Michel de Graaf 99205026f8 feat(ci): publish installable nightly builds 2026-09-08 13:40:50 +02:00
Michel de Graaf 3ef78c6de1 refactor(tests): share one soulfind fixture across the e2e suites
The TUI suite had grown its own copy of the soulfind spawner the protocol
suite already carried, and its own copy of the CLI suite's in-process
client and settle helpers. All four suites now spawn, log in, settle and
skip through `tests/common`: the CLI suite's own server goes too, and
the fixture checks its child is still alive before calling the port
answered, so a lost port cannot pass another server off as ours.
2026-09-08 13:12:12 +02:00
Michel de Graaf 76f4854169 feat(tui): run the highlighted search again with S
The search list survives a restart, but a restored query was only ever a
reminder: seeing what the network has now meant typing it out again. In
the Searches pane, `S` now re-runs the highlighted query in its place — no
duplicate row, no reordering — cancelling a run still collecting first and
starting the Results display over for what comes back.

The key is listed in the shortcut bar, the `?` overlay, the README and the
website. `start_search` is split into launching a search and showing it,
so both paths share one implementation.

A new `tui_e2e` suite drives the real window over a real session against
soulfind: keys in, a screen out, a sharing peer on the other end. It
restores a query from a previous run's state directory, presses `S`, and
checks the peer's file lands on screen — then again once the query has
been on the wire, proving a repeat is answered and the display does not
stack. To make that possible `MainTui` is exported with `handle_key_event`,
`render`, `poll_session` and a `state()` accessor, and CI runs the suite
with `SOULSEEK_E2E_REQUIRED=1`.

Closes #85
2026-09-08 13:11:40 +02:00
Michel de Graaf af1f9241e4 Merge pull request #91 from michel/tui/results-folder-column
feat(tui): folder column, paged long lists, and filters in every popup
2026-09-08 12:32:08 +02:00
Michel de Graaf 607532cc44 feat(tui): search a room's log and members, and a conversation
A busy room's log scrolls past and its member list runs to a thousand
names, with nothing to narrow either. In a room / now filters the log
to lines mentioning the text and u filters the member list, the same
way the room list and the results filter: type, Enter keeps it, Esc
clears it before it leaves the room. The inbox takes / over the open
conversation. The member keys, b and m among them, act on the narrowed
list, and the titles say what is filtered and how many made it.
2026-09-08 11:24:53 +02:00
Michel de Graaf b8da8668b4 feat(tui): folder jumps, open-all and a path filter in the browse popup
A browsed share only moved a row at a time, so a folder of a few
hundred files stood between the user and the next folder. J and K now
jump between folder rows, H and L close or open every folder, and /
filters the whole tree by path the way the results and the room list
filter: type to narrow, Enter keeps it, Esc clears it, then hides. A
folder under a filter shows the paths that match and d downloads just
those. The keys popup gains a browse section and the bar lists them.
2026-09-08 11:24:41 +02:00
Michel de Graaf 1fcde0a78b feat(tui): W puts the pane layout back
Hiding panes with w and zooming with z had no way back short of
pressing each hidden pane's number and Esc. W shows every pane and
leaves zoom in one go, and the shortcut bar offers it only while there
is a layout to reset.
2026-09-08 11:24:31 +02:00
Michel de Graaf b0abb90f4f Enhance README with installation instructions and links
Added quick install instructions and updated project description with a link to the website.
2026-09-08 11:02:41 +02:00
Michel de Graaf 2733ee26f7 refactor(tui): trim the folder column change to what it needs
The path splitter is a one-line stdlib call with a single caller, so it
lives beside that caller as a private fn, without its own tests. The pane
tests assert the two columns in one screen fragment each, the end-offset
test keeps only the two cases the existing name tests do not cover, and
the key-handling test that repeated one of them goes. The website mock
splits on the backslash the same way its Info pane already does.
2026-09-08 05:59:39 +00:00
Michel de Graaf da371d4176 feat(tui): show a result's file name and folder in columns of their own
The results list held the whole shared path in one column, so a search
was read through its folder: every row began with the same peer prefix
and the file name, the part being searched for, sat off the right edge.
Split the column at the last separator, the file name first with the
larger share of the width and the folder dimmed beside it, so names line
up and read at a glance.

The one scroll offset moves both columns together, and $ runs to the end
of whichever overflows further. The website's mock of the pane follows.

Suggested in #55.
2026-09-08 05:59:38 +00:00
Michel de Graaf f6423a2ed6 feat(lib): carry audio attributes in browse and folder listings
A browse listed each file as a name and a size, so a searcher who saw a
bitrate in a search reply lost it the moment they opened the sharer's
files. Listings (codes 5 and 37) now carry each file's attributes the
way Nicotine+ sends them, as a SharedFileEntry with name, size and
attributes in place of the (name, size) pair; the CLI's browse record
gains bitrate and duration, and the daemon protocol moves to version 2
for the new listing shape.
2026-09-08 01:44:49 +02:00
Michel de Graaf 0b9fdaf58e Update README description for clarity and detail
(cherry picked from commit 6d47b3e594)
2026-09-07 17:17:57 +02:00
Michel de Graaf fd7bab3050 docs: add a contributing guide and link it from the README and site
Nothing in the repo said that pull requests go to develop rather than master,
or that release-plz reads the commit subject to pick the next version. Both
cost a first-time contributor a round trip.

CONTRIBUTING.md covers those, the three checks CI runs, and the fact that the
e2e suites skip without a soulfind server. The README and the community page
link to it, and it joins the footer's Project column.
2026-09-07 17:17:57 +02:00
Michel de Graaf 478d976776 feat(tui): scroll long names in every list and page through popups and chat
The sideways keys the results had (h/l, arrows, 0 and $) now scroll a
transfer's file name and a query too, each list on its own. The browse tree
and the room list take Home/End, PgUp/PgDn and the vim jumps like the main
lists, and a chat log, room or private, pages back through its history and
holds its place while new messages arrive; what arrives meanwhile counts as
unread until the reader returns to the end.
2026-09-07 08:16:20 +00:00
Michel de Graaf da8ab45303 feat(tui): results across the top, with panes you can hide, zoom and Tab through
The results pane now has the whole width, where a long file name needs it,
and Searches, Downloads and Info share the row underneath. Tab and Shift-Tab
walk the panes, z zooms the focused one to the window, w hides it so the
others widen, and its number brings it back. Every list takes the same
navigation keys, the shortcuts bar wraps instead of cutting keys off, and ?
lists every key by pane, scrolling on a short terminal.

Follows up on #55.
2026-09-07 07:49:32 +00:00
Michel de Graaf aa61a1a434 feat: cancel a download from the TUI, the CLI, and the daemon
Closes #54.

A download can now be cancelled whether it is queued, in progress, or
paused. Cancelled is a terminal status: the store refuses to overwrite
it, the status channel relays nothing after it, the partial file is
deleted, and the transfer thread releases its socket at its next
one-second poll instead of the 30s read deadline. A queued download that
is cancelled declines the peer's later offer with TransferResponse
"Cancelled", and an inbound file connection for it is refused. A fresh
download of the same file evicts the dead row so it is not shadowed,
and a cancelled transfer is not re-queued after a restart.

Surfaces: Client::cancel_download, download.cancel over RPC, `transfer
cancel <user> <path>` on the CLI (which also stops an upload by that
name), and x in the transfers pane, where d dismisses the row.
2026-09-06 21:40:45 +02:00
Michel de Graaf 1611b4baf3 docs: list projects built on soulseek-rs-lib
Add a 'Projects using soulseek-rs-lib' section to the README and a
matching section on the website community page, starting with seakarr.
Both point new projects at issue #12 to reserve a client minor version.
2026-08-17 09:46:06 +02:00
Michel de Graaf d11326f149 fix(daemon): ship the OpenRPC document inside the crate
docs/openrpc.json lived at the repository root, outside the soulseek-rs
package, so the include_str! in proto compiled from a checkout but not
from the published tarball - cargo publish failed verification and the
v13.0.0 release stopped after publishing the lib. The document moves
into the crate's own docs/, where packaging can see it; the generator
test and every link follow.
2026-07-29 17:34:29 +02:00
Michel de Graaf d2d1558708 docs: cut the README's padding, and tell the skill whose account it is
The feature list said the same things at twice the length, and Project
Goals / Project Structure were two headings for one paragraph.

SKILL.md argued against throwaway accounts on cost grounds. The reason
that holds is standing: the agent arrives under the user's name, peers
judge that name, and inventing a second one throws it away. Exit 7 now
points at starting a daemon rather than at --username.
2026-07-29 11:24:24 +02:00
Michel de Graaf ab5cef8cfe 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.
2026-07-29 09:22:02 +02:00
Michel de Graaf cd56fb3b39 feat(daemon): make the download folder a live setting in both modes
The full-surface audit found the settings story half-true: `config set
download_dir` and the TUI's settings form edited a file a running daemon
never rereads, while the attached TUI displayed the laptop's path and
discarded the edit entirely.

- `download.set_dir`: a session-wide move, published in the contract;
  the per-transfer destination stays refused. `daemon.status` reports
  it and the next transfer lands there.
- `config set download_dir` and `config set shared_dirs` push to a
  live daemon the way `shares add` does, through one shared helper;
  the socket probe is paid only by the keys a daemon owns.
- The attached TUI seeds its settings form from the daemon's status,
  pushes both folders over the socket as typed, reports a partial
  refusal as exactly that, and creates no client-side directories. It
  reads the daemon token from the resolved config — with the token only
  in config.toml it could not attach over TCP — and settings
  persistence honours --config instead of hard-coding the default path.
- `search.wishlist` records a start time, so an attached window
  renders it as running rather than instantly finished.

The rest is the coverage the audit demanded: CLI-level token auth
(wrong, missing, and valid over TCP), `daemon status|stop` with
nothing running, --sort, --limit, --pick first, --password-cmd,
--log-file, --upload-slots, the config-named download folder in both
modes, and headless settings-form tests attached and local. Every
guard was checked by disabling what it protects. Both e2e modes run
green: 94 CLI tests each way, 16 protocol, 285 unit, 34 lib.
2026-07-28 10:44:06 +02:00
Michel de Graaf 011eafaa7f feat(daemon): make every window a view of one shared session
Two TUIs on one daemon behaved as two separate windows that happened to share
a login: each showed only the transfers it had queued and the searches it had
run, and neither said it was attached to anything.

The status bar now names the daemon, and both lists are views of the session:
transfers and searches appear in every window, dismissing one dismisses it
everywhere, and a window opened later starts from what is already running
rather than blank. message read likewise starts from the conversation the
daemon collected instead of only what arrives while it runs.

Search state needed the daemon to say when a query went out. The window a
search collects for belongs to whichever client started it, so without that
every other window showed a running search as finished the moment it appeared
— which is what it did. The summary carries a file count too, so a window has
the right number before it has pulled the results across.

Adds Client::forget_search, which the library had no way to do: a resident
client otherwise holds every result set it has ever seen, and a window that
dismissed a search had no way to say so.

A review of the first cut found five defects, all fixed here and all covered:
syncing ran locally too, where it cost a full copy of every result set per
frame and deleted the completed-download history restored from disk; a
transfer could be listed twice by racing its own registration; clearing all
searches did not reach the session, so every row came back; a search dismissed
elsewhere never left; and a wishlist search reported as finished the instant
it went out.
2026-07-27 20:58:49 +02:00
Michel de Graaf 01152d91f6 feat(daemon): say what to do next on startup, and prove multiple clients agree
Starting the daemon printed 'ready' and nothing else, so the token and the
next step were both somewhere else. It now prints what other commands can do
from here, and when --bind is given, the token itself with the two config
commands a remote client needs. All on stderr, so stdout stays empty and
daemon still composes in a pipeline.

Adds the guarantee daemon mode is for as a test: eight clients connected at
once are each answered correctly, none has its replies crossed with another's,
and the daemon still counts all eight. Plus 250 connect-and-drop cycles, well
past the connection cap, to catch a per-connection resource that is not
released — the shape a daemon that works for a while and then refuses
everybody would take.

A connection the daemon drops before it can be served now logs why at warning
level. A client only ever sees 'the connection closed', so that line is the
only account of the reason.
2026-07-27 14:39:31 +02:00
Michel de Graaf 2a906df1b5 feat(config): remember which daemon to use, and document the download box
A remote daemon could only be named with --daemon and --daemon-token on every
invocation, which made the one workflow daemon mode is most useful for — a
headless download box driven from a laptop — a matter of retyping a 64-character
token. Both are now config keys, so it is two 'config set' commands once and
plain commands thereafter.

Fixes a bug that setup exposed: the up-front daemon status probe read the token
from the command line only, so with the token in the config file it failed
silently and every command that names a path or a server fell back to this
machine's defaults — whoami claiming downloads land in the laptop's home
directory when they land on the box. The regression test was checked by
reintroducing the bug.

Documents the download-box workflow in plain terms across the README, the
website and the agent skill, and says plainly that the schema is open: OpenRPC,
served by the daemon itself, so an Android, iOS or web remote is something
anyone can build without asking.
2026-07-27 14:11:51 +02:00
Michel de Graaf 3f914c4425 docs: put daemon mode on the website and tighten the README
The website made three claims daemon mode falsifies — "No TTY, no daemon",
"no daemon, no MCP server, no API key", and "no GUI and no daemon mode yet"
under what-this-is-not. All three corrected.

Adds a docs section and a home feature card, both leading with the reason
rather than the mechanics: the server allows one session per account, so
parallel one-shot commands either displace each other or mean a throwaway
username per run, which is account churn on infrastructure other people
maintain. A daemon is one login for as long as you work.

The README section said that three times over and buried the reasoning below
the mechanics; rewritten to state it once, up front. It now also covers which
commands stay local and that shares add reaches a running daemon, which was
meant to land earlier and silently did not — the edit anchored on text that
only existed in SKILL.md.
2026-07-27 14:04:14 +02:00
Michel de Graaf fe56a070e1 docs(skill): make the daemon the default way an agent works
The skill buried daemon mode two-thirds down as a scaling trick, and still
offered --username "$SOULSEEK_USERNAME-$RANDOM" as the way to run commands in
parallel. That is worse than underselling it: the server allows one session
per account, so a per-run name means registering a throwaway account on a
public server for every search — account churn on infrastructure other people
maintain and share.

Starting a daemon is now the first instruction in the file, with the reason
stated, and the random-username idiom is named as the anti-pattern rather than
offered as an alternative. Exit 7's advice changes accordingly: it means
something else logged in as you, and the fix is a daemon, not a new name.

Three agents given only this file and a parallel-work task all started a
daemon and none invented a username.

The startup snippet also had a shell precedence bug: in
`daemon status || daemon &`, the & backgrounds the whole list, so it returned
instantly and the next command raced the login it was meant to use. Replaced
with a bounded wait, checked against a live server.
2026-07-27 14:01:17 +02:00
Michel de Graaf 834fbcd35b docs: publish the daemon protocol and describe daemon mode in the README
The OpenRPC document is the machine-readable contract; daemon-protocol.md
covers what a schema cannot say — framing, the auth handshake, and that
events are pushed, unreplayable, and will disconnect a client that stops
reading.
2026-07-27 10:46:05 +02:00
Michel de Graaf 7435d48b32 perf: raise transfer defaults and stop uploads sleeping on their slot
The slot cap merged from master defaults to 2 uploads, and the CLI capped
downloads at 5. Those are the numbers you pick when your own bandwidth is the
scarce resource, but a Soulseek transfer is paced by the peer on the other end
— usually a few hundred KiB/s — so what fills a modern link is many transfers
at once, not faster ones. Upload slots go to 10, downloads to 20. The queue
itself stays: privileged users being able to jump it is a condition of being a
tolerated third-party client.

serve_file also slept half a second after the last byte so the downloader could
drain before the socket closed. With a cap that is time no other peer can use,
and 64 uploads over 10 slots paid it six times over. A half-close says "that is
the whole file" and TCP still delivers everything written before the FIN.

Measured with 64 waiting peers: 2 slots 24.2s, 8 slots 6.2s, 32 slots 3.1s; at
10 slots the linger alone was 5.4s to 3.05s.
2026-07-26 18:25:50 +02:00
Michel de Graaf e8a48c6b06 fix!: make concurrent invocations work instead of silently seeing nothing (#22)
Running several one-shot commands at once returned "no results" for queries that
had plenty. Four defects, each of which made a dead session look like an empty
network:

- The peer listener bound its port inside a detached thread and panicked on
  EADDRINUSE, unwinding that thread alone. The process carried on with no
  listener, so no peer could deliver a search response. `Listen::bind` now takes
  the socket before login and falls back to an ephemeral port when the
  configured one is held.
- SetWaitPort advertised the configured port whether or not it was bound, so
  peers dialled a sibling process that discarded responses to tickets it never
  issued. The port the listener really holds is what gets advertised, mapped
  through UPnP, and reported by `whoami` and `serve`.
- A zero-byte read is EOF on a TCP socket, but MessageReader reported it as
  success, so a server that closed cleanly left the actor polling a dead socket.
  It is now an error, and Relogged (code 41) has a handler. The peer actor
  classifies that error: a control connection that was established and then
  closed is a clean disconnect, because peers drop idle sockets while the bytes
  move over their own connection, and reporting an error there fails every
  download queued with that peer including the one streaming. A connection that
  never established keeps the error path, which is what the server-brokered
  fallback reads to know a peer is unreachable.
- A session the server cut off - usually because another process logged in under
  the same name - exited 4, documented as "it worked and found nothing". Callers
  concluded the file was unavailable. Sessions now record why they ended and
  those verdicts become exit 7.

The server allows one session per account, so concurrency still needs a distinct
--username per run; SKILL.md documents that. The listener port needs no such
care any more.

Six e2e tests against soulfind cover it, each of which fails without the fix: a
taken listen port, a displaced session, three clients searching at once under
one configured port, the two CLI equivalents, and a peer that hangs up its
control connection while a transfer is still to come.

BREAKING CHANGE: `Listen::start` is replaced by `Listen::bind` and
`Listen::serve`, so the caller owns the socket and can read the port it actually
bound. `Client::listen_port` and `Client::session_loss` are new.
2026-07-26 14:52:31 +02:00
Michel de Graaf 9b87bd6a0f feat!: wishlist, upload-queue privilege recognition, and search filters (#20)
Closes the last two gaps in Soulseek's list of features an alternative client
must implement to be tolerated, and adds the result filters Nicotine+ has had
all along.

Wishlist: server code 103 is sent and code 104's announced interval is kept
rather than logged and discarded, so the re-search timer runs off the schedule
the server set. `wish add|remove|list|run` plus `serve --wishlist`; wishes
persist in config.toml as a TOML array so a comma inside a query survives.
`wish run` prints search-shaped records, so it pipes into `download --stdin`.

Privilege recognition: the code-69 user list is parsed instead of counted and
dropped, and code 92 answers `whoami` as `privilege_seconds`. Uploads now go
through a queue with a slot cap (`--upload-slots`, default 2) that serves
privileged users first — the cap is what makes the ordering mean anything, so
they are one change. `PlaceInQueueRequest` (peer code 51) is answered, where it
previously had no handler at all, and a departed peer's slot is released so a
stalled peer cannot wedge uploads shut.

Search filters: `--exclude`, `--extension`, `--min-size`, `--max-size` beside
the existing `--min-bitrate` and `--free-slots`, in one flattened arg group
shared by `search`, `get` and `wish run`.

Also serialises the library e2e suite behind the same gate `cli_e2e` already
uses: the new queue tests hold peer sockets open on purpose, which starved
concurrently-spawned soulfinds and failed unrelated download tests with
`login: Timeout`.

BREAKING CHANGE: `UploadStatus` gains a `Queued(u32)` variant, so an exhaustive
match on it no longer compiles, and `cli::Commands` gains `Wish`.
2026-07-26 14:20:07 +02:00
Michel de Graaf a72dd64081 feat: resume an interrupted download instead of refetching it (#18)
Downloads buffered the whole file in memory and wrote it out only on
success, so an interrupted transfer left nothing behind and always
started over. The START_DOWNLOAD offset was hardcoded to zero on the
download side and discarded on the upload side, so neither half of the
protocol's resume support was wired up.

Bytes now stream into `<file>.part` as they arrive and it is renamed
only once the file is whole. A new attempt sends the partial file's
length as the START_DOWNLOAD offset, and `serve_file` seeks to the
offset a peer asks for rather than replaying from the start. A `.part`
at or past the expected size is treated as stale and restarted.

Partial files are kept out of the share index, since the download
directory is shared by default.
2026-07-26 12:51:48 +02:00
Michel de Graaf 534b09a20a feat: install tab completion for bash, zsh, and fish (#16)
`completions install` generates the script from the same clap definition
the binary parses with, so it cannot drift from `--help`, and writes it
where each shell already looks. fish scans its own completions directory
unprompted; bash and zsh get one marked `source` line appended to their rc
file, because nothing puts a per-user directory on their search path by
default. `completions uninstall` takes back exactly those lines and leaves
the rest of the file alone.
2026-07-26 12:18:53 +02:00
Michel de Graaf 72f2e31a2c docs: point the library snippets at the 8.x lib
v8.0.0 is on crates.io, so the Cargo.toml examples in the readme and on the
install page were a major behind.

Replace the 5.x upgrade note with one that does not name a version, so it
stops going stale every release.
2026-07-26 11:51:48 +02:00
Michel de Graaf 9869b11f6d docs: cut the padding from the readme
Drop the "This structure allows" bullets justifying a two-crate workspace,
collapse four one-line "To run X:" stubs into a single block, and lose the
adverbs, the emoji heading, and the bolded emphasis.

Project Goals no longer claims a hard no-external-crates rule for the
library, which stopped being the stance; it says lean instead. Also fixes
"Headless mode daemon mode".
2026-07-26 11:45:33 +02:00
Michel de Graaf c074ac3d7b docs: show the agent session in the README agent skills section 2026-07-25 05:14:37 +02:00
Michel de Graaf 307f1713b8 docs: replace the demo gif with an animated SVG of the TUI
Recorded with asciinema against the live network and rendered with
svg-term, so the demo is vector text instead of pixels: 20 KB gzipped
against 400 KB, crisp at any width, and it links through to the site.
2026-07-25 04:15:32 +02:00
Michel de Graaf 06459ddced docs: use the wordmark at the top of the README
The mark is a glyph; the wordmark reads "soulseek-rs" outright, which is what
a README header wants. Both tones ship so prefers-color-scheme can pick one.
Named for the background they sit on rather than their Figma tone, because the
existing logo-ink/logo-light pair means the opposite of what it sounds like.
2026-07-25 03:34:44 +02:00
Michel de Graaf 6b755872ff docs: mention Homebrew where the README lists how users install
The project-structure summary still named cargo as the only route.
2026-07-25 03:22:45 +02:00
Michel de Graaf 9e41456507 docs: follow up the v7 release and the homebrew section
The homebrew install text arrived with an em-dash in it, and both READMEs still
told library users to depend on version 6.
2026-07-25 03:22:04 +02:00
Michel de Graaf 12c8147f71 docs: point the README logo at the website's copy
The logo already lived in website/public; copying it into fixtures meant two
files to keep in step for no gain. The README now references the original.
2026-07-25 03:21:33 +02:00
Michel de Graaf 9c966672af feat: install via homebrew (#9)
Add a release job that regenerates Formula/soulseek-rs.rb in michel/homebrew-tap
from the archives the binaries matrix uploads, and lead with
`brew install michel/tap/soulseek-rs` in the READMEs and on the website.
2026-07-25 03:18:53 +02:00
Michel de Graaf d84360a803 docs: drop the em-dashes from the README
Thirty-eight of them, in three roles that each wanted a different fix: label
separators in lists became colons, table cells standing in for "no value"
became the word they meant (required, unset, not a config key), and the
mid-sentence asides became commas, parentheses, or two sentences.
2026-07-25 03:18:37 +02:00
Michel de Graaf 13c58feafe docs: open the README with the logo and the site's own intro
The logo ships in both variants the site uses, picked by prefers-color-scheme
so it stays legible on either GitHub theme — the names are inverted from what
you would guess, `ink` being the cream one for dark backgrounds. The opening
copy now matches the site rather than describing the project twice in two
voices, and still says the repository holds a library as well as a client.
2026-07-25 02:55:47 +02:00