import { Button } from '@/components/ui/button' import { FeatureCard } from '@/components/ui/feature-card' import { ExtLink } from '@/components/ui/ext-link' import { Code } from '@/components/ui/inline-code' import { Cols, PageHead, Prose, Section, SectionHead } from '@/components/ui/layout' import { OsIcon, type OsName } from '@/components/ui/os-icon' import { Callout } from '@/components/ui/panel' import { Terminal, type TermLine } from '@/components/ui/terminal' import { LINKS } from '@/lib/links' const LIB_SRC = `use soulseek_rs::Client; use std::time::Duration; fn main() -> Result<(), Box> { let mut client = Client::new("username", "password"); client.connect()?; client.login()?; let results = client.search("public domain field recordings", Duration::from_secs(10))?; if let Some(r) = results.iter().find(|r| !r.files.is_empty()) { let f = &r.files[0]; client.download(f.name.clone(), f.username.clone(), f.size, "~/Downloads".to_string())?; } Ok(()) }` const LIB_LINES: TermLine[] = LIB_SRC.split('\n').map((text) => ({ t: 'code' as const, text, })) interface InstallRoute { id: string os: OsName best: string lines: readonly TermLine[] alt: React.ReactNode } // aislop-ignore-file code-quality/duplicate-block -- three findings here, all data tables: the INSTALLS records repeat the field names InstallRoute requires, and the two uninstall `lines` arrays repeat the `{ t, text }` shape around different commands. The rule matches punctuation, not logic. const INSTALLS: readonly InstallRoute[] = [ { id: 'macos', os: 'macOS', best: 'Homebrew', lines: [ { t: 'cmd', text: 'brew install michel/tap/soulseek-rs' }, { t: 'cmd', text: 'soulseek-rs' }, ], alt: ( <> A prebuilt binary for Apple silicon and Intel, no Rust toolchain needed. With a toolchain, cargo install soulseek-rs builds from crates.io instead. ), }, { id: 'linux', os: 'Linux', best: 'cargo', lines: [ { t: 'cmd', text: 'cargo install soulseek-rs' }, { t: 'cmd', text: 'soulseek-rs' }, ], alt: ( <> Homebrew on Linux works the same as on macOS. Without either, the{' '} releases page has static musl archives for x86-64 and arm64 that run on any distribution. ), }, { id: 'windows', os: 'Windows', best: 'cargo or a prebuilt .exe', lines: [ { t: 'cmd', text: 'cargo install soulseek-rs' }, { t: 'cmd', text: 'soulseek-rs.exe' }, ], alt: ( <> Cargo needs the MSVC build tools. To skip both, take the{' '} pc-windows-msvc zip from the{' '} releases page, unpack it, and put{' '} soulseek-rs.exe on your PATH. ), }, ] interface PlatformNote { id: string name: string credentialStore: string config: string state: string downloads: string note: string } const PLATFORMS: readonly PlatformNote[] = [ { id: 'macos', name: 'macOS', credentialStore: 'Apple Keychain', config: '~/.config/soulseek-rs/config.toml', state: '~/.local/share/soulseek-rs/state', downloads: '~/Downloads/Soulseek', note: 'Follows the XDG layout rather than Application Support.', }, { id: 'linux', name: 'Linux', credentialStore: 'Secret Service (pure-Rust backend, no libdbus needed)', config: '~/.config/soulseek-rs/config.toml', state: '~/.local/share/soulseek-rs/state', downloads: '~/Downloads/Soulseek', note: 'Honours XDG_CONFIG_HOME and XDG_DATA_HOME when they are set.', }, { id: 'windows', name: 'Windows', credentialStore: 'Windows Credential Manager', 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.', }, ] interface Setting { key: string fallback: string body: React.ReactNode } const SETTINGS: readonly Setting[] = [ { key: 'username', fallback: 'unset', body: <>Your Soulseek account name. The TUI writes it here after a first login., }, { key: 'server', fallback: 'server.slsknet.org:2416', body: <>The server to log in to, as host:port., }, { key: 'listener_port', fallback: '2234', body: ( <> The TCP port peers connect back on. Forward it, or let portmap ask your router. ), }, { key: 'disable_listener', fallback: 'false', body: ( <> true refuses incoming peer connections. You can still download from reachable peers, but firewalled ones can no longer reach you. ), }, { key: 'download_dir', fallback: 'the Downloads path above', body: <>Where finished transfers land., }, { key: 'shared_dirs', fallback: 'the download folder', body: ( <> Folders offered to the network. An empty list shares nothing.{' '} shares add writes this; the older shared_dir spelling still reads. ), }, { key: 'max_concurrent_downloads', fallback: '20', body: <>Transfers allowed to run at once. The rest queue., }, { key: 'search_timeout', fallback: '10', body: ( <> Seconds a search collects replies before reporting. Results trickle in from separate peers, so a short window finds less. ), }, { key: 'password_cmd', fallback: 'unset', body: ( <> A shell command whose stdout is the password, used when the keychain has none. The password itself never belongs in this file. ), }, ] const CONFIG_EXAMPLE: TermLine[] = [ 'username = "your-name"', 'download_dir = "~/Music/Soulseek"', 'shared_dirs = ["~/Music/Soulseek", "~/Music/Library"]', 'max_concurrent_downloads = 3', 'search_timeout = 15', 'listener_port = 2234', '', '# never store the password itself; shell out for it instead', 'password_cmd = "pass show soulseek"', ].map((text) => ({ t: 'code' as const, text })) interface StepProps { n: number title: string children: React.ReactNode } const Step = ({ n, title, children }: StepProps) => (
{n}

