module

ArrTop::Render

Pure, I/O-free rendering helpers for the TUI. Every function here takes plain data (and, where colour is wanted, a Theme) and returns a String; nothing touches the terminal, the clock, or the network, so the whole module is unit-testable offline. The TUI loop is the only thing that turns these strings into terminal writes.

ALIGNMENT CONTRACT: every column/box helper lays out on plain text and applies colour only after the visible width is fixed (see Theme#colorize, which never changes visible width). So with a disabled theme each helper returns a string whose .size is exactly the width it was asked for — the property the specs assert to guarantee the right border lines up.

Constants

BAR_WIDTH = 20

Max cells inside a progress bar's brackets. The bar shrinks below this to fit a narrow terminal; it never grows past it.

BOX_BL = "╚"
BOX_BR = "╝"
BOX_H = "═"
BOX_ML = "╠"
BOX_MR = "╣"
BOX_TL = "╔"

Double-line box-drawing pieces (U+2550–U+2563).

BOX_TR = "╗"
BOX_V = "║"
BYTE_UNITS = ["B", "KB", "MB", "GB", "TB", "PB"]

Byte-size unit ladder (base 1024).

HEADER_LABELS = {movie: "MEDIA", torrent: "TORRENT", status: "STATUS", size: "SIZE", progress: "PROGRESS"}

Column header labels.

MAX_PROGRESS = ((BAR_WIDTH + 2) + 1) + PCT_WIDTH

Widest a whole progress cell (bar + space + percent) is allowed to get: a full BAR_WIDTH bar (+2 brackets) + a space + the percent.

MOVIE_WIDTH = 20

Fixed column widths (visible cells). Movie/Torrent are hard limits — their text is truncated to fit, never allowed to widen the layout.

PCT_WIDTH = 6

Visible width of the percent readout, e.g. " 50.0%" / "100.0%".

SIZE_WIDTH = 16
STATE_LABELS = {State::Importing => "importing", State::ImportPending => "pending", State::Downloading => "downloading", State::Failed => "failed", State::Queued => "queued", State::Unknown => "other"}

Human-readable status label per State (also used in the queue summary).

STATUS_WIDTH = 11
TORRENT_WIDTH = 28

Class methods

bar(percent : Float64, width : Int32, theme : Theme = Theme.disabled) : String

A bracketed two-tone block bar filled to percent (clamped 0–100) with exactly width cells between the brackets: [ + filled cells + remaining cells + ]. Both cell glyphs come from the theme (both default to ), so the bar is a solid run distinguished only by colour — filled in the theme's filled colour, the remainder in its remaining colour, the brackets in the bracket colour. Filled count = round(pct/100 · width), clamped to [0, width]. With a disabled theme the glyphs are emitted bare, so the visible width is always width + 2. width <= 0 yields [].

Source
bottom_border(cols : Int32, theme : Theme) : String

The ╚═…═╝ bottom border, cols wide.

Source
divider(cols : Int32, theme : Theme) : String

The ╠═…═╣ divider between the header labels and the data rows, cols wide.

Source
header_row(theme : Theme, width : Int32) : String

The column-header label row, aligned to the same columns as render_row, exactly width visible cells wide.

Source
human_bytes(n : Int64) : String

Formats a byte count like 9.43 GB (base 1024). Whole bytes stay as 512 B; larger units get two decimals. Non-positive counts are 0 B.

Source
human_duration(span : Time::Span) : String

Formats a span like 1h20m, 5m3s, or 42s. Non-positive spans are 0s; anything past 99h caps at 99h+.

Source
human_size_pair(disk : Int64 | Nil, total : Int64) : String

Formats a row's SIZE cell. When disk is nil the row is not actively importing, so it shows just the size — a single human_bytes(total), e.g. 2.90 GB. When disk is non-nil (an importing row) it shows a disk / total pair where each side picks its own unit — so a small on-disk value keeps a meaningful magnitude instead of rounding to 0 in the total's larger unit: 0 B / 2.1 GB (just started), 44 MB / 2.1 GB (copying), 1.5 GB / 2.1 GB, 2.1 GB / 2.1 GB (done). Each number uses up to one decimal with a trailing .0 trimmed. The on-disk value is capped at the total so the pair never shows a numerator larger than the denominator (an import file that overshoots the per-episode estimate displays as full, not 2.3 / 2.1 GB). total <= 0.

Source
plan_columns(width : Int32) : Tuple(Int32, Int32, Int32, Int32, Int32)

Column widths {movie, torrent, status, size, progress} for a content row of width visible cells. Columns are placed left-to-right at their fixed widths with one-space gaps; when the row is too narrow the rightmost columns shrink and then drop (width 0) rather than wrap. progress takes whatever remains, capped at MAX_PROGRESS.

Source
render_row(row : QueueRow, import : ImportProgress | Nil, disk_bytes : Int64 | Nil, size_bytes : Int64, theme : Theme, width : Int32, display_state : State = row.state) : String

One data row laid out into width visible cells: Movie · Torrent · Status · Size · Progress. Movie is media_name ( when nil), Torrent is the release title; both are truncated to their fixed widths. Status is the coloured state label. Size is right-aligned via human_size_pair: an actively importing row (disk_bytes non-nil, the copied bytes) shows the disk / total pair (e.g. 0 B / 2.1 GB2.1 GB / 2.1 GB); every other row (disk_bytes nil) shows just the size size_bytes (e.g. 2.9 GB). Progress is a bar+percent — only for Downloading (from download_percent) and Importing (from import's copy percent) rows; every other state (incl. ImportPending) leaves the progress cell blank. The returned string is exactly width visible cells wide.

display_state is the effective state to render (label + progress); it can differ from row.state when the TUI reclassifies a Sonarr season-pack row (e.g. an "importing" episode with no file yet is displayed as pending). It defaults to row.state so callers that don't reclassify are unaffected.

Source
summary_text(counts : Hash(State, Int32)) : String

One-line summary of the queue counts, e.g. 3 importing · 1 downloading, or idle when every count is zero. Zero-count states are omitted.

Source
top_border(cols : Int32, counts : Hash(State, Int32), speed : String, theme : Theme) : String

The top border: ╔ … ╗ filled with , embedding the app title, the queue-counts summary, and (when non-blank) the aggregated transfer speed at the right. Exactly cols visible cells wide. Degrades by dropping the speed, then truncating the summary, and finally to a plain rule when the terminal is too narrow for any text.

Source
truncate(str : String, width : Int32) : String

Truncates str to at most width characters, ending with when it overflows. width <= 0 yields an empty string.

Source
wrap(content : String, cols : Int32, theme : Theme) : String

Wraps interior content (already exactly cols - 2 visible cells) in the left/right borders, producing a cols-wide line.

Source