import { Button } from '@/components/ui/button' import { FeatureCard } from '@/components/ui/feature-card' import { Code } from '@/components/ui/inline-code' import { Cols, PageHead, Prose, Section, SectionHead } from '@/components/ui/layout' 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(()) }` // LIB_SRC carries no comment lines, so every line is plain code. const LIB_LINES: TermLine[] = LIB_SRC.split('\n').map((text) => ({ t: 'code' as const, text, })) 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: 'macOS deliberately follows the XDG layout rather than Application Support, so config sits beside your other dotfile config.', }, { 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 StepProps { n: number title: string children: React.ReactNode } const Step = ({ n, title, children }: StepProps) => (
{n}

{title}

{children}
) export const Install = () => { return ( <> One command if you have Rust. Build from source if you’d rather. Or depend on the library and write your own client.

Needs a Rust toolchain, available from{' '} rustup.rs . Then:

Cargo puts the binary on your PATH. Run soulseek-rs and the TUI opens.

No Rust toolchain? Grab a prebuilt archive for your platform from the{' '} releases page , unpack it, and put the binary somewhere on your PATH.

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 . Note: v6 moved sharing from one directory to a list. See the{' '} changelog {' '} if you’re upgrading from 5.x.

Browsing and downloading are peer-to-peer, so at least one side has to accept an incoming connection.

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 LISTENER_PORT, default 2234); it’s renewed automatically and removed on exit. Pass --disable-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.

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. If you would rather it never be stored at all, have password_cmd shell out to your own password manager and print it on demand. 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; anything unreadable is set aside as{' '} .bak instead of being overwritten.
Three things exist on disk: the binary, the config, and the state. Nothing else is written, and there is no telemetry to opt out of. Windows Credentials' }, ]} />

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

) }