DocsxtopCustomization
kerneldocs/customization.mdrepo: xtop-cli/xtop

Customization Guide

xtop supports runtime customization of color themes and layout modes via external JSONC files. This guide explains how to create and manage your own themes and layouts.


Table of Contents


Themes

Location

Theme files live in the themes/ subfolder of the platform config directory (the same tree as config.json and the layouts):

~/.config/xtop/themes/*.jsonc                (Linux)
~/Library/Application Support/xtop/themes/   (macOS)
%APPDATA%\xtop\themes\                       (Windows)

The directory and the shipped theme files are created automatically on first run; no manual copy is needed.

Format

Each theme file defines a name, an explicit background/foreground pair and a 16-entry palette (format v2, UX8.1). Colors are hex strings with an optional # prefix. Comments (// and /* */) are supported in JSONC files. The background/foreground keys are optional for third-party files written against the old 16-slot format: absent keys fall back to palette[0] / palette[7]. The palette entries are not arbitrary colors: every slot has a fixed role (see the Palette Reference), so themes stay interchangeable and every renderer — widget packs and kernel chrome alike — picks colors by role, never by taste.

{
    // my-custom-theme -- Dark background, warm accents
    "name": "my-custom-theme",
    "background": "#1a1b1c", // screen/frame background (role bg)
    "foreground": "#abb2bf", // primary text (role fg)
    "palette": [
        "#1a1b1c", //  0: legacy background alias (ROLE_BG)
        "#e06c75", //  1: alert red (high fills, avg cpu line)
        "#98c379", //  2: good green (normal fills, RAM line)
        "#e5c07b", //  3: warn yellow (gradient mid stop)
        "#d19a66", //  4: read / RX (network RX, disk reads)
        "#c678dd", //  5: write / TX / GPU (network TX, disk writes)
        "#56b6c2", //  6: accent (titles, headers, selection)
        "#abb2bf", //  7: legacy foreground alias (ROLE_FG)
        "#3e4451", //  8: dim (zebra rows, separators, dividers)
        "#e06c75", //  9..15: bright series ramp (multi-series charts)
        "#98c379",
        "#e5c07b",
        "#d19a66",
        "#c678dd",
        "#56b6c2",
        "#abb2bf"
    ]
}

Contrast normalization (UX8.2)

Every theme is normalized once, right after parsing, against its explicit background. The engine measures the WCAG contrast ratio of each role and auto-lifts the colors that fail their floor, deterministically and hue-preserving (colors move toward white on dark backgrounds, toward black on light ones, in small steps until the floor clears):

RoleFloorSource
foreground text4.5:1explicit foreground (legacy files: slot 7)
accent3.0:1slot 6
dim3.0:1slot 8
zebra-row text3.0:1foreground painted over the dim stripe (slots share 8)
series ramp / accents2.0:1slots 1–5, 7, 9–15 (colored marks on the background)

The dim floor and the zebra-row-text floor share slot 8, so they cannot both hold on palettes whose foreground sits close to the background: zebra-row text always wins, and dim keeps the highest value that still clears it (for the shipped helsinki/oslo palettes no lift is possible at all and dim keeps its canonical value). The lifted values replace the in-memory palette entries, so renderers keep reading theme_palette() and the role accessors unchanged — the shipped/user files are never rewritten: normalization happens at load only.

Palette Reference

This table is the single truthful role reference: the kernel theme accessors (bg()/fg()/accent()/dim() in src/theme/model.rs) and the widget packs' ROLE_* constants (widgets repo, src/util.rs) map to exactly these slots, and the usage column reflects what the code really paints today. If a renderer needs a color, it takes it from here — no undocumented palette index. Text is drawn with the explicit foreground/background pair (theme_fg()/theme_bg() on the widget contract); the palette slots feed the colored marks.

