You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Tracks all renderer candidates for splashboard. Part of the widget catalog index in #41.
What a renderer is
See #41 and AGENTS.md for the full architecture. In short: a renderer consumes a Payload + RenderOptions and draws into a ratatui frame. It declares name(), accepts() (which Shape variants it can render), animates(), and render(). One shape can feed multiple renderers.
Before building a renderer from scratch, check awesome-ratatui. Many primitive gaps already have community crates that we can wrap with a thin Renderer impl — a few lines mapping Payload → widget input. Each gap below notes candidate crates where relevant.
Two paradigms of "animation" are worth distinguishing, since different libraries serve each:
Post-process effects — render the widget normally, then apply a shader-like pass (fade, color shift, glitch, slide, dissolve, sweep). tachyonfx (50+ effects) is the clear winner; it works on the ratatui buffer after the widget drew into it.
Reveal animations — the widget's final form emerges from a particle-like animation (matrix rain decrypting into text, beams scanning, fireworks coalescing, characters burning up from below). TTE is the reference. tachyonfx can't cleanly express most of these because its paradigm is buffer post-processing, not per-character physics. Expect a custom build for this family (clean-room, TTE as algorithmic reference). tui-rain covers matrix/rain specifically.
ansi-to-tui covers ANSI passthrough as a separate concern.
Naming conventions
Renderer names are user-facing (they appear in render = "..." config) so drift is expensive to undo. Locking in the rules here so new renderers don't add fresh inconsistencies:
Registered name == module name. One-to-one. Grep-ability across config and source.
Every renderer carries a family prefix, never a suffix.chart_bar / chart_line / chart_pie, not bar_chart / line_chart / pie_chart. Prefix form is sortable (all siblings cluster), autocomplete-friendly, and consistent with the clock_* / system_* / git_* / github_* fetchers. No bare names — every renderer belongs to exactly one family.
[x] shipped (code in src/render/), [ ] planned. Planned entries are annotated with wrap (existing ecosystem crate) or build (from-scratch). Sorted by family to match the naming convention above.
text_*
text_plain — plain-text lines (accepts Text + TextBlock)
text_ascii — figlet-style big text via tui-big-text / figlet-rs
text_markdown — render Markdown via tui-markdown. Shipped in feat(render): add text_markdown renderer + MarkdownTextBlock shape #151. Introduced a dedicated MarkdownTextBlock shape (rather than reusing Text/TextBlock) so a fetcher emitting it commits to "this string is Markdown source" — Text payloads can't accidentally route here, and text_plain can't be asked to render Markdown verbatim. Theme integration via SplashStyleSheet: headings → panel_title, code/links → accent_event, blockquote → text_secondary, heading_meta → text_dim. Syntect highlight intentionally disabled (tui-markdowndefault-features = false) so fenced code blocks stay on theme tokens. Currently only basic_static emits the shape; richer fetcher consumers (README excerpts, release notes) deferred to follow-up. Side fix: default_cache_key now includes ctx.shape so multi-shape fetchers don't cache-poison across shape variants.
text_diffwrap — highlighted unified diff (last commit, staged changes). Wrap syntect or paint inline with ratatui Text + red/green line styling.
list_links — clickable rows over the new LinkedTextBlock shape, shipped in feat: LinkedTextBlock shape with clickable rows (OSC 8) + hn_top fetcher #148. Each row whose url is Some(_) is wrapped in OSC 8 escape sequences so modern terminals (iTerm2, kitty, WezTerm, recent Windows Terminal, …) surface it as a hyperlink; rows without a URL render as plain text. Implementation note: the OSC 8 sequence is collapsed into the row's first cell with skip = true on the trailing cells — without that, ratatui's Buffer::diff counts printable bytes inside the escape sequence as visible columns and silently drops the cell after the link.
list_checklistbuild — checkbox items with done/pending state. New shape Checklist { items: Vec<{label, done}> }. Todos, release checklists, pre-flight checks.
list_cards — card rows over the new ImageLinkedList shape, shipped in feat(render): add list_cards renderer + ImageLinkedList shape #217. Each item is {title, url?, thumbnail_path?, subtitle?}; rows with url get the same OSC 8 wrap as list_links (via a shared pub(crate) wrap_osc8 helper), rows without a thumbnail_path keep a blank cell so the text column stays column-aligned. Options: thumbnail_width (default 6 cells), row_height (default 3), gap (default 1), fit (contain / cover / stretch, mirrors media_image). Reuses media_image::draw_thumbnail (extracted to pub(crate)) so the ratatui-image pipeline isn't duplicated. Remote thumbnail URLs route through a shared fetcher::thumbnails::download_to_cache that caches to $HOME/.splashboard/cache/thumbnails/<sha256>.<ext> with magic-byte sniffing and a 4 MB cap; per-row failures collapse to None so a single broken image doesn't break a feed. Fetcher consumers shipped in the same PR: rss (extracts media:thumbnail → media:content → first HTTP(S) <img> in <content>/<summary> with HTML-entity decoding), reddit_subreddit_posts / reddit_user_posts (Reddit thumbnail URL, filtering placeholder strings self/default/nsfw/spoiler), wikipedia_featured / wikipedia_random (REST API thumbnail.source), and basic_read_store (JSON/TOML escape hatch). reddit_user_comments deliberately stays out since comments don't carry images.
list_ranking — top-N ranking, shipped in feat(render): list_ranking renderer for Bars shape #125. Sorts Bars descending, prints <rank> <label> <value> rows with rank-prefix and value columns aligned across rows. style = "number" | "medal" | "none" selects the prefix glyph (medal decorates 🥇/🥈/🥉 for the top three, falls back to numeric for 4th+). Reuses existing RenderOptions (style / max_items / align) — no new fields, no shape changes. Delta marker (▲ +3) deferred: would need a delta field on BarsData::Bar, follow-up if/when fetchers want period-over-period.
list_metricsbuild — a stat table where each row carries label + current value + an inline mini-sparkline of recent history. The canonical ops-dashboard row. Needs a new MetricRows { items: [{label, value, series: Vec<f64>}] } shape. Consumers: system_monitor_* rollups, git_churn, package-download trends.
chart_histogram — value distribution. Consumes NumberSeries. Complement to chart_sparkline (sequence → distribution). Shipped in feat(render): add chart_histogram #122. Stretches bar width to fill the slot, lifts long-tail bins above ratatui's sub-cell render threshold, and supports axis_labels = true for min / max / peak overlays.
chart_radarbuild — multi-axis comparison. New shape Radar { axes: Vec<String>, series: Vec<Vec<f64>> }. Language breakdown, per-core CPU, skill charts.
chart_tree_mapbuild — area-weighted rectangles. Consumes Bars. Disk usage, repo language composition, dependency sizes.
chart_bulletbuild — target vs actual. New shape Bullet { value: f64, target: f64, ranges: Vec<f64> }. SLO / OKR progress.
chart_candlestickbuild — OHLC. New shape Candles { items: Vec<{time, open, high, low, close}> }. Stock prices, CI timing min/max/median.
chart_word_cloudbuild — tag / keyword cloud sized by frequency. New shape WordCloud { items: Vec<{word, weight, style?}> }. Moved from text_* — output is size-weighted layout, not line text.
chart_waterfallbuild — running-total bars showing per-step contribution to a final value (up bars green, down bars red, total bar grey). Performance regression breakdown (PR-by-PR ms delta), monthly budget contribution, PR-by-PR LOC delta. Consumes the same SignedBars shape as chart_diverging_bars (running-total semantics is the differentiator — bars are sequenced and accumulate).
chart_diverging_barsbuild — diverging centered bars on a single signed-value axis. Two visual orientations:
orientation = "vertical" (default for paired data) — population-pyramid: same-category mirrored bars left vs right. Languages added vs deleted, PRs opened vs closed.
Absorbs the previously drafted chart_tornado and chart_population_pyramid — the visual difference is one option, not two renderers. Needs new SignedBars { items: Vec<{label, value: f64}> } shape (shared with chart_waterfall).
chart_slopebuild — 2-column slope chart (left col → right col with connecting lines), good for before/after value comparison ("this month vs last month", language rank diff after a refactor). Evaluate overlap with chart_bumps first — bumps generalises slope to N rounds, slope might collapse into chart_bumps with rounds.len() == 2. Needs new Slope { left: Vec<{label, value}>, right: Vec<{label, value}> } shape if it stays standalone.
chart_bumpsbuild — multi-round rank-over-time spaghetti (chained slope chart). F1 standings progression, repo star ranking over weeks, top-N project ranking. Distinct from chart_line because of explicit rank semantics (Y axis is rank, lines cross when ranks swap) and per-line label decoration at each round. Needs new Bumps { rounds: Vec<String>, series: Vec<{label, ranks: Vec<u32>}> } shape.
chart_radial_dialbuild — 24h circular timeline (clock-face) with event dots placed by time-of-day. Daily event distribution, PR-merged-at distribution. Consumes Timeline. Shares clock-face geometry with media_analog_clock; factor a common helper rather than duplicating.
chart_boxbuild — box plot (min / Q1 / median / Q3 / max + outliers). Consumes NumberSeries. CI duration distribution (p50/p95), benchmark variance, request latency distribution. Distinct primitive from chart_histogram (binned distribution) and chart_violin (KDE shape, planned as a chart_histogramsmoothing mode rather than a separate renderer).
chart_sunburstbuild — multi-level radial hierarchy (concentric rings, inner = parent). Disk usage by nested directory, dependency tree by size. Consumes the same hierarchical Tree shape as chart_tree_map — evaluate whether it's a chart_tree_map { layout = "radial" } mode vs a sibling renderer first.
chart_dumbbellbuild — one row per category with two dots joined by a line (before / after, min / max, you vs team). Evaluate overlap with chart_slope first — slope connects two columns, dumbbell connects two points per row; might collapse into chart_slope { layout = "rows" } over the same Slope shape.
gauge_*
gauge_circle — circular gauge
gauge_line — single-metric bar
gauge_ringbuild — Apple activity-ring style circle. Evaluate overlap with existing gauge_circle first.
gauge_thermometer — vertical mercury-tube gauge for Ratio, shipped in feat(render): add gauge_thermometer vertical mercury-tube renderer #126. Tone follows the gauge_battery / gauge_segment neutral / fill / drain semantics. Falls back to a plain fill column when the slot is too narrow for tube + side labels; degrades to top-cap + bulb only at h=2. Unit ticks deferred — can land as a follow-up if a fetcher needs them.
gauge_speedometerbuild — half-circle dial with a needle pointing to the value position. Showy variant of gauge_circle for percentage / throttle / RPM-like readings. Consumes Ratio. Evaluate overlap with gauge_circle / gauge_ring first — three Ratio gauges in the circular family is a lot; consider whether this can be a gauge_circlestyle = "speedometer" mode (half-circle + needle) instead of a sibling renderer.
gauge_compassbuild — N/E/S/W compass dial with a directional indicator. Wind direction, magnetic bearing, optional magnitude as inner ring fill. Needs new Bearing { degrees: f64, magnitude: Option<f64> } shape.
gauge_ratingbuild — discrete star / pip rating (★★★☆☆). Consumes Ratio mapped onto N pips (default 5). Package score, code-health grade, review rating. Evaluate whether it's just gauge_segment { glyph = "star" } before adding a sibling.
gauge_stepsbuild — linear labeled progress stepper (① ─ ② ─ ③ ─ ○ ─ ○ with stage names). Deploy pipeline stages, onboarding progress, a release checklist as a flow. Distinct from status_traffic_light (stacked independent badges) and grid_kanban (columns). Needs a Steps { stages: Vec<{label, state}>, current: usize } shape.
grid_*
grid_table — key/value rows
grid_calendar — month grid (today highlight)
grid_heatmap — 2D intensity grid, shipped in feat(render): heatmap renderer for contribution grids #42. GitHub light-mode palette (pending theme migration in feat: Theme / color scheme system #17). Body::Heatmap { cells, thresholds, row_labels, col_labels }; 5-level intensity, auto-quartile or explicit thresholds, right-aligns the window when the slot is narrower than the data, optional top-row month labels, honours align for horizontal placement.
grid_kanbanbuild — column board layout (To Do / Doing / Done, or arbitrary column titles) with item cards under each column header. Consumes Linear / Todoist / Jira ticket lists by status. Needs new KanbanBoard { columns: Vec<{title: String, items: Vec<String>}> } shape.
status_*
status_badge — single-status traffic light (green/red/yellow), shipped in feat(render): add badge renderer #46. Used by CI, deploy, SLO, oncall.
status_rowbuild — row of status_badges. Probably nested layout rather than a dedicated renderer.
status_traffic_lightbuild — multiple stacked badges at a glance (CI + deploy + SLO in one widget). New shape BadgeGroup { items: Vec<BadgeData> }.
status_dotsbuild — recent N-state history dots (e.g., last 10 CI runs as coloured dots). New shape StatusHistory { states: Vec<StatusLevel> }.
status_pulsebuild — animated ECG-style heartbeat trace. Service-alive indicator for uptime / oncall widgets — visual "still beating" cue distinct from a static badge. Animates within ANIMATION_WINDOW. Consumes Text (label) or Ratio (frequency).
media_*
media_image — PNG/JPEG via ratatui-image
media_ansiwrap — render hand-authored colored art verbatim. Wrap ansi-to-tui (converts ANSI strings to ratatui::text::Text).
media_pixel — shipped in feat(render,weather): PixelArt shape + media_pixel + weather sprites #260. Half-block + truecolor renderer for the new PixelArt shape; works in any 24-bit terminal (no kitty/sixel/iTerm2 dependency). Accepts align / max_width / max_height. Jagged rows surface an in-band error; zero-row grids fall through the centralized empty-state placeholder.
media_gradientbuild or wrap — color-gradient background for banner text. Potentially via tachyonfx or custom shader.
media_qrwrap — takes string, paints QR pattern. Wrap the qrcode crate + paint as block cells. Consumes Text. Moved from text_qr — output is block-cell drawing, not line text.
media_barcodewrap — barcode counterpart to QR. Consumes Text.
media_analog_clockbuild — takes a time, paints analog hands. Consumes Text (time string); a future dedicated Instant shape is possible but not required for a single-reader renderer. Previously drafted under a clock_* family, but splashboard's one-shot render model doesn't really support a "live ticking clock", and a single pictorial clock renderer doesn't justify its own family — media_* is the right bucket alongside media_image / media_qr.
media_videobuild — frame-based small video loop within the 2s ANIMATION_WINDOW. New shape Frames { paths: Vec<PathBuf>, fps: u32 }.
media_ascii_mapbuild — ASCII world / region map painted as block cells with markers placed by latitude/longitude. Remote-team locations, error-origin map, server-region map, earthquake / news geographic feeds. Needs new GeoPoints { points: Vec<{lat: f64, lon: f64, label: Option<String>, intensity: Option<f64>}>, region: Option<String> } shape.
media_braillebuild — render an image as 2×4 braille dots: monochrome but high-resolution, and works on terminals with no sixel / kitty / iTerm graphics protocol. The universal fallback to media_image. Consumes Image; evaluate whether it's a media_image { mode = "braille" } option vs a sibling renderer.
animated_*
animated_typewriter — char-by-char reveal (equivalent to TTE's print effect; will be absorbed into the animated_reveal family)
animated_reveal ⭐ build — TTE-style reveal animations where the final text emerges from a particle-like animation. Fits the 2s ANIMATION_WINDOW. Not expressible via tachyonfx's buffer-post-process paradigm.
Covers: fade, slide, sweep_in, dissolve, glitch, hsl_shift, translate, resize, shader_fn, and their compositions (repeat, ping_pong, chain).
Shape: one renderer animated_postfx + effect = "..." option.
Effect candidates to add (no new renderer needed — these are effect values on this renderer): aurora (gradient sweep via hsl_shift + sweep), plasma (sine-sum shader_fn over cell coords), breath (slow fadeping_pong). Same logic as animated_glitch — only split into a sibling renderer if the option surface gets unwieldy for a specific effect family.
animated_skeletonwrap — pulse/sweep/shimmer placeholder for widgets that haven't returned yet. Wrap tui-skeleton.
animated_spinnerwrap — throbber for pending network widgets. Wrap throbber-widgets-tui.
animated_marqueebuild — scrolling text for long titles. Consumes Text / TextBlock.
animated_flipbuild — digit / character flip reveal animation. Consumes Text / TextBlock. Absorbs the previously drafted clock_flip use-case — the value is in the flip effect itself, not the clock-ness.
animated_glitchwrap — glitch-focused variant of animated_postfx. Split out if the postfx effect = "..." option surface gets too generic.
animated_countbuild — a number rolls up from 0 (or from a cached previous value) to its final value within ANIMATION_WINDOW, then freezes. Reveal-to-final-state, so it fits the one-shot model. Star counts, LOC, downloads, revenue. Likely folds into animated_flip { mode = "roll" } rather than a sibling — note the overlap.
Adding a shape is a 5-touch-point change (Body variant, Shape enum, shape_of(), default_renderer_for(), renderer accepts()). Grouped here so PRs can batch them cleanly rather than overloading Entries:
clock_* renderer family — dropped. Splashboard renders one-shot (ANIMATION_WINDOW = 2s, then the buffer freezes), so a "live ticking clock" renderer fundamentally doesn't fit the model. clock_analog is kept as media_analog_clock (one-shot pictorial render), and the previously drafted clock_flip is superseded by the generic animated_flip effect.
text_big_number — dropped. The "huge number with a delta" use case is already covered by the existing text_ascii renderer (figlet-style big text via tui-big-text): a fetcher can emit "⭐ 142 (+3)" as Text and text_ascii renders it big. A dedicated renderer would only have justified its name with a separate visual treatment for the delta line, which doesn't carry its weight as a stand-alone primitive.
chart_horizon / chart_step / chart_violin / chart_polar_area / chart_punch_card / status_eq_bars — collapsed into options on existing renderers rather than sibling renderers, because they share the consumed shape with a sibling and only differ visually:
chart_violin → chart_histogram { smoothing = "kde" } (KDE-smoothed distribution on NumberSeries).
chart_polar_area → chart_pie { mode = "rose" } (Nightingale rose: equal slice angles, value as radius).
chart_punch_card → grid_heatmap { cell = "dot" } (size-encoded intensity dots already planned as a heatmap option).
status_eq_bars → chart_sparkline { mode = "bars", animated = true } (audio-EQ idiom on existing bars sparkline).
General principle: when a candidate consumes the same Shape as a sibling and the difference is purely visual, it's an option on the sibling renderer, not a new renderer name in the catalog.
chart_tornado / chart_population_pyramid — merged into chart_diverging_bars with orientation = "horizontal" | "vertical". The two were the same renderer with different orientation conventions on the same SignedBars shape; an option absorbs the difference without paying the catalog tax of two names.
animated_aurora / animated_plasma / animated_breath — folded into animated_postfx as effect values (aurora, plasma, breath). animated_postfx already wraps tachyonfx's full effect set (fade / hsl_shift / shader_fn / ping_pong / chain), so these are parameter values, not sibling renderers — same logic as the already-noted animated_glitch.
Renderer option coverage
Most renderers today read only align / style / pixel_size from RenderOptions (see src/render/mod.rs::RenderOptions). option_schemas() is wired into the docs generator (src/catalog.rs) and already used heavily by fetchers, but no renderer has populated it yet. That's a lot of low-risk expressive power sitting unclaimed.
Per-renderer option ideas (not a rename, just new optional fields):
list_plain — marker = "•" | "-" | "1." | "none", highlight_first = true, separator = "…", max_items = N (truncate with +3 more footer).
chart_line / chart_scatter — grid = true, axis_labels = true, series_colors = [...], smoothing = "none" | "spline", interpolation = "linear" | "step" (absorbs the previously drafted chart_step — staircase line for state transitions and CDFs is a chart_line mode, not a sibling renderer).
chart_sparkline — mode = "bars" | "dots" | "braille" | "horizon", baseline = "min" | "zero", animated = true (peak-hold VU style). mode = "horizon" absorbs the previously drafted chart_horizon (folded color-band time-series compression) — the horizon idiom is a sparkline rendering mode for the same NumberSeries shape, not a sibling renderer. animated = true covers the previously drafted status_eq_bars (audio-EQ idiom) on mode = "bars".
chart_histogram — smoothing = "none" | "kde" (KDE-smoothed = violin shape; absorbs the previously drafted chart_violin — both consume NumberSeries and visualize distribution shape, the only difference is the smoothing kernel).
Scoping: these land per-renderer as small PRs. The schema lives next to the renderer (OPTION_SCHEMAS: &[OptionSchema]), the render() body reads from RenderOptions, and xtask docs regenerate automatically.
Tracks all renderer candidates for splashboard. Part of the widget catalog index in #41.
What a renderer is
See #41 and
AGENTS.mdfor the full architecture. In short: a renderer consumes aPayload+RenderOptionsand draws into a ratatui frame. It declaresname(),accepts()(whichShapevariants it can render),animates(), andrender(). One shape can feed multiple renderers.Before building a renderer from scratch, check awesome-ratatui. Many primitive gaps already have community crates that we can wrap with a thin
Rendererimpl — a few lines mappingPayload→ widget input. Each gap below notes candidate crates where relevant.Two paradigms of "animation" are worth distinguishing, since different libraries serve each:
ansi-to-tuicovers ANSI passthrough as a separate concern.Naming conventions
Renderer names are user-facing (they appear in
render = "..."config) so drift is expensive to undo. Locking in the rules here so new renderers don't add fresh inconsistencies:chart_bar/chart_line/chart_pie, notbar_chart/line_chart/pie_chart. Prefix form is sortable (all siblings cluster), autocomplete-friendly, and consistent with theclock_*/system_*/git_*/github_*fetchers. No bare names — every renderer belongs to exactly one family.text_*— rich single-glyph-per-cell text.text_plain,text_ascii,text_markdown, futuretext_diff.list_*— multi-row lists.list_plain,list_timeline, futurelist_tree,list_checklist,list_ranking.chart_*— data charts.chart_bar,chart_line,chart_pie,chart_scatter,chart_sparkline, futurechart_stacked_bar,chart_area,chart_histogram,chart_radar,chart_tree_map,chart_bullet,chart_candlestick,chart_gantt,chart_funnel,chart_word_cloud,chart_waterfall,chart_slope,chart_bumps,chart_diverging_bars,chart_radial_dial,chart_box.gauge_*— ratio indicators.gauge_circle,gauge_line, futuregauge_ring,gauge_battery,gauge_thermometer,gauge_segment,gauge_speedometer,gauge_compass.grid_*— 2D cell grids.grid_table,grid_heatmap,grid_calendar, futuregrid_kanban.status_*— single-value status display.status_badge, futurestatus_row,status_traffic_light,status_dots,status_pulse.media_*— raster / ANSI / pictorial renderers.media_image, futuremedia_ansi,media_pixel,media_gradient,media_qr,media_barcode,media_analog_clock,media_video,media_ascii_map.animated_*— animation effects.animated_typewriter, futureanimated_reveal,animated_postfx,animated_skeleton,animated_spinner,animated_marquee,animated_flip,animated_glitch.Catalog
[x]shipped (code insrc/render/),[ ]planned. Planned entries are annotated withwrap(existing ecosystem crate) orbuild(from-scratch). Sorted by family to match the naming convention above.text_*text_plain— plain-text lines (acceptsText+TextBlock)text_ascii— figlet-style big text via tui-big-text / figlet-rstext_markdown— render Markdown via tui-markdown. Shipped in feat(render): add text_markdown renderer + MarkdownTextBlock shape #151. Introduced a dedicatedMarkdownTextBlockshape (rather than reusingText/TextBlock) so a fetcher emitting it commits to "this string is Markdown source" —Textpayloads can't accidentally route here, andtext_plaincan't be asked to render Markdown verbatim. Theme integration viaSplashStyleSheet: headings →panel_title, code/links →accent_event, blockquote →text_secondary, heading_meta →text_dim. Syntect highlight intentionally disabled (tui-markdowndefault-features = false) so fenced code blocks stay on theme tokens. Currently onlybasic_staticemits the shape; richer fetcher consumers (README excerpts, release notes) deferred to follow-up. Side fix:default_cache_keynow includesctx.shapeso multi-shape fetchers don't cache-poison across shape variants.text_diffwrap— highlighted unified diff (last commit, staged changes). Wrap syntect or paint inline with ratatuiText+ red/green line styling.list_*list_plain— bulleted/styled entrieslist_timeline— time-prefixed event list ("3h ago: merged feat(render): heatmap renderer for contribution grids #42") with relativeNm/Nh/Nd/Nwlabels, shipped in feat(render): add timeline renderer for timestamped event lists #48.list_links— clickable rows over the newLinkedTextBlockshape, shipped in feat: LinkedTextBlock shape with clickable rows (OSC 8) + hn_top fetcher #148. Each row whoseurlisSome(_)is wrapped in OSC 8 escape sequences so modern terminals (iTerm2, kitty, WezTerm, recent Windows Terminal, …) surface it as a hyperlink; rows without a URL render as plain text. Implementation note: the OSC 8 sequence is collapsed into the row's first cell withskip = trueon the trailing cells — without that, ratatui'sBuffer::diffcounts printable bytes inside the escape sequence as visible columns and silently drops the cell after the link.list_treewrap— nested structures (todo nesting, folder view). Wrap tui-tree-widget.list_checklistbuild— checkbox items with done/pending state. New shapeChecklist { items: Vec<{label, done}> }. Todos, release checklists, pre-flight checks.list_cards— card rows over the newImageLinkedListshape, shipped in feat(render): add list_cards renderer + ImageLinkedList shape #217. Each item is{title, url?, thumbnail_path?, subtitle?}; rows withurlget the same OSC 8 wrap aslist_links(via a sharedpub(crate) wrap_osc8helper), rows without athumbnail_pathkeep a blank cell so the text column stays column-aligned. Options:thumbnail_width(default 6 cells),row_height(default 3),gap(default 1),fit(contain/cover/stretch, mirrorsmedia_image). Reusesmedia_image::draw_thumbnail(extracted topub(crate)) so theratatui-imagepipeline isn't duplicated. Remote thumbnail URLs route through a sharedfetcher::thumbnails::download_to_cachethat caches to$HOME/.splashboard/cache/thumbnails/<sha256>.<ext>with magic-byte sniffing and a 4 MB cap; per-row failures collapse toNoneso a single broken image doesn't break a feed. Fetcher consumers shipped in the same PR:rss(extractsmedia:thumbnail→media:content→ first HTTP(S)<img>in<content>/<summary>with HTML-entity decoding),reddit_subreddit_posts/reddit_user_posts(RedditthumbnailURL, filtering placeholder stringsself/default/nsfw/spoiler),wikipedia_featured/wikipedia_random(REST APIthumbnail.source), andbasic_read_store(JSON/TOML escape hatch).reddit_user_commentsdeliberately stays out since comments don't carry images.list_ranking— top-N ranking, shipped in feat(render): list_ranking renderer for Bars shape #125. SortsBarsdescending, prints<rank> <label> <value>rows with rank-prefix and value columns aligned across rows.style = "number" | "medal" | "none"selects the prefix glyph (medaldecorates 🥇/🥈/🥉 for the top three, falls back to numeric for 4th+). Reuses existingRenderOptions(style/max_items/align) — no new fields, no shape changes. Delta marker (▲ +3) deferred: would need adeltafield onBarsData::Bar, follow-up if/when fetchers want period-over-period.list_metricsbuild— a stat table where each row carries label + current value + an inline mini-sparkline of recent history. The canonical ops-dashboard row. Needs a newMetricRows { items: [{label, value, series: Vec<f64>}] }shape. Consumers:system_monitor_*rollups,git_churn, package-download trends.chart_*chart_bar— horizontal barschart_line— time serieschart_pie— proportion sliceschart_scatter— pointschart_sparkline— compact trendchart_stacked_bar/chart_areawrap— trend primitive. Wrap ratatui-stacked-bar.chart_histogram— value distribution. ConsumesNumberSeries. Complement tochart_sparkline(sequence → distribution). Shipped in feat(render): add chart_histogram #122. Stretches bar width to fill the slot, lifts long-tail bins above ratatui's sub-cell render threshold, and supportsaxis_labels = truefor min / max / peak overlays.chart_radarbuild— multi-axis comparison. New shapeRadar { axes: Vec<String>, series: Vec<Vec<f64>> }. Language breakdown, per-core CPU, skill charts.chart_tree_mapbuild— area-weighted rectangles. ConsumesBars. Disk usage, repo language composition, dependency sizes.chart_bulletbuild— target vs actual. New shapeBullet { value: f64, target: f64, ranges: Vec<f64> }. SLO / OKR progress.chart_candlestickbuild— OHLC. New shapeCandles { items: Vec<{time, open, high, low, close}> }. Stock prices, CI timing min/max/median.chart_ganttbuild— milestone timeline. New shapeGantt { items: Vec<{label, start, end}> }. Release schedule, roadmap view.chart_funnelbuild— ordered conversion stages. ConsumesBars. PR funnel (opened → reviewed → merged), onboarding funnel.chart_word_cloudbuild— tag / keyword cloud sized by frequency. New shapeWordCloud { items: Vec<{word, weight, style?}> }. Moved fromtext_*— output is size-weighted layout, not line text.chart_waterfallbuild— running-total bars showing per-step contribution to a final value (up bars green, down bars red, total bar grey). Performance regression breakdown (PR-by-PR ms delta), monthly budget contribution, PR-by-PR LOC delta. Consumes the sameSignedBarsshape aschart_diverging_bars(running-total semantics is the differentiator — bars are sequenced and accumulate).chart_diverging_barsbuild— diverging centered bars on a single signed-value axis. Two visual orientations:orientation = "horizontal"— horizontal tornado: positive right, negative left. Sentiment, A/B lift.orientation = "vertical"(default for paired data) — population-pyramid: same-category mirrored bars left vs right. Languages added vs deleted, PRs opened vs closed.Absorbs the previously drafted
chart_tornadoandchart_population_pyramid— the visual difference is one option, not two renderers. Needs newSignedBars { items: Vec<{label, value: f64}> }shape (shared withchart_waterfall).chart_slopebuild— 2-column slope chart (left col → right col with connecting lines), good for before/after value comparison ("this month vs last month", language rank diff after a refactor). Evaluate overlap withchart_bumpsfirst — bumps generalises slope to N rounds, slope might collapse intochart_bumpswithrounds.len() == 2. Needs newSlope { left: Vec<{label, value}>, right: Vec<{label, value}> }shape if it stays standalone.chart_bumpsbuild— multi-round rank-over-time spaghetti (chained slope chart). F1 standings progression, repo star ranking over weeks, top-N project ranking. Distinct fromchart_linebecause of explicit rank semantics (Y axis is rank, lines cross when ranks swap) and per-line label decoration at each round. Needs newBumps { rounds: Vec<String>, series: Vec<{label, ranks: Vec<u32>}> }shape.chart_radial_dialbuild— 24h circular timeline (clock-face) with event dots placed by time-of-day. Daily event distribution, PR-merged-at distribution. ConsumesTimeline. Shares clock-face geometry withmedia_analog_clock; factor a common helper rather than duplicating.chart_boxbuild— box plot (min / Q1 / median / Q3 / max + outliers). ConsumesNumberSeries. CI duration distribution (p50/p95), benchmark variance, request latency distribution. Distinct primitive fromchart_histogram(binned distribution) andchart_violin(KDE shape, planned as achart_histogramsmoothingmode rather than a separate renderer).chart_sunburstbuild— multi-level radial hierarchy (concentric rings, inner = parent). Disk usage by nested directory, dependency tree by size. Consumes the same hierarchicalTreeshape aschart_tree_map— evaluate whether it's achart_tree_map { layout = "radial" }mode vs a sibling renderer first.chart_dumbbellbuild— one row per category with two dots joined by a line (before / after, min / max, you vs team). Evaluate overlap withchart_slopefirst — slope connects two columns, dumbbell connects two points per row; might collapse intochart_slope { layout = "rows" }over the sameSlopeshape.gauge_*gauge_circle— circular gaugegauge_line— single-metric bargauge_ringbuild— Apple activity-ring style circle. Evaluate overlap with existinggauge_circlefirst.gauge_battery— battery-icon fill, shipped in feat(render): add gauge_battery renderer for Ratio shape #113. ConsumesRatio. Playful framing for laptop battery, quota, progress.gauge_thermometer— vertical mercury-tube gauge forRatio, shipped in feat(render): add gauge_thermometer vertical mercury-tube renderer #126. Tone follows thegauge_battery/gauge_segmentneutral /fill/drainsemantics. Falls back to a plain fill column when the slot is too narrow for tube + side labels; degrades to top-cap + bulb only at h=2. Unit ticks deferred — can land as a follow-up if a fetcher needs them.gauge_segment— 5-LED segmented bar, shipped in feat(render): add gauge_segment renderer for Ratio shape #117. ConsumesRatio. Retro-hardware feel.gauge_speedometerbuild— half-circle dial with a needle pointing to the value position. Showy variant ofgauge_circlefor percentage / throttle / RPM-like readings. ConsumesRatio. Evaluate overlap withgauge_circle/gauge_ringfirst — three Ratio gauges in the circular family is a lot; consider whether this can be agauge_circlestyle = "speedometer"mode (half-circle + needle) instead of a sibling renderer.gauge_compassbuild— N/E/S/W compass dial with a directional indicator. Wind direction, magnetic bearing, optional magnitude as inner ring fill. Needs newBearing { degrees: f64, magnitude: Option<f64> }shape.gauge_ratingbuild— discrete star / pip rating (★★★☆☆). ConsumesRatiomapped onto N pips (default 5). Package score, code-health grade, review rating. Evaluate whether it's justgauge_segment { glyph = "star" }before adding a sibling.gauge_stepsbuild— linear labeled progress stepper (① ─ ② ─ ③ ─ ○ ─ ○with stage names). Deploy pipeline stages, onboarding progress, a release checklist as a flow. Distinct fromstatus_traffic_light(stacked independent badges) andgrid_kanban(columns). Needs aSteps { stages: Vec<{label, state}>, current: usize }shape.grid_*grid_table— key/value rowsgrid_calendar— month grid (today highlight)grid_heatmap— 2D intensity grid, shipped in feat(render): heatmap renderer for contribution grids #42. GitHub light-mode palette (pending theme migration in feat: Theme / color scheme system #17).Body::Heatmap { cells, thresholds, row_labels, col_labels }; 5-level intensity, auto-quartile or explicit thresholds, right-aligns the window when the slot is narrower than the data, optional top-row month labels, honoursalignfor horizontal placement.grid_kanbanbuild— column board layout (To Do / Doing / Done, or arbitrary column titles) with item cards under each column header. Consumes Linear / Todoist / Jira ticket lists by status. Needs newKanbanBoard { columns: Vec<{title: String, items: Vec<String>}> }shape.status_*status_badge— single-status traffic light (green/red/yellow), shipped in feat(render): add badge renderer #46. Used by CI, deploy, SLO, oncall.status_rowbuild— row ofstatus_badges. Probably nested layout rather than a dedicated renderer.status_traffic_lightbuild— multiple stacked badges at a glance (CI + deploy + SLO in one widget). New shapeBadgeGroup { items: Vec<BadgeData> }.status_dotsbuild— recent N-state history dots (e.g., last 10 CI runs as coloured dots). New shapeStatusHistory { states: Vec<StatusLevel> }.status_pulsebuild— animated ECG-style heartbeat trace. Service-alive indicator for uptime / oncall widgets — visual "still beating" cue distinct from a static badge. Animates withinANIMATION_WINDOW. ConsumesText(label) orRatio(frequency).media_*media_image— PNG/JPEG via ratatui-imagemedia_ansiwrap— render hand-authored colored art verbatim. Wrap ansi-to-tui (converts ANSI strings toratatui::text::Text).media_pixel— shipped in feat(render,weather): PixelArt shape + media_pixel + weather sprites #260. Half-block + truecolor renderer for the newPixelArtshape; works in any 24-bit terminal (no kitty/sixel/iTerm2 dependency). Acceptsalign/max_width/max_height. Jagged rows surface an in-band error; zero-row grids fall through the centralized empty-state placeholder.media_gradientbuildorwrap— color-gradient background for banner text. Potentially via tachyonfx or custom shader.media_qrwrap— takes string, paints QR pattern. Wrap theqrcodecrate + paint as block cells. ConsumesText. Moved fromtext_qr— output is block-cell drawing, not line text.media_barcodewrap— barcode counterpart to QR. ConsumesText.media_analog_clockbuild— takes a time, paints analog hands. ConsumesText(time string); a future dedicatedInstantshape is possible but not required for a single-reader renderer. Previously drafted under aclock_*family, but splashboard's one-shot render model doesn't really support a "live ticking clock", and a single pictorial clock renderer doesn't justify its own family —media_*is the right bucket alongsidemedia_image/media_qr.media_videobuild— frame-based small video loop within the 2s ANIMATION_WINDOW. New shapeFrames { paths: Vec<PathBuf>, fps: u32 }.media_ascii_mapbuild— ASCII world / region map painted as block cells with markers placed by latitude/longitude. Remote-team locations, error-origin map, server-region map, earthquake / news geographic feeds. Needs newGeoPoints { points: Vec<{lat: f64, lon: f64, label: Option<String>, intensity: Option<f64>}>, region: Option<String> }shape.media_braillebuild— render an image as 2×4 braille dots: monochrome but high-resolution, and works on terminals with no sixel / kitty / iTerm graphics protocol. The universal fallback tomedia_image. ConsumesImage; evaluate whether it's amedia_image { mode = "braille" }option vs a sibling renderer.animated_*animated_typewriter— char-by-char reveal (equivalent to TTE'sprinteffect; will be absorbed into theanimated_revealfamily)animated_reveal⭐build— TTE-style reveal animations where the final text emerges from a particle-like animation. Fits the 2s ANIMATION_WINDOW. Not expressible via tachyonfx's buffer-post-process paradigm.matrix,beams,decrypt,burn,fireworks,blackhole,scattered,spray,rings,waves,wipe,pour,swarm,synthgrid,unstable.animated_typewriteris TTE'sprinteffect; absorb into this family.matrixspecifically can start from wrapping tui-rain (partial coverage).animated_reveal+effect = "..."option.animated_postfxwrap— apply shader-like effects over already-rendered widgets. Fits the 2s ANIMATION_WINDOW (no separate screensaver mode needed). Shipped in feat(render): add animated_postfx renderer #74 with fade_in / fade_out / dissolve / coalesce / sweep_in{,_right,_down,_up} / slide_in{,_right,_down,_up} / hsl_shift — foreground-only variants (pattern-based for directional effects) so the terminal background shows through untouched.animated_postfx+effect = "..."option.effectvalues on this renderer):aurora(gradient sweep viahsl_shift+sweep),plasma(sine-sumshader_fnover cell coords),breath(slowfadeping_pong). Same logic asanimated_glitch— only split into a sibling renderer if the option surface gets unwieldy for a specific effect family.animated_skeletonwrap— pulse/sweep/shimmer placeholder for widgets that haven't returned yet. Wrap tui-skeleton.animated_spinnerwrap— throbber for pending network widgets. Wrap throbber-widgets-tui.animated_marqueebuild— scrolling text for long titles. ConsumesText/TextBlock.animated_flipbuild— digit / character flip reveal animation. ConsumesText/TextBlock. Absorbs the previously draftedclock_flipuse-case — the value is in the flip effect itself, not the clock-ness.animated_glitchwrap— glitch-focused variant ofanimated_postfx. Split out if the postfxeffect = "..."option surface gets too generic.animated_countbuild— a number rolls up from 0 (or from a cached previous value) to its final value withinANIMATION_WINDOW, then freezes. Reveal-to-final-state, so it fits the one-shot model. Star counts, LOC, downloads, revenue. Likely folds intoanimated_flip { mode = "roll" }rather than a sibling — note the overlap.Cross-cutting
render_payloadshort-circuits any empty body (shipped feat: $HOME/.splashboard/ home-dir layout + ReadStore fetcher #43) to a centered "nothing here yet" glyph so widgets stay visible while data hasn't landed yet.New shapes introduced by planned renderers
Adding a shape is a 5-touch-point change (
Bodyvariant,Shapeenum,shape_of(),default_renderer_for(), rendereraccepts()). Grouped here so PRs can batch them cleanly rather than overloadingEntries:Radar { axes, series }chart_radarBullet { value, target, ranges }chart_bulletCandles { items: [{time, open, high, low, close}] }chart_candlestickGantt { items: [{label, start, end}] }chart_ganttWordCloud { items: [{word, weight, style?}] }chart_word_cloudChecklist { items: [{label, done}] }list_checklistBadgeGroup { items: Vec<BadgeData> }status_traffic_lightStatusHistory { states: Vec<StatusLevel> }status_dotsFrames { paths, fps }media_videoSignedBars { items: [{label, value: f64}] }chart_waterfall/chart_diverging_barsSlope { left: [{label, value}], right: [{label, value}] }chart_slope(might collapse intoBumpswithrounds.len() == 2)Bumps { rounds: Vec<String>, series: [{label, ranks: Vec<u32>}] }chart_bumpsBearing { degrees, magnitude? }gauge_compassKanbanBoard { columns: [{title, items: Vec<String>}] }grid_kanbanGeoPoints { points: [{lat, lon, label?, intensity?}], region? }media_ascii_mapImageLinkedList { items: [{title, url?, thumbnail_path?, subtitle?}] }list_cardsReused existing shapes (no new shape needed):
Bars→chart_tree_map/chart_funnel/list_ranking;Ratio→gauge_battery/gauge_thermometer/gauge_segment/gauge_speedometer(under overlap review withgauge_circle) /status_pulse(frequency);NumberSeries→chart_histogram/chart_box;Timeline→chart_radial_dial;Text/TextBlock→media_qr/media_barcode/media_analog_clock/animated_marquee/animated_flip/status_pulse(label).Rejected / dropped renderer ideas
clock_*renderer family — dropped. Splashboard renders one-shot (ANIMATION_WINDOW = 2s, then the buffer freezes), so a "live ticking clock" renderer fundamentally doesn't fit the model.clock_analogis kept asmedia_analog_clock(one-shot pictorial render), and the previously draftedclock_flipis superseded by the genericanimated_flipeffect.text_big_number— dropped. The "huge number with a delta" use case is already covered by the existingtext_asciirenderer (figlet-style big text viatui-big-text): a fetcher can emit"⭐ 142 (+3)"asTextandtext_asciirenders it big. A dedicated renderer would only have justified its name with a separate visual treatment for the delta line, which doesn't carry its weight as a stand-alone primitive.chart_horizon/chart_step/chart_violin/chart_polar_area/chart_punch_card/status_eq_bars— collapsed into options on existing renderers rather than sibling renderers, because they share the consumed shape with a sibling and only differ visually:chart_horizon→chart_sparkline { mode = "horizon" }(folded color-band time-series compression onNumberSeries).chart_step→chart_line { interpolation = "step" }(staircase interpolation onPointSeries).chart_violin→chart_histogram { smoothing = "kde" }(KDE-smoothed distribution onNumberSeries).chart_polar_area→chart_pie { mode = "rose" }(Nightingale rose: equal slice angles, value as radius).chart_punch_card→grid_heatmap { cell = "dot" }(size-encoded intensity dots already planned as a heatmap option).status_eq_bars→chart_sparkline { mode = "bars", animated = true }(audio-EQ idiom on existing bars sparkline).General principle: when a candidate consumes the same
Shapeas a sibling and the difference is purely visual, it's an option on the sibling renderer, not a new renderer name in the catalog.chart_tornado/chart_population_pyramid— merged intochart_diverging_barswithorientation = "horizontal" | "vertical". The two were the same renderer with different orientation conventions on the sameSignedBarsshape; an option absorbs the difference without paying the catalog tax of two names.animated_aurora/animated_plasma/animated_breath— folded intoanimated_postfxaseffectvalues (aurora,plasma,breath).animated_postfxalready wraps tachyonfx's full effect set (fade / hsl_shift / shader_fn / ping_pong / chain), so these are parameter values, not sibling renderers — same logic as the already-notedanimated_glitch.Renderer option coverage
Most renderers today read only
align/style/pixel_sizefromRenderOptions(seesrc/render/mod.rs::RenderOptions).option_schemas()is wired into the docs generator (src/catalog.rs) and already used heavily by fetchers, but no renderer has populated it yet. That's a lot of low-risk expressive power sitting unclaimed.Per-renderer option ideas (not a rename, just new optional fields):
marker = "•" | "-" | "1." | "none",highlight_first = true,separator = "…",max_items = N(truncate with+3 morefooter).relative_labels = true,max_events = N,show_time_column = true.border = "none" | "ascii" | "rounded",show_header = true,key_style = "bold",align_values = "left" | "right".palette = "github" | "viridis" | "warm"(also blocks on feat: Theme / color scheme system #17 theme work),cell = "block" | "dot"(thedotmode with size-encoded intensity = the GitHub punch-card visualisation; absorbs the previously draftedchart_punch_card— punch-card is a heatmap rendering mode, not a sibling renderer),show_labels = true.first_day = "sun" | "mon",range = "month" | "week",marker_style = "dot" | "badge".orientation = "horizontal" | "vertical",value_label = "inside" | "outside" | "hidden",max_bars = N.grid = true,axis_labels = true,series_colors = [...],smoothing = "none" | "spline",interpolation = "linear" | "step"(absorbs the previously draftedchart_step— staircase line for state transitions and CDFs is achart_linemode, not a sibling renderer).mode = "bars" | "dots" | "braille" | "horizon",baseline = "min" | "zero",animated = true(peak-hold VU style).mode = "horizon"absorbs the previously draftedchart_horizon(folded color-band time-series compression) — the horizon idiom is a sparkline rendering mode for the sameNumberSeriesshape, not a sibling renderer.animated = truecovers the previously draftedstatus_eq_bars(audio-EQ idiom) onmode = "bars".donut = true(inner-radius hole),legend = "right" | "below" | "none",show_percentages = true,mode = "slice" | "rose"(Nightingale rose: equal slice angles, value encoded as radius — absorbs the previously draftedchart_polar_area).smoothing = "none" | "kde"(KDE-smoothed = violin shape; absorbs the previously draftedchart_violin— both consumeNumberSeriesand visualize distribution shape, the only difference is the smoothing kernel).label_position = "inside" | "outside",color_thresholds = [0.5, 0.8],unit = "%" | "MB" | "…".shape = "dot" | "pill" | "square",show_label = true.fit = "contain" | "cover" | "stretch",dither = "floyd-steinberg" | "none".style/pixel_size; expose them throughoption_schemas()so docs pick them up.speed_ms = N,cursor = "_" | "|" | "none".Scoping: these land per-renderer as small PRs. The schema lives next to the renderer (
OPTION_SCHEMAS: &[OptionSchema]), therender()body reads fromRenderOptions, andxtaskdocs regenerate automatically.