Moving through a browsed share felt laggy for the same reason the
results did: the popup re-flattened the whole expanded tree on every
frame, cloning every row's name and path, then built a table row for
each, and every keypress flattened it twice more. A heavy sharer's
tree runs to tens of thousands of rows once a few folders are open.
The flattened rows now live on the browse state and are rebuilt only
when a folder opens or closes, and the popup draws the visible page
alone. In the harness a 30,000-row tree went from 11ms to 0.35ms a
frame and a keypress from 2.7ms to under a microsecond.
The downloads pane had the same shape (every transfer became a row
each frame, and a select-all can queue ten thousand), so it takes the
same page window, which now lives beside page_of for every pane to
share.
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.
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.
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.
Seven region audits, every finding adversarially verified against the
whole workspace before being applied; 80 landed and the two that failed
in application were refuted honestly (ParseError has a from_str_radix
producer the audit missed; NatPmpError's Display is the only reader of
its diagnostic payloads).
The bigger cuts:
- The NewPeer/PierceFireWall cluster: an operation variant nothing ever
sent, its 77-line arm, a protocol-wrong Client::pierce_firewall that
wrote a peer-init frame down the server socket, and the struct only
they used. The live paths in peer/listen.rs are untouched.
- The ops loop reads `for operation in reader` instead of hand-rolling
recv/break, de-indenting the whole dispatch body.
- Dead lib surface: Message::print_hex and four unused accessors,
UploadFailed, registry and actor getters, build_watch_user,
strip_ansi_codes, delegating shims on ClientContext. Removing public
items makes this a semver major, which the release pipeline enforces.
- Never-used flexibility: the state-migration framework that never held
a migration, border-type helpers that all answered Rounded, the TUI's
max_concurrent_downloads field nothing read.
- Convergence: six poll loops into poll_until, three wrap-around
navigation blocks into cycle, hit tests into Rect::contains, char
walkers into floor/ceil_char_boundary, five Download literals into
one helper, the suites' shared scaffolding into tests/common.
Deliberately not taken: swapping the hand-rolled zlib inflate and MD5
for crates (they replace wire-facing machinery whose tolerance of
real-world peer quirks no local test exercises) and pulling chrono into
the lib for a log timestamp. Recorded for their own decision.
Net -1002 lines. Both e2e modes stay green: 94 CLI tests each way, 16
protocol, 33 lib, unit suites, clippy pedantic clean, aislop 100/100
with the four scanner false positives suppressed inline where they
fire.
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`.
The cursor row set a foreground, and ratatui only skips a cell's colour
when that field is None — so selecting a row repainted its gold sizes,
blue usernames and status glyphs into one flat cream. Table cursors now
tint the background only, over a warm oxide wash instead of neutral.
Browse files, their sizes and the settings popup rendered in the raw
terminal foreground; they take the palette now, and free slots read
green when a peer has one.
Peer layer:
- Stream peer messages with a partial-write queue instead of write_all, so a
large SharedFileListResponse over the non-blocking socket is no longer
truncated (third-party clients like SoulseekQt can now browse our shares).
- Bound the peer-init handshake with a read timeout and handle each accepted
connection on its own thread, so one stalled peer can't wedge the listener.
- e2e: third-party direct/brokered/large-share browse + stalled-peer coverage.
TUI:
- Uploads share the downloads table columns (real Speed) and the info pane now
renders details for a selected upload.
- Download progress bar counts only in-progress transfers and hides when idle.
- Always-visible unread badge (private messages + rooms) in the shortcuts bar,
self-drawn blink so it works under tmux.
- Persist the unread private-message count across restarts.
Replace the ANSI Light* palette with the design system's tokens
(tokens/colors.css): oxide accent, phosphor focus, tape/signal/alarm
semantics, paper text on dust chrome. Only foregrounds and the row
highlight are painted so the user's terminal background shows through.
Route every bordered pane through a single pane_block() helper carrying
one column of horizontal padding, and give legends a consistent
[n] Title form with the tag in signal. The command bar now derives its
cursor position from the block's inner rect instead of a hardcoded
border offset.
Also collapses the download-status glyph match that was duplicated
across three render sites into download_status_glyph().
Press 'o' for a settings popup: edit the download folder, add/remove
share paths (applied live via a rescan and persisted to config.toml),
and force a re-index with 'r' for files changed on disk. The state
machine is unit-tested; feedback shows the shared file/folder counts.
The downloads pane is now Downloads/Uploads: uploads being served are
listed after downloads with an up-arrow marker, live progress, and
status; 'x' cancels an in-progress upload. Client::shared_counts exposes
the index size for the status line.
Carry a human-readable reason on DownloadStatus::Failed so it flows
through the existing status channel to the TUI. The download Info pane
now shows an Error line for a failed download (connect failure, peer
disconnect, truncated transfer, peer-reported upload failure, or the
underlying transfer error). Failures without a known reason render as
before.