Index Role Actual usage (kernel + widget packs)
0 legacy background alias (ROLE_BG) Terminal/block background of the packs' frames; the screen itself paints the explicit background key (Theme::bg())
1 alert (ROLE_ALERT) Red/high fills: CPU/mem/swap gauges past their alert threshold, the CPU average chart line, the minimal-view CPU gauge
2 good (ROLE_GOOD) Green/normal fills: low gradient stop, RAM history line, battery fill, minimal-view memory gauge
3 warn (ROLE_WARN) Yellow fills: gradient mid stop for CPU/mem/storage/swap at or above 50% but below the alert threshold
4 read/RX (ROLE_RX) Download/read metrics: network RX totals/lines, disk_io read gauges
5 write/TX (ROLE_TX) Upload/write metrics: network TX totals/lines, disk_io write gauges, GPU fill
6 accent (ROLE_ACCENT) Accents: process table header/selection, help key spans, overlay titles/borders; Theme::accent()
7 legacy foreground alias (ROLE_FG) Near-white anchor of the base hue family; can legitimately equal the background (Paris slot 7 == its background — renderers draw text with the explicit foreground, Theme::fg())
8 dim (ROLE_DIM) Dim/secondary: zebra row backgrounds, column separators, chart dividers, muted notes; Theme::dim()
915 bright series ramp (ROLE_SERIES_START..ROLE_SERIES_END) Seven bright variants for multi-series charts (per-core history lines, cycle by series index)

The shipped themes repeat the base hue family in slots 9–15 (bright variants of slots 1–7 in the same order), which is what makes the series ramp look coherent. Keep that convention when writing a theme: slots 9–15 should stay distinguishable from each other and from slots 1–8.

Starter Themes

xtop ships 12 themes. The x palette (almost-black background, purple-pink accents) is compiled into the binary as the startup fallback; all 12 definitions — including x and miami — are embedded in the binary as seeding templates. The first run writes them into the themes directory above, so every shipped theme is available without copying anything.

If you want to restore them later, copy from the repository:

