Every size cell converted bytes to gigabytes and formatted the float in
place, so anything below a gigabyte rendered as 0.00 GB. Books and
tracks are almost always that small, which made the Size columns in
Readarr and Lidarr useless.
Replace convert_to_gb and convert_f64_to_gb with one format_size helper
that renders gigabytes at or above 1 GiB and megabytes below it, at the
precision each site already used. Every render site across the four
servarr trees now calls it, including the two movie and episode detail
strings built on the network side. Values at or above a gigabyte render
byte for byte as before.
Give the Readarr book statistics and book file fixtures and the Lidarr
album statistics fixture realistic sub-gigabyte sizes so the megabyte
path is pinned by the committed snapshots.
DownloadStatus was declared four times, byte for byte identically, in
radarr_models, sonarr_models, lidarr_models, and readarr_models: the same
variants, the same derive list, and the same serde, strum, and display_style
attributes. Every servarr's queue endpoint reports the same status
vocabulary, so there is nothing servarr specific about the type.
Move it into servarr_models, preserving that file's alphabetical ordering
(between DiskSpace and IndexerSettings), and drop the four copies. The
DownloadRecord types keep their per-servarr shapes and now reference the
shared enum.
The two enum tests, test_download_status_display and
test_download_status_to_display_str, were likewise duplicated in the four
per-servarr model test files. Keep one copy of each in servarr_models_tests
alongside the other shared enum display tests and delete the rest. The
per-servarr DownloadRecord fallback deserialization tests stay where they
are since they exercise each servarr's own record type.
Behavior is unchanged. Every remaining edit is an import path update.
The shared Book fixture left statistics as None, so every author details
snapshot rendered the missing-statistics N/A cells and the populated
branch of the Files and Size columns was only exercised by one test.
Give the fixture distinct, non-default counts and a size that formats to
a value different from the dedicated statistics test.
The season, album and book tables rendered 0/0 and 0.00 GB whenever the
API omitted the statistics object for a row, presenting fabricated counts
as real. The row colour already treated that case as indeterminate; the
cell text now does too, rendering N/A for both cells, matching what these
tables already do for a missing release date.
Sonarr's season row is restructured from a destructure-with-default to the
same Option-aware map_or_else shape lidarr and readarr use.
The readarr test fixture book has no statistics, so its existing author
details snapshots change from the fabricated zeros to N/A on exactly that
row.
The add author confirmation popup previously left the space above the
root folder field blank. It now renders the selected search result's
overview there, in a fixed six line area matching the Radarr add movie
prompt, so the user can confirm they picked the right author before
adding. An author without an overview keeps the same layout.
The fully populated search result fixture gains a long overview so the
snapshot proves the paragraph is clipped to six lines.
The collapsed one-line overview on the Lidarr artist, Sonarr series, and Readarr author details views replaced every line break and tab with an empty string. Words on either side of a paragraph break fused together, rendering as "20th century.She was born in Ohio".
Collapse each run of whitespace to a single space instead, and trim the result. Handling a whole run at a time keeps a Windows line ending, an indented paragraph, or a space that already precedes a line break from producing two spaces.
The overview popups are unaffected: they receive the raw text and keep their paragraph breaks.
draw_logs read the level from the log line's Display output, which drops the first offset characters. Once a log line is scrolled, the field at index 1 is no longer the level: past the timestamp it yields the logger name and colours the row wrong, and past the final delimiter the index is out of bounds and panics, taking the TUI down.
Parse log.text instead, matching what draw_logs_popup already does. The displayed text is unchanged.
ReadarrData::test_default_fully_populated seeded both profile maps with the same id and the same name, and gave the author fixture the same id for both profiles. The library table renders the two profiles in adjacent columns, so both read Standard and swapping the maps produced identical output.
The maps now use the disjoint ids and the vocabulary the root folder and add author tests already use, so the columns read Standard and Comprehensive.
Verified by swapping the two map lookups where the authors table is built. That swap survived the whole suite before this change and is now caught by sixteen snapshots.
An author's overview renders as the last line of a description box that
does not scroll, so a long overview overflows with no way to read the
rest. Pressing v on the author details Books tab now opens a popup
holding the full overview.
The popup owns its own ScrollableText, built fresh from the author's
overview when it opens and torn down on Esc so a reopened popup starts
at the top. Scrolling mutates only that owned state. The background
paragraph renders the Author record's overview, which carries no scroll
offset, so it cannot scroll along with the popup.
Add a view keybinding bound to v, cite it from the author details
context clues, and give the popup its own clue set so it does not
inherit the hints of the view underneath.
Render the indexers table with its delete and test popups, the edit
indexer prompt with separate torrent and usenet field grids, the prompt
for configuring all indexer settings, and the test all indexers results
table. All four are dispatched from the Readarr UI.
Extract indexer_flag_text, decorate_test_result_row_with_style and the
two prompt highlight builders as named functions so their branches can
be unit tested. Rendering to a string discards colour, so a snapshot
cannot tell a success style from a failure style.
The system tab renders the tasks table, the queued events table and the log list, and overlays the log details, tasks, queued events, task start confirmation and updates popups.
SystemUi and SystemDetailsUi ship together because SystemUi::accepts covers every block that only SystemDetailsUi draws. Landing them apart would advertise five blocks that nothing renders.
Log level extraction is pulled into a named function so it can be unit tested directly: the level only ever reaches the output as a colour, and the rendered snapshots are plain strings that discard it.
Renders the root folders table, the delete prompt and the add root
folder modal. The modal conveys its current step purely through
styling, which rendered output discards, so the step highlights are
computed by a separate function whose branches are asserted directly.
Renders the history table, its sort, search and filter prompts, both
empty result popups and the item details view. Snapshots cover a
non-default selection so that reading a row from a fixed index rather
than the current selection cannot pass unnoticed.
Render the Readarr downloads table with the cancel download prompt and
the update downloads prompt, and dispatch it from ReadarrUi so the tab
is reachable.
The snapshot suite covers every block in DOWNLOADS_BLOCKS plus loading
and empty states. A second set of snapshots seeds a two row fixture and
selects the second row. Its rows deliberately have different completion
ratios, so that inverting the percentage calculation is caught rather
than cancelling out against a half finished download.
Render the Readarr blocklist table with the item details popup, the
remove item prompt and the clear blocklist prompt, and dispatch it from
ReadarrUi so the tab is reachable.
The snapshot suite covers every block in BLOCKLIST_BLOCKS plus loading
and empty states. A second set of snapshots seeds a two row fixture with
distinct values in every column and selects the second row, so that
reading items[0] instead of the current selection cannot pass.
The overview was rendered second of twelve into a fixed height block
with no scrolling, so free text of any length pushed status, genres,
rating, path and the statistics off the bottom where they could not be
reached. Every author in a real library has an overview and the median
runs several lines, so those fields were usually invisible.
Rendering it last keeps the structured fields on screen and lets the
overview take whatever space is left.
Renders the edit author prompt with a snapshot per step of the
selection state machine, seeding distinct quality and metadata profile
values so a swap between the two dropdowns cannot go unnoticed.
Renders the author search box, the search results table, and the
multi-step add confirmation prompt with its five dropdown popups.
The prompt's per-field highlighting is conveyed purely by style, which
render_to_string_with_app discards, so the highlight mapping is extracted
into add_author_prompt_highlights and unit tested per selection step.
Enriches the fully populated author search fixture with a second result
that is absent from the library, so both branches of the in-library
marker render in one snapshot and every table column carries a value.
Span::style assigns rather than patches, so chaining it after bold()
discarded the modifier that bold() had just set and every detail popup
label rendered plain. Apply the modifier to the style itself instead.
Snapshots compare rendered text and cannot observe styling, so this is
pinned by asserting the label span's modifier directly.
Every line of the edition details popup was parsed as a label and a value,
so a line carrying no colon had one appended to it. A multi-line overview
therefore rendered with a spurious colon at the end of each continuation
line, and Goodreads sourced descriptions, which use CRLF, turned their
blank lines into a lone colon.
Split each line on the first colon only, leave a line without one
unlabelled, and trim the trailing carriage return so a CRLF blank line
renders as a real paragraph break.
Building the line is now a named function so its branches can be asserted
directly. Rendered text discards the carriage return, so a snapshot alone
cannot tell the trimming apart from leaving it in place.
A continuation line that happens to contain a colon is still styled as a
label. Telling those apart needs the details to be built as styled lines
rather than flattened to text and parsed back.
Draw the edition details popup over the book details view. The popup body is
the scrollable text the book details handler builds from the already loaded
edition row, so it needs no tabs and no network call.
The text styling is extracted into a named function so both branches are unit
tested, and the shared test fixture now holds the real field lines instead of a
placeholder, which exercises the scroll offset and the values that themselves
contain a colon.
Draw the book details popup over the author details view, with the editions
table, book history, the book file info tab, and the manual search releases
table, plus the automatic search, delete book file, download release, and
history details prompts.
Edition row styling is extracted into a named function so each branch is unit
tested; text snapshots discard colour and cannot see it. The shared test
fixture now populates the edition format, ISBN13, ASIN, and publisher, and the
book file media info, so the rendered columns are exercised rather than blank.
Add AuthorDetailsUi: the author description pane plus the three author info
tabs (Books, History, and Author Search), along with the history item details,
automatic search, update and scan, and manual search confirmation popups.
LibraryUi now accepts and draws it over the authors table.
The description pane omits the Disambiguation and Type lines that the Lidarr
equivalent renders. GET /author returns neither key, so both lines would be
blank on every record. The books table drops the Type and Duration columns for
the same reason, since books have no type and no duration, and renders Files as
the book file count over the total book count.
Land create_history_event_details together with this first caller so it is not
committed as an unused function.
Extract decorate_book_row_with_style rather than inlining the row styling in
the table closure, matching decorate_author_row_with_style in the sibling
library view. Rendering to a string discards colour, so the styling branches
are only reachable by tests through a named function.
The stats, downloads, and logo panes of the Readarr context row had no test
exercising them. Add snapshots for the populated, loading, and empty variants
of the context row, plus loading and empty variants of the tab view.
Promote Route::Readarr from a unit stub to a tuple variant and wire Readarr
through every layer, mirroring the Lidarr tree. Readarr appears as a fourth
servarr tab and its library view renders the Authors table.
Adds ReadarrData and its modals, the tick and context clue modules, the
top-level ReadarrHandler, ReadarrUi with the Authors library view, and an
empty ReadarrCommand behind ReadarrCliHandler. The default port is 8787.
Promoting the route variant compiles cleanly without any of the arms that
give it behaviour, so all six of those sites are wired explicitly here:
App::on_tick, handlers::handle_events, handle_prompt_toggle,
ServarrContextClueProvider, ui(), and verify_config_present_for_cli. Each was
checked by removing the arm and confirming its covering assertion then fails.
ReadarrEvent and ReadarrCommand are both declared empty. They gain variants
alongside the network calls and CLI commands that use them, which keeps each
later change to a single function.
Adding the tab changes the rendered tab bar, so 13 existing snapshots
regenerate. The only delta in each is that single tab bar line.
This scaffold adds 24 dead_code warnings, all inside the Readarr tree and
none outside it. They clear as the handlers, UI, network events, and CLI
commands that consume these items land: block arrays awaiting their accepts
implementations and BlockSelectionState seeding, modal and state fields
awaiting their handlers, the CLI handler fields awaiting an inhabited command
enum, and the top-level handler fields awaiting sub-handlers. The count must
strictly decrease from here, so a later increase is a defect rather than
transient noise. The 12 pre-existing collapsible_match warnings are unrelated
and untouched.