docs(website): fold the docs page into five parts and drop the byline
The docs page carried three daemon sections in a row, each with its own numbered head, so the one story the daemon tells read as three. They are now sub-heads under 5 · the daemon. The agents section is gone, first run points at /install instead of assuming Homebrew, and the layout section shows the demo next to the pane descriptions rather than describing boxes nobody sees. The footer no longer signs the project with a personal name, and the TUI demo no longer shows a home directory that only exists on one machine. Community picks up the sharing etiquette cards and SeeleSeek; install and home lose a few em-dashes.
This commit is contained in:
@@ -90,8 +90,7 @@ export const Footer = () => (
|
||||
</div>
|
||||
|
||||
<div className="mt-5.5 flex flex-wrap items-center gap-x-4 gap-y-1 border-t border-hairline pt-4 text-[11.5px] text-muted sm:mt-11 sm:gap-x-6 sm:gap-y-2 sm:pt-5.5 sm:text-[12.5px]">
|
||||
<span>MIT · © 2026 Michel de Graaf</span>
|
||||
<span className="text-success">No telemetry. Ever.</span>
|
||||
<span>MIT · © 2026 soulseek-rs contributors</span>
|
||||
<span className="sm:ml-auto">{VERSION}</span>
|
||||
</div>
|
||||
</Wrap>
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface TransferRow {
|
||||
rateMbps?: number
|
||||
}
|
||||
|
||||
export const SHARE = '/Users/micheldegraaf/Downloads/Soulseek'
|
||||
export const SHARE = '~/Downloads/Soulseek'
|
||||
|
||||
export const SEARCHES: readonly SearchRow[] = [
|
||||
{ id: 's1', query: 'public domain field recordings', results: 606, status: 'Done' },
|
||||
|
||||
@@ -12,6 +12,7 @@ export const LINKS = {
|
||||
nicotine: 'https://nicotine-plus.org',
|
||||
slskd: 'https://github.com/slskd/slskd',
|
||||
slsknet: 'https://www.slsknet.org',
|
||||
seeleseek: 'https://seeleseek.net',
|
||||
} as const
|
||||
|
||||
export const VERSION = __APP_VERSION__
|
||||
|
||||
@@ -27,7 +27,7 @@ export const ROUTES = [
|
||||
path: '/community',
|
||||
title: 'soulseek-rs, community',
|
||||
description:
|
||||
'How to help with soulseek-rs, and where to go when another Soulseek client fits you better. MIT licensed. No telemetry.',
|
||||
'Soulseek etiquette: what sharing asks of you, how to help with soulseek-rs, and where to go when another client fits you better.',
|
||||
},
|
||||
] as const satisfies readonly RouteMeta[]
|
||||
|
||||
|
||||
+120
-51
@@ -22,6 +22,16 @@ const ALTERNATIVES: readonly Alternative[] = [
|
||||
{ label: 'Nicotine+', href: LINKS.nicotine },
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'On a Mac and want it to feel like one?',
|
||||
answer: (
|
||||
<>
|
||||
SeeleSeek is native Swift and SwiftUI rather than Qt or Python, so it opens like
|
||||
any other Mac app. macOS 15 or later, MIT licensed.
|
||||
</>
|
||||
),
|
||||
links: [{ label: 'SeeleSeek', href: LINKS.seeleseek }],
|
||||
},
|
||||
{
|
||||
question: 'Want feature completeness?',
|
||||
answer: (
|
||||
@@ -49,22 +59,25 @@ const CITIZENSHIP = [
|
||||
{
|
||||
glyph: '↑',
|
||||
color: 'var(--status-success)',
|
||||
title: 'Share, and share real files',
|
||||
title: 'Share something, anything',
|
||||
body: (
|
||||
<>
|
||||
Point <code>shares add</code> at the collection you listen to. Full albums,
|
||||
honest bitrates, nothing transcoded up from a lossy source and relabelled.
|
||||
honest bitrates, nothing transcoded up from a lossy source and relabelled. Nobody
|
||||
checks whether you reshared the exact file you took, only whether you offer
|
||||
anything at all.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
glyph: '≡',
|
||||
color: 'var(--status-info)',
|
||||
title: 'Organize so others can find it',
|
||||
title: 'Name it so a search can find it',
|
||||
body: (
|
||||
<>
|
||||
Artist and album folders, tagged files, readable names. Someone else’s search
|
||||
only reaches you if your filenames say what the music is.
|
||||
Artist, album, year, track. Most people search rather than browse, so your
|
||||
filenames are the only route to you. One flat folder of untagged rips is
|
||||
invisible.
|
||||
</>
|
||||
),
|
||||
},
|
||||
@@ -74,8 +87,9 @@ const CITIZENSHIP = [
|
||||
title: 'Stay online',
|
||||
body: (
|
||||
<>
|
||||
Leave <code>serve --follow</code> running in a tmux session on the box that is
|
||||
already on. A share that appears for an hour a week is close to no share at all.
|
||||
Leave <code>serve --follow</code> running in tmux on the box that is already on,
|
||||
or hand <code>daemon</code> to systemd. A share that appears an hour a week barely
|
||||
counts.
|
||||
</>
|
||||
),
|
||||
},
|
||||
@@ -93,11 +107,24 @@ const CITIZENSHIP = [
|
||||
{
|
||||
glyph: '@',
|
||||
color: 'var(--status-info)',
|
||||
title: 'Answer people',
|
||||
title: 'Ask before you take a discography',
|
||||
body: (
|
||||
<>
|
||||
Rooms and private messages are part of the client for a reason. Someone asking
|
||||
where the rest of a discography went is worth a reply.
|
||||
<code>user <name></code> shows how someone is set up, and many spell out
|
||||
their limits in their profile. One line from <code>message send</code> beats
|
||||
getting banned mid-queue. Answer the people who write to you, too.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
glyph: '⇅',
|
||||
color: 'var(--status-success)',
|
||||
title: 'Size your slots for your uplink',
|
||||
body: (
|
||||
<>
|
||||
<code>--upload-slots</code> is not a score. Set it past what your uplink can feed
|
||||
and everyone in the queue gets a trickle. Start low, raise it when uploads finish
|
||||
fast.
|
||||
</>
|
||||
),
|
||||
},
|
||||
@@ -107,8 +134,21 @@ const CITIZENSHIP = [
|
||||
title: 'Wait your turn',
|
||||
body: (
|
||||
<>
|
||||
Queues are one person’s upload bandwidth being divided up. Queue it, leave it,
|
||||
come back later. Do not requeue the same file to jump the line.
|
||||
A queue is one person’s upload bandwidth divided up. Queue it, leave it,
|
||||
come back later. Requeuing to jump the line burns a slot someone else was waiting
|
||||
on.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
glyph: '⚑',
|
||||
color: 'var(--status-warning)',
|
||||
title: 'Keep the files reachable',
|
||||
body: (
|
||||
<>
|
||||
Sharing off an external drive works until it is unmounted. Run{' '}
|
||||
<code>shares reindex</code> once it is back, or your listing promises files that
|
||||
error out when somebody asks.
|
||||
</>
|
||||
),
|
||||
},
|
||||
@@ -117,16 +157,71 @@ const CITIZENSHIP = [
|
||||
export const Community = () => {
|
||||
return (
|
||||
<>
|
||||
<PageHead
|
||||
eyebrow="community"
|
||||
title="Help out, or go where you’re better served."
|
||||
>
|
||||
It’s mostly one person who’s been on Soulseek since the early 2000s.
|
||||
Issues and pull requests are welcome, and if another client fits you better, use it.
|
||||
<PageHead eyebrow="community" title="Share what you take.">
|
||||
Soulseek exists because its users leave their collections open to each other. What
|
||||
that asks of you, how to help with the client, and where to go if another one fits
|
||||
you better.
|
||||
</PageHead>
|
||||
|
||||
<Section>
|
||||
<SectionHead eyebrow="contributing" title="How to help.">
|
||||
<SectionHead
|
||||
eyebrow="sharing is caring"
|
||||
title="The network is other people's hard drives."
|
||||
>
|
||||
Every file you pull came off a machine somebody left running and spent years
|
||||
filling. Records and live sets that exist nowhere else stay online because those
|
||||
people keep them there. Download and share nothing back and you are a leech.
|
||||
</SectionHead>
|
||||
<Cols start className="mb-9">
|
||||
<Prose>
|
||||
<p>
|
||||
The protocol has no ratio and no tracker. Users decide for themselves who to
|
||||
serve, and they check what you share before they queue you.
|
||||
</p>
|
||||
<p>
|
||||
Sharing costs you a directory and a process that stays up. It buys you a
|
||||
catalogue that still exists in ten years, and peers who move you up the
|
||||
queue because they browsed your files first.
|
||||
</p>
|
||||
</Prose>
|
||||
<Terminal
|
||||
label="stop being a leech"
|
||||
lines={[
|
||||
{ t: 'cmd', text: 'soulseek-rs shares add ~/Music' },
|
||||
{ t: 'cmd', text: 'soulseek-rs shares status' },
|
||||
{ t: 'cm', text: '# folders and files the network will see' },
|
||||
{ t: 'cmd', text: 'soulseek-rs serve --follow' },
|
||||
{ t: 'cm', text: '# stay online; every upload as it happens' },
|
||||
]}
|
||||
/>
|
||||
</Cols>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{CITIZENSHIP.map((item) => (
|
||||
<FeatureCard
|
||||
key={item.title}
|
||||
glyph={item.glyph}
|
||||
color={item.color}
|
||||
title={item.title}
|
||||
>
|
||||
{item.body}
|
||||
</FeatureCard>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Callout tone="warn" title="locked shares, traders and sellers">
|
||||
<p>
|
||||
Some users restrict their files to buddies, often for fair reasons. A few sit
|
||||
on rare releases and ask you to trade or pay for access. Read their profile,
|
||||
then walk away from the ones charging admission; they thin out when people
|
||||
stop playing along.
|
||||
</p>
|
||||
</Callout>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionHead eyebrow="contributing" title="How to help with the client.">
|
||||
It’s mostly one person who’s been on Soulseek since the early 2000s.
|
||||
File an issue, open a pull request, or just say what’s missing. No
|
||||
contributor agreement, no template gymnastics.
|
||||
</SectionHead>
|
||||
@@ -139,13 +234,10 @@ export const Community = () => {
|
||||
</p>
|
||||
<p>
|
||||
Format and Clippy run clean on every push, and unit tests plus an end-to-end
|
||||
suite against{' '}
|
||||
<ExtLink href={LINKS.soulfind}>soulfind</ExtLink>{' '}
|
||||
run in CI, so a green checkmark means it built and talked to a server.
|
||||
</p>
|
||||
<p>
|
||||
soulfind is an open-source Soulseek server. Run it locally and you develop
|
||||
against a real protocol implementation without touching the public network.
|
||||
suite against <ExtLink href={LINKS.soulfind}>soulfind</ExtLink> run in CI, so a
|
||||
green checkmark means it built and talked to a server. soulfind is an
|
||||
open-source Soulseek server; run it locally to develop against a real protocol
|
||||
implementation without touching the public network.
|
||||
</p>
|
||||
<div className="mt-1 flex flex-wrap gap-2 sm:gap-3">
|
||||
<Button href={LINKS.issues} variant="accent">
|
||||
@@ -167,33 +259,10 @@ export const Community = () => {
|
||||
</Cols>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionHead
|
||||
eyebrow="being a good member"
|
||||
title="The network is other people's hard drives."
|
||||
>
|
||||
Decades of records, live sets, and pressings that never made it anywhere else,
|
||||
kept online by people who care about them. None of this is enforced. It is just
|
||||
how the network stays worth using.
|
||||
</SectionHead>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{CITIZENSHIP.map((item) => (
|
||||
<FeatureCard
|
||||
key={item.title}
|
||||
glyph={item.glyph}
|
||||
color={item.color}
|
||||
title={item.title}
|
||||
>
|
||||
{item.body}
|
||||
</FeatureCard>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<SectionHead
|
||||
eyebrow="what this is not"
|
||||
title="Three good clients to point you elsewhere."
|
||||
title="Four good clients to point you elsewhere."
|
||||
>
|
||||
soulseek-rs has no graphical interface. If one of these fits you better, use it.
|
||||
</SectionHead>
|
||||
@@ -231,7 +300,7 @@ export const Community = () => {
|
||||
MIT, and no strings.
|
||||
</h2>
|
||||
<p className="text-secondary">
|
||||
Free to use, copy, modify, and distribute. Copyright © 2026 Michel de Graaf.
|
||||
Free to use, copy, modify, and distribute. Copyright © 2026 soulseek-rs contributors.
|
||||
</p>
|
||||
<div className="mt-1 flex flex-wrap gap-2 sm:gap-3">
|
||||
<Button href={LINKS.license} variant="accent">
|
||||
|
||||
+90
-108
@@ -1,12 +1,12 @@
|
||||
import { Link } from 'react-router'
|
||||
|
||||
import { HeroTerminal } from '@/components/tui/hero-terminal'
|
||||
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,
|
||||
@@ -73,15 +73,17 @@ const KeyRow = ({ combo, description }: { combo: string; description: string })
|
||||
)
|
||||
|
||||
const FirstRun = () => (
|
||||
<Section>
|
||||
<SectionHead eyebrow="1 · first run" title="Two commands to a running client.">
|
||||
Homebrew puts the binary on your PATH. With a Rust toolchain,{' '}
|
||||
<Code>cargo install soulseek-rs</Code> does the same.
|
||||
<Section flush className="pt-10">
|
||||
<SectionHead eyebrow="1 · first run" title="One command to a running client.">
|
||||
With <Code>soulseek-rs</Code> on your PATH, running it opens the TUI.{' '}
|
||||
<Link to="/install" className="text-link hover:text-link-hover">
|
||||
Installing it
|
||||
</Link>{' '}
|
||||
is one command on macOS, Linux and Windows.
|
||||
</SectionHead>
|
||||
<Cols start>
|
||||
<Terminal
|
||||
lines={[
|
||||
{ t: 'cmd', text: 'brew install michel/tap/soulseek-rs' },
|
||||
{ t: 'cmd', text: 'soulseek-rs' },
|
||||
{ t: 'cm', text: '# the TUI opens; log in or register on first run' },
|
||||
]}
|
||||
@@ -89,9 +91,8 @@ const FirstRun = () => (
|
||||
<Prose>
|
||||
<p>
|
||||
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.
|
||||
register a new one. If a daemon is already running, 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,
|
||||
@@ -108,13 +109,18 @@ const Layout = () => (
|
||||
The whole client is these four boxes. Press <Code>1</Code>–<Code>3</Code> to focus
|
||||
one; the focused pane’s legend turns green.
|
||||
</SectionHead>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{PANES.map((pane) => (
|
||||
<Panel key={pane.title} num={pane.num} title={pane.title}>
|
||||
<p className="text-[13px] leading-[22px] text-secondary">{pane.body}</p>
|
||||
</Panel>
|
||||
))}
|
||||
</div>
|
||||
<Cols start>
|
||||
<div className="overflow-hidden">
|
||||
<HeroTerminal />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{PANES.map((pane) => (
|
||||
<Panel key={pane.title} num={pane.num} title={pane.title}>
|
||||
<p className="text-[13px] leading-[22px] text-secondary">{pane.body}</p>
|
||||
</Panel>
|
||||
))}
|
||||
</div>
|
||||
</Cols>
|
||||
</Section>
|
||||
)
|
||||
|
||||
@@ -185,7 +191,7 @@ const FromTheShell = () => (
|
||||
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, 7 another login took
|
||||
the session away, which is the one a daemon spares you.
|
||||
the session away, the one a daemon spares you.
|
||||
</p>
|
||||
<p>
|
||||
<Code>completions install</Code> gives bash, zsh and fish tab completion for all
|
||||
@@ -202,22 +208,6 @@ const FromTheShell = () => (
|
||||
</Prose>
|
||||
</Cols>
|
||||
<div className="mt-6">
|
||||
<Callout title="give it to your agent">
|
||||
<p>
|
||||
<Code>soulseek-rs skills install</Code> writes a <Code>SKILL.md</Code> into every
|
||||
coding agent on the machine: Claude Code and opencode today, <Code>--dir</Code>{' '}
|
||||
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”.
|
||||
</p>
|
||||
<p>
|
||||
It carries what <Code>--help</Code> cannot: the JSON keys of every record, and
|
||||
what each exit code means. Rerun it after an upgrade;{' '}
|
||||
<Code>skills uninstall</Code> takes it back out.
|
||||
</p>
|
||||
</Callout>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Callout tone="warn" title="the one limit worth knowing up front">
|
||||
<p>
|
||||
If both you and a peer are behind routers with no forwarded port, browsing that
|
||||
@@ -229,6 +219,24 @@ const FromTheShell = () => (
|
||||
</Section>
|
||||
)
|
||||
|
||||
const SubHead = ({
|
||||
eyebrow,
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
eyebrow: string
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
}) => (
|
||||
<div className="mt-13 mb-9 flex max-w-[660px] flex-col gap-3">
|
||||
<Eyebrow>{eyebrow}</Eyebrow>
|
||||
<h3 className="text-[20px] leading-[28px] font-medium tracking-[-0.005em] text-balance">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-base leading-[26px] text-pretty text-secondary">{children}</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
const Daemon = () => (
|
||||
<Section>
|
||||
<SectionHead eyebrow="5 · the daemon" title="One login, however many commands.">
|
||||
@@ -253,58 +261,52 @@ const Daemon = () => (
|
||||
/>
|
||||
<Prose>
|
||||
<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, 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
|
||||
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 works 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. Windows has no Unix sockets: 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. 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.
|
||||
searches and transfers already running; queue something in either and both see it.
|
||||
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 paths you type there
|
||||
name its filesystem, not this machine’s. Close a window and its downloads
|
||||
keep going: they belong to the daemon.
|
||||
</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>.
|
||||
server. It holds the session, not 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
|
||||
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 username is your standing on Soulseek, and the server permits one session per
|
||||
account, so two one-off commands at once cut each other off. A daemon is one login
|
||||
under that name for as long as you work, shares indexed once and reachable the
|
||||
whole time instead of going dark between invocations, and downloads that outlive
|
||||
the command that queued them. If you drive this from a script or an agent, start
|
||||
one.
|
||||
</p>
|
||||
</Prose>
|
||||
</Cols>
|
||||
</Section>
|
||||
)
|
||||
|
||||
const DownloadBox = () => (
|
||||
<Section>
|
||||
<SectionHead
|
||||
eyebrow="6 · a download box"
|
||||
title="Put it on the machine that should be downloading."
|
||||
>
|
||||
A home server, a NAS, a Raspberry Pi. It needs no terminal and nobody logged in.
|
||||
You drive it from your laptop, and it keeps working after you close the lid.
|
||||
</SectionHead>
|
||||
<SubHead eyebrow="use case · an always-on box" title="Put it on a machine you never turn off.">
|
||||
A home server, a NAS, a Raspberry Pi. No terminal, nobody logged in. Downloads keep
|
||||
going after you close the lid, and your shares stay reachable instead of vanishing
|
||||
when the laptop sleeps.
|
||||
</SubHead>
|
||||
<Cols start>
|
||||
<Terminal
|
||||
label="on the box"
|
||||
lines={[
|
||||
{ t: 'cmd', text: 'soulseek-rs shares add /srv/music' },
|
||||
{ t: 'cm', text: '# what the box offers the network' },
|
||||
{ t: 'cmd', text: 'soulseek-rs daemon --bind 0.0.0.0:5030' },
|
||||
{ t: 'cmd', text: 'soulseek-rs daemon token' },
|
||||
{ t: 'cm', text: '# copy this once' },
|
||||
@@ -323,52 +325,41 @@ const DownloadBox = () => (
|
||||
<Prose>
|
||||
<p>
|
||||
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 <Code>daemon status</Code>, and pick up where you left off. The
|
||||
files are on the box, which is where you wanted them.
|
||||
what you want and close the lid: the downloads belong to the daemon, not the
|
||||
command that asked for them. The box answers searches and serves uploads the whole
|
||||
time, which a sleeping laptop cannot.
|
||||
</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:{' '}
|
||||
<Code>ssh -L 5030:localhost:5030 nas</Code>, or run the commands on the box.
|
||||
That port has no encryption. Fine on a home network. Over the internet, reach it
|
||||
through SSH instead: <Code>ssh -L 5030:localhost:5030 nas</Code>, or run the
|
||||
commands on the box.
|
||||
</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’s settings form, which sends paths through as typed.
|
||||
<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 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’s
|
||||
settings form, which sends paths through as typed.
|
||||
</p>
|
||||
</Callout>
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
|
||||
const OpenSchema = () => (
|
||||
<Section>
|
||||
<SectionHead eyebrow="7 · build your own" title="The interface is open.">
|
||||
<SubHead eyebrow="build your own" title="The interface is open.">
|
||||
Nothing about the daemon is specific to this CLI. Point a client generator at its
|
||||
schema and write your own remote.
|
||||
</SectionHead>
|
||||
</SubHead>
|
||||
<Cols start>
|
||||
<Prose>
|
||||
<p>
|
||||
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. The same document is checked in at{' '}
|
||||
<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 it what
|
||||
it can do. The same document is checked in at{' '}
|
||||
<ExtLink href={`${LINKS.gh}/blob/master/docs/openrpc.json`}>
|
||||
docs/openrpc.json
|
||||
</ExtLink>{' '}
|
||||
@@ -377,14 +368,14 @@ const OpenSchema = () => (
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
get typed bindings in whatever language you work in. Build the phone app that
|
||||
queues downloads on your home server from the sofa, or a web page showing what is
|
||||
transferring. Nobody to ask first.
|
||||
</p>
|
||||
<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:{' '}
|
||||
Searches, transfers, rooms, private messages and shares all sit on the same
|
||||
interface this CLI uses, with no private back channel it keeps for itself. 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
|
||||
@@ -432,20 +423,11 @@ const TheRest = () => (
|
||||
|
||||
export const Docs = () => (
|
||||
<>
|
||||
<PageHead
|
||||
eyebrow="docs · quick start"
|
||||
title="Install it, run it, drive it from the keyboard."
|
||||
>
|
||||
This is the short version. The full reference (every flag, every message) lives in the
|
||||
README and on docs.rs.
|
||||
</PageHead>
|
||||
<FirstRun />
|
||||
<Layout />
|
||||
<Keys />
|
||||
<FromTheShell />
|
||||
<Daemon />
|
||||
<DownloadBox />
|
||||
<OpenSchema />
|
||||
<TheRest />
|
||||
</>
|
||||
)
|
||||
|
||||
+29
-26
@@ -29,9 +29,9 @@ const FEATURES = [
|
||||
body: (
|
||||
<>
|
||||
Run it once and your agent knows the record shapes, the exit codes, and the
|
||||
filters: “find the 1998 pressing, lossless only” becomes a pipeline it
|
||||
writes itself. No MCP server, no wrapper. The skill ships in the binary and finds
|
||||
Claude Code and opencode on its own.
|
||||
filters, so “find the 1998 pressing, lossless only” becomes a pipeline
|
||||
it writes itself. The skill ships in the binary and finds your coding agents on its
|
||||
own. No MCP server.
|
||||
</>
|
||||
),
|
||||
},
|
||||
@@ -66,9 +66,9 @@ const FEATURES = [
|
||||
cmd: 'soulseek-rs daemon',
|
||||
body: (
|
||||
<>
|
||||
Hold one session and let every other command borrow it. The network allows one
|
||||
login per account, so this is the neighbourly way to run several at once. It
|
||||
speaks JSON-RPC, so other tools can drive it too.
|
||||
One session every other command borrows. Leave it on a NAS to share and download
|
||||
around the clock, or let an agent fan out twenty commands on one login. It speaks
|
||||
JSON-RPC, so your own tools can drive it too.
|
||||
</>
|
||||
),
|
||||
},
|
||||
@@ -139,7 +139,7 @@ const Hero = () => (
|
||||
<div className="flex items-center gap-3.5 sm:gap-6 md:items-start md:gap-11">
|
||||
<Logo size={172} className="max-md:!h-32 md:mt-2" />
|
||||
<h1 className="text-[29px] leading-[37px] font-bold tracking-[-0.01em] text-balance sm:text-[34px] sm:leading-[42px] md:text-display md:leading-[var(--text-display--line-height)]">
|
||||
A soulseek client for the terminal. Built for agents and people who live there.
|
||||
A Soulseek client for the terminal. Built for agents and people who live there.
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-lg leading-[29px] text-pretty text-secondary max-md:hidden">
|
||||
@@ -214,9 +214,13 @@ const About = () => (
|
||||
nowhere else.
|
||||
</p>
|
||||
<p>
|
||||
soulseek-rs speaks that protocol. It’s a third-party client, not the
|
||||
official app. It shares, browses, and does rooms and private messages, because a
|
||||
client that only downloads takes without giving anything back.
|
||||
soulseek-rs speaks that protocol, as a third-party client rather than the
|
||||
official app. It shares, browses, does rooms and private messages, because a
|
||||
client that only downloads makes you a leech on people who left their machines on
|
||||
for you.{' '}
|
||||
<Link to="/community" className="text-link hover:text-link-hover">
|
||||
What sharing asks of you
|
||||
</Link>
|
||||
</p>
|
||||
</Prose>
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -231,11 +235,11 @@ const About = () => (
|
||||
</Callout>
|
||||
<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. 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.{' '}
|
||||
Every command runs as a one-off. Point a script or an agent at it and read JSON
|
||||
records off stdout. Start <Code>soulseek-rs daemon</Code> first when several run
|
||||
at once, so they share one login instead of cutting each other off.{' '}
|
||||
<Code>skills install</Code> hands your agent that habit, plus the JSON keys and
|
||||
exit codes <Code>--help</Code> leaves out.{' '}
|
||||
<Link to="/docs" className="text-link hover:text-link-hover">
|
||||
See the commands
|
||||
</Link>
|
||||
@@ -249,8 +253,7 @@ const About = () => (
|
||||
const Features = () => (
|
||||
<Section id="features">
|
||||
<SectionHead eyebrow="features" title="What it does.">
|
||||
Everything happens in the TUI, and every command also runs as a one-off from the
|
||||
shell.
|
||||
Everything happens in the TUI, and every command also runs as a one-off.
|
||||
</SectionHead>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{FEATURES.map((feature, i) => (
|
||||
@@ -282,19 +285,19 @@ const Agents = () => (
|
||||
<Prose className="mt-11 max-w-[660px]">
|
||||
<p>
|
||||
<Code>--help</Code> lists flags. It cannot say which JSON keys a record carries, or
|
||||
that exit 4 means widen the query rather than retry it. An agent without that
|
||||
guesses, and guessing against a stranger’s file server hangs the job.
|
||||
that exit 4 means widen the query rather than retry it. An agent guesses instead,
|
||||
and guessing against a stranger’s file server hangs the job.
|
||||
</p>
|
||||
<p>
|
||||
<Code>skills install</Code> fills that in. The requests stop being pipelines you
|
||||
write: mirror a peer’s FLAC folder overnight, or check whether a user is worth
|
||||
queueing behind before a 2 GB transfer.
|
||||
<Code>skills install</Code> fills that in, so you ask for outcomes instead of
|
||||
writing pipelines: fill the gaps in a discography from whoever has a free slot, or
|
||||
read a peer’s profile for their limits before queueing 2 GB behind them.
|
||||
</p>
|
||||
<p>
|
||||
It stays a CLI: no MCP server, no API key. The skill tells your agent to start a
|
||||
daemon first, so a batch of parallel requests is one login rather than one per
|
||||
command. Upgrading the binary upgrades the skill; rerun <Code>skills install</Code>{' '}
|
||||
to take it.{' '}
|
||||
Still a CLI: no MCP server, no API key. The skill tells your agent to start a daemon
|
||||
first, so a batch of parallel requests is one login rather than one per command.
|
||||
Upgrading the binary upgrades the skill; rerun <Code>skills install</Code> to take
|
||||
it.{' '}
|
||||
<Link to="/docs" className="text-link hover:text-link-hover">
|
||||
See the commands
|
||||
</Link>
|
||||
|
||||
@@ -298,10 +298,10 @@ const Steps = () => (
|
||||
instead.
|
||||
</p>
|
||||
<p className="text-[13px] text-muted">
|
||||
<Code>soulseek-rs completions install</Code> then adds tab completion to
|
||||
whichever of bash, zsh and fish this machine uses — generated from the
|
||||
same definition the binary parses with, so it never drifts from{' '}
|
||||
<Code>--help</Code>. Open a new shell to pick it up.
|
||||
<Code>soulseek-rs completions install</Code> adds tab completion for whichever of
|
||||
bash, zsh and fish this machine uses, generated from the same definition the
|
||||
binary parses with, so it never drifts from <Code>--help</Code>. Open a new shell
|
||||
to pick it up.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 sm:gap-3">
|
||||
<Button href={LINKS.releases}>Prebuilt binaries</Button>
|
||||
@@ -320,8 +320,8 @@ const Steps = () => (
|
||||
]}
|
||||
/>
|
||||
<p className="text-[13px] text-muted">
|
||||
You’ll find the binary at <Code>target/release/soulseek-rs</Code>.
|
||||
Prebuilt archives for tagged releases are on the{' '}
|
||||
The binary lands at <Code>target/release/soulseek-rs</Code>. Prebuilt archives for
|
||||
tagged releases are on the{' '}
|
||||
<ExtLink href={LINKS.releases}>releases page</ExtLink>.
|
||||
</p>
|
||||
</Step>
|
||||
@@ -506,9 +506,9 @@ const Config = () => (
|
||||
const Uninstall = () => (
|
||||
<Section band>
|
||||
<SectionHead eyebrow="uninstall" title="Removing it completely.">
|
||||
Three things exist on disk: the binary, the config, and the state — plus
|
||||
the completion scripts and the agent skill, if you asked for those. Nothing else,
|
||||
unless you point <Code>--log-file</Code> somewhere. No telemetry to opt out of.
|
||||
Three things exist on disk: the binary, the config, and the state, plus the completion
|
||||
scripts and the agent skill if you asked for those. Nothing else, unless you point{' '}
|
||||
<Code>--log-file</Code> somewhere. No telemetry to opt out of.
|
||||
</SectionHead>
|
||||
<Cols start>
|
||||
<Terminal
|
||||
|
||||
Reference in New Issue
Block a user