cp -r assets/themes/* ~/.config/xtop/themes/   # Linux
# macOS: ~/Library/Application Support/xtop/themes/

Available themes: x, berlin, bogota, helsinki, lahabana, london, madrid, miami, oslo, paris, praha, tokio.

All theme palettes are documented in colors.md.

Loading Order

  1. The compiled-in x palette (startup fallback, index 0).
  2. Themes from the themes directory (seeded on first run) load on top; a file reusing the name x overrides the compiled palette in place.
  3. If a custom theme has the same name as a shipped one, it replaces it; new names are appended after the shipped set.

Tips

  • Try the grayscale themes (london, berlin) as a base and add your own accent colors.
  • Prefer writing the explicit background/foreground keys: the fallback (slot 0/slot 7) is only for legacy files, and a slot-7 foreground can equal the background (the shipped Paris palette keeps that quirk on purpose — the explicit pair is what the roles are anchored on).
  • Do not fight the contrast normalizer: roles below their floor are lifted at load. Write the palette you want; the engine guarantees the floors in-memory while the file stays canonical.
  • Slots 9–15 are the bright series ramp: make each entry a brighter sibling of slots 1–7 in order so multi-series charts stay distinct.
  • Never repurpose a slot: renderers pick colors by the role table above, so a theme that reorders roles breaks every widget that reads them.

Layouts

Location

Place layout files in the platform layouts directory (config dir + layouts):

~/.config/xtop/layouts/*.jsonc          (Linux)
~/Library/Application Support/xtop/layouts/*.jsonc   (macOS)
%APPDATA%\xtop\layouts\*.jsonc          (Windows)

Both .jsonc and .json extensions are accepted.

Format

A layout is a recursive tree of splits and widgets:

LayoutDef
 ├── name: string
 └── root: Area
      ├── direction: "horizontal" | "vertical"
      ├── size: constraint (optional, defaults to "*")
      └── areas: [Area, ...]
           ├── Area with "widget" → leaf node (renders a widget)
           └── Area with "direction" → nested split

Size Constraints

Syntax Meaning
"*" or omitted Fill remaining space
3 (number) Fixed n rows/columns
"45%" Percentage of parent

Available Widgets

Widget Description
header System info bar (uptime, load, keys)
cpu Per-core CPU usage gauges
memory RAM + Swap gauges + RAM history chart
storage Disk usage gauges per mount point
network Network RX/TX totals and speeds
processes Process table with search filter
disk_io Disk read/write speeds
battery Battery charge gauges
gpu GPU usage gauges

Examples

Simple custom layout

A minimal three-row layout: header, CPU, and processes.

{
    // "monitor" — CPU top-half, processes bottom-half
    "name": "monitor",
    "root": {
        "direction": "vertical",
        "areas": [
            { "widget": "header", "size": 3 },
            { "widget": "cpu", "size": "55%" },
            { "widget": "processes", "size": "*" }
        ]
    }
}

Complex nested layout

A full dashboard with a horizontal split in the middle section:

{
    "name": "my-dashboard",
    "root": {
        "direction": "vertical",
        "areas": [
            { "widget": "header", "size": 3 },
            {
                "direction": "horizontal",
                "size": "50%",
                "areas": [
                    { "widget": "cpu", "size": "60%" },
                    {
                        "direction": "vertical",
                        "size": "40%",
                        "areas": [
                            { "widget": "network", "size": "50%" },
                            { "widget": "disk_io", "size": "50%" }
                        ]
                    }
                ]
            },
            { "widget": "processes", "size": "*" }
        ]
    }
}

Starter Layouts

The 10 built-in layouts ship in the xtop-layout crate (github.com/xtop-cli/layouts, folder layouts/default/) and are embedded in the binary. On startup their JSONC sources are copied to the platform layouts directory (see Location) as editable templates. Community layouts live in layouts/custom/ of the same repo; install one with xtop layout install <name> (or copy the file into the layouts directory for your platform). Validate a local file with xtop layout check <file>.

A layout file whose name matches a built-in layout overrides it (e.g. edit dashboard.jsonc to customize the Dashboard). Files with new names show up as extra layouts.

Mode layouts: dashboard, vertical, horizontal, cpu_focus, memory_focus, network_focus, process_focus — these seven map to the layout modes.

Preset extras: detail_dashboard, detail_network, detail_processes — detail-focused layouts appended after the modes (not modes themselves; they are selected by name). They exercise per-widget display options (CPU basis on processes, cores/show_freq on cpu, ifaces on network); see the per-widget options section below.

Cycling Order

  1. Mode layouts (Dashboard → Vertical → Horizontal → CPU Focus → Memory Focus → Network Focus → Process Focus)
  2. Preset extras (Detail DashboardDetail NetworkDetail Processes)
  3. Any custom layout from the platform layouts directory with a new name (filesystem order)
  4. Custom files that reuse a built-in name override that built-in in place (no duplicates)
  5. Wraps back to Dashboard

Press l to cycle forward through all available layouts.

Notes

  • If a widget name in your layout doesn't match any available widget, that area is skipped and xtop prints a one-time warning to stderr (xtop: layout '<layout>' references unknown widget '<name>').
  • Nested splits can be arbitrarily deep, but very deep nesting may overflow small terminals.
  • The terminal must be at least 40×8 for any layout to render; smaller terminals show a warning.
  • Very small terminals (under 60×14) fall back to a minimal hardcoded layout (CPU + Memory gauges + process list).

Widget glyph style

Charts (CPU/Memory/Network) and widget borders are drawn with glyph styles you can change in config.json under the style key (see configuration.md):

{
  "theme": "x",
  "style": {
    "charset": "block",
    "borders": "ascii",
    "widgets": {
      "cpu": { "charset": "bar" },
      "network": { "borders": "double" }
    }
  }
}
  • charset: braille (default), dot, block, half_block, bar.
  • borders: native (default; the classic single-line box-drawing frame), rounded, double, plain and ascii (both plain and ascii draw a pure ASCII +-| frame).
  • widgets: per-widget overrides. Keys are the widget names layouts use: header, cpu, memory, storage, network, processes, disk_io, battery, gpu. Each entry accepts charset, borders and an optional pack (widget pack to render that name with, e.g. "blocks"). A global style.pack sets the pack for every widget without a per-widget override.

Glyph styles only change the look: the data behind each widget is drawn by the widget packs (see plugin.md for how a plugin adds completely new renderers, which take precedence over packs).

Per-widget display options

Beyond glyph style, every widget instance in a layout file can carry an options JSON object that refines how that instance draws its data. The layout format accepts it on widget nodes as an opaque passthrough (see the layouts repo, docs/layout-schema.md, section "Widget options"):

{
  "name": "My Layout",
  "root": {
    "direction": "vertical",
    "areas": [
      { "widget": "header", "size": 3 },
      { "widget": "cpu", "size": "60%", "options": { "cores": "all" } },
      { "widget": "processes", "size": "*" }
    ]
  }
}
  • The kernel forwards each node's options object to the widget's renderer while that instance is drawn (via WidgetState::widget_options in the widget-api contract). Multiple instances of the same widget in one layout can carry different options.
  • No options key (or null) means the widget renders exactly as before this feature — the defaults preserve the current behavior byte-for-byte. Only documented keys refine a widget; unknown keys are ignored.
  • Recognized keys are documented per widget in the widgets repo (docs/widgets.md) as the UX milestones land them. The shipped Detail presets showcase the first wave: the processes CPU basis (cpu: "total"/"both"), the cpu core/frequency keys (cores, show_freq) and the network interface list (ifaces). Until a widget documents a key, that key is inert (DR-UX2 defaults).
  • Fullscreen and minimal views look the options up by widget name in the current layout (first matching node); when the layout has no such node they use the defaults.
  • Plugin widget renderers see the plugin HostState, not WidgetState: layout options are not forwarded to plugins.

Widget Packs

Widget packs are the installable unit of widget code: each pack is a separate crate (xtop-widget-<name>) that registers renderers by widget name against the xtop-widget-api contract. The kernel ships two packs out of the box — the base pack (default, always compiled in) and the blocks pack (gated behind the widget-blocks Cargo feature) — and lists them in a single compile-time catalog (src/ui/layout/pack_table.rs, one (feature, label) row per pack), which the render engine and xtop widget list share.

Widget-pack management mirrors the plugin workflow:

CommandDescription
xtop widget list List the widget packs wired into the kernel (pack-table rows whose Cargo feature is declared in the root Cargo.toml)
xtop widget scaffold <name> Create a compiling single-widget pack template in widgets-dev/xtop-widget-<name>/ (git-ignored)
xtop widget install <name> Install a pack by name from github.com/xtop-cli/widgets
xtop widget install <url|path> Install a pack from any git URL or a local crate directory

xtop widget install is a self-modifying-source workflow (the same spirit as xtop plugin install): it adds an optional dependency + a widget-<name> feature flag to the root Cargo.toml, appends one (feature, label) row and its registry-linking arm to the pack catalog in src/ui/layout/pack_table.rs, and runs cargo check. The pack is not enabled by default: add widget-<name> to the [features] default list (or build with --features widget-<name>) and rebuild. Once enabled, select the pack per widget with style.pack (all widgets) or style.widgets.<name>.pack (one widget), then place its widget names in a layout file. Authoring guidance (the pack contract, how packs register renderers, the renderers' options) lives in the widgets repo docs (docs/authoring.md, docs/widgets.md).


← Back to README

Source: docs/customization.mdMirrored from the xtop-cli repositories · MITAll docs →