{title}

{children}
) const Steps = () => (

One command per platform. Cargo needs a Rust toolchain from{' '} rustup.rs; the other routes don’t.

{INSTALLS.map((route) => (

{route.os}

{route.best}

{route.alt}

))}

Every route puts the same binary on your PATH. Run soulseek-rs in a terminal and the TUI opens, in a script or a pipe it wants a subcommand instead.

soulseek-rs completions install 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{' '} --help. Open a new shell to pick it up.

Clone the workspace and build a release binary.

You’ll find the binary at target/release/soulseek-rs. Prebuilt archives for tagged releases are on the{' '} releases page.

Write your own client or bot on soulseek-rs-lib: the protocol implementation, separate from the TUI.

Full API reference on docs.rs. The lib API changes between majors, so check the{' '} changelog when you upgrade.

) const Reachable = () => (
Browsing and downloading are peer-to-peer, so at least one side has to accept an incoming connection. serve refuses to start with the listener off.

With the listener on (the default), soulseek-rs tries to open its listen port on your router via{' '} UPnP-IGD and{' '} NAT-PMP. It’s best-effort: if your router has those disabled it’s a no-op, and you forward the port yourself.

The port is your --listener-port (env{' '} SOULSEEK_LISTENER_PORT, default 2234); soulseek-rs renews it while it runs and removes it on exit. Pass{' '} --no-listener to turn it off. Check your own network without launching the client:

If both you and a peer are behind routers with no forwarded port, browsing that peer can’t work, that’s a fundamental Soulseek/peer-to-peer limitation, not a bug. Forward --listener-port, or let{' '} portmap try UPnP.

) const Platforms = () => (
Sensible defaults per OS, all overridable. Set SOULSEEK_CONFIG_DIR or{' '} SOULSEEK_STATE_DIR to relocate either one, which also makes a portable install possible.
{PLATFORMS.map((platform) => (

{platform.name}

{( [ ['Password', platform.credentialStore, false], ['Config', platform.config, true], ['State', platform.state, true], ['Downloads', platform.downloads, true], ] as const ).map(([label, value, mono]) => (
{label}
{mono ? ( {value} ) : ( value )}
))}

{platform.note}

))}
Your password goes to the OS keychain, never a plain-text file. Point{' '} password_cmd at your own password manager and it is never stored at all. For CI and containers,{' '} --password-stdin reads it off a pipe so it never reaches{' '} ps or the environment. config.toml 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 .bak rather than overwriting it.
) const Config = () => (
Every one has a working default, so an empty file, or no file at all, is valid.{' '} config list prints the effective value of each.
{SETTINGS.map((setting) => (
{setting.key} {setting.fallback}

{setting.body}

))}

config path prints the file in use, config get{' '} reads one setting and config set writes one. It rejects unknown keys, so a typo fails instead of doing nothing.{' '} --no-config ignores the file entirely and{' '} --config FILE points at another one.

) const Uninstall = () => (
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 --log-file somewhere. No telemetry to opt out of. Windows Credentials' }, ]} />

Your downloads and your shared directories are yours: they stay where they are. On macOS, delete the keychain entry for soulseek-rs in Keychain Access if you saved a password.

) export const Install = () => ( <> One command with Homebrew or cargo. Build from source if you’d rather. Or depend on the library and write your own client. )