xtop-widget-api (api/crates/widget-api/) is the renderer contract for
widget packs (DR-2). Widgets are the visual parts drawn inside layout areas:
the kernel ships a base pack, and any other pack (blocks pack, community,
plugin) provides renderers for the same widget names or new ones.
Three pieces make up the crate:
state::WidgetState— the read-only view a renderer receives;glyph— the glyph-style enums plus the canonical mappings to ratatui drawing primitives;renderer::{WidgetRegistration, WidgetRenderer}— how packs register renderers by widget name.
The crate root re-exports WidgetState, WidgetRegistration,
WidgetRenderer and the glyph enums (ChartCharset, WidgetBorders);
the glyph mapping helpers are reachable only under the glyph module
(xtop_widget_api::glyph::to_color etc. — decision D5).
Like every contract crate, it never depends on the kernel. It depends on
ratatui (for Frame, Rect, Marker, border sets), serde (for the glyph
enums) and xtop-plugin-api (for the data model its WidgetState methods
name).
Every renderer draws against this trait — never against kernel types. The
kernel implements it over its live AppState (xtop/src/state/widget_state.rs)
and the render engine hands it to the pack renderers. Read-only by design:
widgets never mutate app state.
The trait's 28 methods, grouped as in the source:
| Group | Method | Signature | Meaning |
|---|---|---|---|
| Sample | snapshot |
fn snapshot(&self) -> Option<&SystemSnapshot> |
current per-tick sample; None before the first tick |
| Theme | theme_name |
fn theme_name(&self) -> &str |
active theme name |
| Theme | theme_fg |
fn theme_fg(&self) -> &[u8; 3] |
theme foreground as an RGB triple |
| Theme | theme_bg |
fn theme_bg(&self) -> &[u8; 3] |
theme background as an RGB triple |
| Theme | theme_palette |
fn theme_palette(&self) -> &[[u8; 3]; 16] |
16-entry palette |
| Theme | alerts |
fn alerts(&self) -> AlertThresholds |
current alert thresholds |
| Glyph style | charset |
fn charset(&self, widget: &str) -> ChartCharset |
resolved charset for a widget |
| Glyph style | borders |
fn borders(&self, widget: &str) -> WidgetBorders |
resolved border style for a widget |
| History | cpu_history |
fn cpu_history(&self) -> &[VecDeque<(f64, f64)>] |
per-core history (x, y) |
| History | mem_history |
fn mem_history(&self) -> &VecDeque<(f64, f64)> |
memory percent history |
| History | net_rx_history |
fn net_rx_history(&self) -> &VecDeque<(f64, f64)> |
summed receive rate history (bytes/s) |
| History | net_tx_history |
fn net_tx_history(&self) -> &VecDeque<(f64, f64)> |
summed transmit rate history (bytes/s) |
| History | disk_read_history |
fn disk_read_history(&self) -> &VecDeque<(f64, f64)> |
summed disk read rate history (bytes/s, aggregate across disks); default empty |
| History | disk_write_history |
fn disk_write_history(&self) -> &VecDeque<(f64, f64)> |
summed disk write rate history (bytes/s, aggregate across disks); default empty |
| History | load_history |
fn load_history(&self) -> &VecDeque<(f64, f64)> |
1-minute load-average history (load_avg.one); default empty |
| View/control | search_query |
fn search_query(&self) -> &str |
active process search text |
| View/control | process_selected_pid |
fn process_selected_pid(&self) -> Option<u32> |
selected row, anchored by PID |
| View/control | process_sort_label |
fn process_sort_label(&self) -> &str |
label of the active sort column |
| View/control | process_sort_desc |
fn process_sort_desc(&self) -> bool |
whether the active process sorting is descending; default false (ascending) reproduces the pre-direction behavior for implementors that do not track a direction |
| View/control | layout_name |
fn layout_name(&self) -> &str |
active layout name |
| View/control | is_searching |
fn is_searching(&self) -> bool |
search overlay active |
| View/control | fullscreen_label |
fn fullscreen_label(&self) -> Option<&str> |
fullscreen widget label, None when not fullscreen |
| View/control | sys_info |
fn sys_info(&self) -> SystemInfo |
owned machine identity copy |
| View/control | process_view |
fn process_view(&self) -> Vec<&ProcessInfo> |
process rows the processes widget draws |
| Process mapping | uid_to_name |
fn uid_to_name(&self, uid: u32) -> Option<String> |
login name for a numeric uid (kernel resolves from /etc/passwd on unix, plus Directory Services users via dscl on macOS and local accounts via Get-LocalUser on Windows, keyed by numeric uid); default None — renderers fall back to the numeric uid (UX9.1) |
| Process mapping | process_cpu_history |
fn process_cpu_history(&self, pid: u32) -> Vec<f64> |
recent per-process CPU-usage samples (percent of one logical core), oldest → newest, ~30 samples per pid; default empty — renderers draw nothing for an empty series (UX9.1) |
| Layout options | logical_core_count |
fn logical_core_count(&self) -> usize |
logical processors the host reports; default 1 reproduces the pre-DR-UX1 behavior for renderers that ignore it |
| Layout options | widget_options |
fn widget_options(&self) -> Option<&serde_json::Value> |
options object of the widget being rendered (None when the layout node carries none); default None; renderers must treat None as default behavior |
The five methods under "Layout options" and "Process mapping" (DR-UX1/UX3/
UX5/UX9.1 additions) come with default impls, so existing implementors
compile unchanged and output without options is byte-identical to the
pre-options behavior. logical_core_count lets a renderer normalize a
per-process (or per-core) CPU usage — a fraction of one logical core — into
a share of the whole machine's CPU (CpuBasis::Total display); the kernel
implements it from available_parallelism. widget_options carries the
per-instance display options from the layout node while that widget is
drawn; the value is never null, is only valid during the render call, and
unknown keys must be ignored. process_sort_desc feeds the processes
widget's direction marker (▼ descending / ▲ ascending) on the sorted
column header; the kernel returns its live cycle_sort flag. The three
History methods added for the UX8 data surface (disk_read_history,
disk_write_history, load_history) also ship with default empty
implementations: they are bounded histories the kernel feeds per tick
(aggregate disk rates in bytes/s and the 1-minute load average), and
implementors that do not track them yet keep compiling unchanged. The two
UX9.1 process helpers default to None / an empty series: uid_to_name
resolves a numeric uid to a login name (the uid→name mapping is a display
concern and deliberately lives on the state view, not in the data model —
renderers fall back to the numeric uid), and process_cpu_history returns
the bounded per-process CPU samples (oldest → newest) for a small braille
spark, empty for untracked pids.
Semantics worth noting:
- Glyph style is resolved: the kernel resolves per-widget overrides
against the global style (
UiStyle::charset_for/borders_forin the kernel config), so the renderer must not resolve anything itself — the contract already returned the effective choice for the named widget. - History is
(f64, f64)pairs — x (tick/elapsed coordinate) and y (value), the shape ratatuiChartdatasets consume. Network history tracks rates, not cumulative counters, so charts show throughput; the same holds for the disk read/write rate histories (aggregate bytes/s across disks).load_historytracks the 1-minute load average. All histories are bounded to the kernel'shistory_points. process_viewis the single per-tick sample filtered by the active search query and sorted by the user's chosen column; selection is anchored by PID so highlight and the kill action always agree on the same row.snapshotreturns a reference (Option<&SystemSnapshot>) — borrow the sample; never clone it per frame.sys_infois the one owned copy (cheap, slowly changing).
pub type WidgetRenderer =
Arc<dyn Fn(&mut Frame, &dyn WidgetState, Rect) + Send + Sync>;
pub struct WidgetRegistration {
pub name: String, // widget name as layouts use it
pub render: WidgetRenderer,
}A pack registers one renderer per widget name. WidgetRegistration is
the canonical registration type (DR-2; nothing outside widget-api defines
it — workspace grep gate M7.4). Packs today return their registry directly
as HashMap<&'static str, WidgetRenderer> (registry() in the widgets
repo); the engine resolves (pack, name) at render time.
Both enums live in glyph.rs, are re-exported at the crate root, and derive
Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize with
#[serde(rename_all = "snake_case")] — these are the values the kernel
persists in the user config (UiStyle/WidgetStyle in the kernel config
schema).
| Enum | Variants | Default | JSON (serde) |
|---|---|---|---|
ChartCharset |
Braille, Dot, Block, HalfBlock, Bar |
Braille |
"braille", "dot", "block", "half_block", "bar" |
WidgetBorders |
Native, Rounded, Double, Plain, Ascii |
Native |
"native", "rounded", "double", "plain", "ascii" |
Packs MUST NOT re-implement these mappings — they import them here, so the same configuration draws identically in every pack. The kernel only stores the contract enums; turning them into glyphs is this module's job. In-crate tests pin every row of the mapping tables below.
// The one canonical import path (helpers are not re-exported at the root):
use xtop_widget_api::glyph::{ASCII_BORDER, border_for, marker_for, to_color};pub fn to_color(palette_entry: [u8; 3]) -> ratatui::style::ColorReturns Color::Rgb(r, g, b) verbatim for the triple — palette entries are
already 24-bit RGB, so no quantization is applied. Note the by-value [u8; 3]
parameter (the packs' private copies take &[u8; 3]; call sites adapt by
dropping the reference when they migrate, M3).
pub const ASCII_BORDER: ratatui::symbols::border::Set<'static> = /* + - | frame */;
pub fn border_for(borders: WidgetBorders) -> ratatui::symbols::border::Set<'static>ratatui's symbols::border module no longer ships an ASCII set (its PLAIN
is the single-line box-drawing frame since ratatui 0.29), so the contract
provides the canonical one. Both Plain and Ascii map to it; packs must
not hand-roll their own copy. Mapping decisions (one border look per ratatui
set, no pack-specific divergence):
WidgetBorders |
border_for returns |
Look |
|---|---|---|
Native |
ratatui::symbols::border::PLAIN |
standard single-line box drawing (┌─┐│└┘), ratatui's own default set — the classic look |
Rounded |
border::ROUNDED |
box drawing with rounded corners |
Double |
border::DOUBLE |
double-line box drawing |
Plain |
ASCII_BORDER |
pure ASCII + - | frame |
Ascii |
ASCII_BORDER |
same frame (Ascii is an explicit config spelling of the same intent) |
ASCII_BORDER glyphs (coded exactly so, and asserted by test):
| Glyph | Value |
|---|---|
top_left |
"+" |
top_right |
"+" |
bottom_left |
"+" |
bottom_right |
"+" |
vertical_left |
"|" |
vertical_right |
"|" |
horizontal_top |
"-" |
horizontal_bottom |
"-" |
pub fn marker_for(charset: ChartCharset) -> ratatui::symbols::MarkerThe mapping mirrors the ratatui marker of the same name:
ChartCharset |
marker_for returns |
|---|---|
Braille |
Marker::Braille |
Dot |
Marker::Dot |
Block |
Marker::Block |
HalfBlock |
Marker::HalfBlock |
Bar |
Marker::Bar |
A pack that needs a different glyph for the same config must not re-implement the table; it diverges deliberately and documents why.
Before this contract existed, every pack re-implemented to_color,
border_for, marker_for and an ASCII set by hand — and the copies already
disagreed (the base pack mapped Plain to the box-drawing PLAIN, the
blocks pack mapped it to its own ASCII frame). The migration (widgets repo,
M3.3/M3.4):
- the base pack's
util.rsdeletes itsto_color/border_for/marker_for/ascii_bordercopies and imports the canonical ones; pack-private formatting helpers (format_bytes,format_uptime,gauge_gradient) stay pack-private where still used; - the blocks pack deletes its hand-rolled
to_color/ascii_border/border_for, drops itsconst _: ChartCharsetlint hack, and honorsstate.charset(widget)instead of hardcodingMarker::Block.
The Plain/Ascii divergence disappears by construction: both spellings
are ASCII here.
xtop-widget-api's WidgetRegistration draws over &dyn WidgetState — the
pack view. xtop-plugin-api's PluginWidget draws over &dyn HostState —
the plugin view (see plugin-contract.md). The two are
distinct contracts and deliberately do not share a name (M1.3). In the
kernel render engine the resolution order is: plugin widgets first, then
the user's chosen pack for the name, then the default pack (kernel
ui/layout/engine.rs + ui/screen.rs): plugin widgets can replace any
name.
A pack is a crate that returns a name → renderer registry. Widget renderers
are plain functions with the exact signature fn(&mut Frame, &dyn WidgetState, Rect):
use std::collections::HashMap;
use std::sync::Arc;
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders};
use ratatui::Frame;
use xtop_widget_api::glyph::{border_for, to_color};
use xtop_widget_api::{WidgetRenderer, WidgetState};
pub fn registry() -> HashMap<&'static str, WidgetRenderer> {
let mut m: HashMap<&'static str, WidgetRenderer> = HashMap::new();
m.insert("cpu", Arc::new(cpu::render));
m.insert("memory", Arc::new(memory::render));
m
}
pub mod cpu {
use super::*;
pub fn render(f: &mut Frame, state: &dyn WidgetState, area: Rect) {
let Some(snap) = state.snapshot() else { return }; // None before first tick
let fg = to_color(*state.theme_fg()); // canonical helper takes [u8; 3] by value
let bg = to_color(*state.theme_bg());
let block = Block::default()
.borders(Borders::ALL)
.border_set(border_for(state.borders("cpu")))
.style(Style::default().fg(fg).bg(bg));
f.render_widget(block, area);
// Draw into block.inner(area): gauges per core from `snap.cpus`,
// charts from state.cpu_history() with marker_for(state.charset("cpu")).
let _ = snap;
}
}Rules of the road for pack authors:
- Render only from
WidgetState; never name kernel types. The base pack (xtop-widgets) provides the classic names used by the default layouts —header,cpu,memory,storage,network,processes,disk_io,battery,gpu— and alternate packs may replace any of them by name. - Guard for
snapshot() == None(first tick) and for small/empty areas; renderers must never panic on small/empty state. - Use the canonical glyph helpers; any deliberate divergence must be documented (e.g. an alternate visual interpretation of a charset).
- Widget style choices come resolved from the contract
(
state.charset(name),state.borders(name)); do not resolve config yourself.