Files
glances/docs/architecture/tui-v4-rendering-patterns.md
nicolargo 80c20863d9 feat(v5): categorical thresholds + processlist VIRT/RES/IO + bold cmd
Three orthogonal improvements bundled under the processlist v5 surface:

1) Categorical thresholds (framework-level)
   - thresholds_v5 grows compute_level_categorical + read_thresholds_categorical:
     value-set membership instead of numeric comparison. Walked
     most-severe-first so misconfigured overlaps escalate to the higher
     bucket. Unmatched values return "ok" (v4 parity).
   - base_v5._compute_levels_for_item dispatches on schema flag
     ``threshold_type: "categorical"``; numeric path unchanged.
   - 8 new threshold unit tests (incl. CSV parsing, whitespace, pk-prefix).

2) processlist status + nice as categorical fields
   - status (R/W/Z/D/...) and nice (-20..19) become watched with
     ``threshold_type: "categorical"``, no defaults — operators opt in:
       [processlist]
       status_ok=R,W,P,I
       status_critical=Z,D
       nice_warning=-20,...,-1,1,...,19
     Without configuration: no _levels entry (no false positives).

3) processlist renderer: VIRT/RES + R/s/W/s + bold cmd
   - Adds VIRT (memory_info.vms) and RES (memory_info.rss) columns.
   - Adds R/s and W/s columns computed from io_counters
     ([r_new, w_new, r_old, w_old, io_tag]) / time_since_update;
     io_tag != 1 renders "?" (access denied or first cycle).
   - Status / nice cells inherit categorical _levels colour.
   - Command rendering ports v4 split_cmdline:
     /usr/bin/python3 myscript.py
       → "/usr/bin/" + **"python3"** + " myscript.py"
     Kthreads (empty cmdline) keep the [name] fallback.

v4 catalogue updated accordingly. Suite v5: 1370+30 green, lint clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 17:30:01 +02:00

23 KiB
Raw Permalink Blame History

TUI v4 Rendering Patterns — Phase 1 Plugins

Reference catalogue of msg_curse() output patterns for the five plugins migrated in Phase 1: cpu, mem, load, network, percpu.

Purpose: this document is the visual-parity contract for the v5 generic curses renderer. It describes what v4 does; it does not prescribe v5 design.


cpu

Source: glances/plugins/cpu/__init__.py::msg_curse

Guard: returns empty if not self.stats, args.percpu is set, or plugin is disabled.

Header line example:

CPU   12.3%   idle  87.1%   ctx_sw  1.2K

Field table:

field label format alignment total width color rule
(title) CPU {:8} left 8 TITLE
total (none) {:5.1f}% right 6 get_views(key='total', option='decoration')
idle idle {:4.1f}% via {:>8} label + {:4.1f}% value right 8+5 optional=get_views(key='idle', option='optional')
ctx_switches ctx_sw curse_add_stat('ctx_switches', width=15, header=' '){:8} label + {:5}K/s value 15 get_views(key='ctx_switches', option='decoration')

Line 2 (user/idle + irq + interrupts):

field label format width color rule
user (or idle on Windows) user curse_add_stat('user', width=15) 15 decoration from views
irq irq curse_add_stat('irq', width=14, header=' ') 14 decoration from views
interrupts inter curse_add_stat('interrupts', width=15, header=' ') 15 decoration from views

Line 3 (system/core + nice + sw_int/ctx_sw):

field label format width color rule
system (or core on Windows) system curse_add_stat('system', width=15) 15 decoration from views
nice nice curse_add_stat('nice', width=14, header=' ') 14 decoration from views
soft_interrupts (or ctx_switches fallback) sw_int curse_add_stat('soft_interrupts', width=15, header=' ') 15 decoration from views

Line 4 (iowait/dpc + steal + guest/syscalls):

field label format width color rule
iowait (or dpc on Windows) iowait curse_add_stat('iowait', width=15) 15 decoration from views
steal steal curse_add_stat('steal', width=14, header=' ') 14 decoration from views
guest (Linux) or syscalls (non-Linux/non-macOS) guest curse_add_stat('guest', width=14, header=' ') 14 decoration from views

curse_add_stat layout (width=N):

  • label cell: header + '{:{width}}'.format(key_name, width=N-7) — left-aligned, padded
  • value cell: '{:5.1f}%' for percent fields; '{:>5}K' for number+min_symbol fields (via auto_unit)
  • Both cells inherit the same optional flag; value cell gets decoration from views

Layout notes: 4-line block; CPU title left-padded to 8; total is a bare {:5.1f}% (no label) directly after the title on line 1; all other fields use curse_add_stat. Lines 24 start with curse_new_line().

Conditional behaviour:

  • Hidden entirely when args.percpu is True (percpu takes priority).
  • idle shown only when 'user' in self.stats (i.e. not Windows; idle_tag=False).
  • Line 2 shows user on Unix/Linux, idle on Windows.
  • Line 3 shows system on Unix/Linux, core on Windows.
  • Line 4 shows iowait on Linux, dpc on Windows.
  • guest shown only on Linux; syscalls shown on non-Linux/non-macOS.
  • All rate fields (ctx_switches, interrupts, soft_interrupts, syscalls) are optional=True — hidden when terminal is narrow.

v5 renderer: glances/plugins/cpu/render_curses_v5.py


mem

Source: glances/plugins/mem/__init__.py::msg_curse

Guard: returns empty if not self.stats or plugin is disabled.

Header line example:

MEM ↑  74.2%   active  5.3G

Field table:

field label format alignment total width color rule
(title) MEM '{}'.format('MEM') left 3 TITLE
trend (space + arrow) ' {:2}'.format(trend_msg(...)) left 3 DEFAULT
percent (none) '{:>7.1%}'.format(percent/100) right 7 get_views(key='percent', option='decoration')
active active curse_add_stat('active', width=16, header=' ') 16 decoration from views

Line 2 (total + inactive):

field label format width color rule
total total curse_add_stat('total', width=15) 15 DEFAULT
inactive inacti curse_add_stat('inactive', width=16, header=' ') 16 DEFAULT

Line 3 (available/used + buffers):

field label format width color rule
available (or used if not available) avail (or used) curse_add_stat('available', width=15) 15 DEFAULT
buffers buffer curse_add_stat('buffers', width=16, header=' ') 16 DEFAULT

Line 4 (free + cached):

field label format width color rule
free free curse_add_stat('free', width=15) 15 DEFAULT
cached cached curse_add_stat('cached', width=16, header=' ') 16 DEFAULT

curse_add_stat for bytes fields: unit='bytes', min_symbol='K' → value rendered via auto_unit(int(value)) with no unit suffix (bytes has no entry in fields_unit_short). Template: '{:>5}' (integer path via min_symbol).

Layout notes: 4-line block; MEM title immediately followed by 2-char trend indicator then 7-char right-aligned percentage; two-column grid from line 2 onward (15 + 16 chars). percent is formatted as {:>7.1%} (Python's % format, i.e. value × 100 with % suffix) but the stat is already divided by 100 before formatting: self.stats['percent'] / 100.

Color logic: percent decoration comes from get_alert_log(used, maximum=total) set in update_views(); thresholds follow the standard CAREFUL/WARNING/CRITICAL ladder. All byte fields are DEFAULT (no threshold configured for them).

Conditional behaviour:

  • Line 3 shows available when self.available is True (Linux/macOS), otherwise used.
  • active, inactive, buffers, cached are optional=True — hidden on narrow terminals.

v5 renderer: glances/plugins/mem/render_curses_v5.py


memswap

Source: glances/plugins/memswap/__init__.py::msg_curse

Guard: returns empty if not self.stats or plugin is disabled.

Expected v4-equivalent output:

SWAP   25.0%
total          16.0G
used            4.0G
free           12.0G

Header field table:

field label format width color rule
(title) SWAP '{:4}'.format('SWAP') 4 TITLE
trend arrow ↑/↓/ '{:2}'.format(trend_msg(...)) 2 DEFAULT
percent 25.0% '{:>6.1%}'.format(percent / 100) 6 get_views(key='percent', option='decoration')

Body rows (lines 2-4): each row is curse_add_stat(<field>, width=15) — single label/value pair, label left-aligned, value right-aligned, total row width 15 chars.

Line Field Notes
2 total Total swap memory
3 used Used swap memory
4 free Free swap memory

Color logic: percent decoration comes from get_alert_log(used, maximum=total) — standard CAREFUL/WARNING/CRITICAL ladder.

Conditional behaviour: the plugin is hidden when the system has no swap configured (psutil raises on swap_memory() — see Illumos/OpenBSD issues #1767, #2719).

v5 renderer: glances/plugins/memswap/render_curses_v5.py (Added in G4-memswap. Trend arrow not yet ported — same status as mem/load.)


fs

Source: glances/plugins/fs/__init__.py::msg_curse

Guard: returns empty if not self.stats, plugin disabled, or max_width is None (logs a debug message).

Expected v4-equivalent output (default — used + total):

FILE SYS              Used   Total
/                   125.0G  500.0G
/home               512.0G    1.0T

name_max_width computation: max_width - 13

Header field table:

field label format alignment width notes
(title) FILE SYS '{:{width}}'.format('FILE SYS', width=name_max_width) left name_max_width TITLE
Used / Free Used or Free '{:>8}'.format(...) right 8 depends on --fs-free-space flag
Total Total '{:>7}'.format('Total') right 7 DEFAULT

Per-filesystem row:

field format alignment width color rule
mnt_point concatenated mnt (device_short) when room, else truncated with leading _ left name_max_width DEFAULT
used / free auto_unit(value) right-padded to 7 right 7 get_alert(used, max=size)
total auto_unit(size) right-padded to 7 right 7 DEFAULT

Sort order: mountpoint ascending (operator.itemgetter('mnt_point')).

Color logic: the used cell carries the alert decoration computed from get_alert(current=size-free, maximum=size, header=mnt_point) — standard CAREFUL/WARNING/CRITICAL ladder. Read-only mounts (ro in options) skip the alert in v4 (issue #3143); v5 keeps the alert universal — operators can suppress via the show/hide filters.

Conditional behaviour:

  • --fs-free-space: header shows Free, row value shows auto_unit(free) instead of auto_unit(used).
  • Mountpoints whose alias / show / hide filters reject them are skipped.

v5 renderer: glances/plugins/fs/render_curses_v5.py (Added in G4-fs. Default mode only — --fs-free-space toggle and the optional (device) suffix on mountpoints are deferred to a later phase pending CLI / max_width plumbing.)


diskio

Source: glances/plugins/diskio/__init__.py::msg_curse

Guard: returns empty if not self.stats, plugin disabled, or max_width is None (logs a debug message).

Expected v4-equivalent output (default — byte rates):

DISK I/O              R/s     W/s
nvme0n1              100B     50B
sda                   1.4M   732K

name_max_width computation: max_width - 13

Header field table (default mode):

field label format alignment width notes
(title) DISK I/O '{:{width}}'.format('DISK I/O', width=name_max_width) left name_max_width TITLE
read R/s '{:>8}'.format('R/s') right 8 DEFAULT
write W/s '{:>7}'.format('W/s') right 7 DEFAULT

Header variants (controlled by args):

mode labels shown
default (rate) R/s + W/s
--diskio-iops IOR/s + IOW/s
--diskio-latency ms/opR + ms/opW

Per-disk row:

field format alignment width color rule
disk_name left-padded, tail-truncated with _ when too long left name_max_width DEFAULT
read auto_unit(read_bytes_rate_per_sec) (no /s suffix — header carries it) right 7 get_views(item=disk, key='read_bytes', option='decoration')
write same for write_bytes right 7 get_views(item=disk, key='write_bytes', option='decoration')

Sort order: disk name ascending (sorted_stats()).

Color logic: read_bytes / write_bytes decorations come from get_alert(bytes, header='rx', action_key=disk_name) and the tx variant. v4 thresholds are configurable but ship with no defaults — v5 keeps this opt-in semantic via strict_thresholds=True so a legacy [diskio] careful=50 cannot bleed onto per-disk rates.

Conditional behaviour:

  • Disks whose name starts with ram are hidden unless --diskio-show-ramfs is passed (issue #714). Not implemented in v5 G4 — operators can use the show/hide regex filters.
  • --diskio-iops / --diskio-latency alt modes deferred to a later phase pending args plumbing through render().
  • v4 hides disks that never had non-zero traffic (hide_zero_fields per-view). v5 keeps zero-traffic disks visible — operators can filter via [diskio] hide= regex.

v5 renderer: glances/plugins/diskio/render_curses_v5.py (Added in G4-diskio. Default rate mode only; alt modes deferred.)


load

Source: glances/plugins/load/__init__.py::msg_curse

Guard: returns empty if not self.stats, self.stats == {}, or plugin is disabled.

Header line example:

LOAD ↑  4core
1 min   0.72
5 min   1.45
15 min  1.23

Field table:

field label format alignment width color rule
(title) LOAD '{:4}'.format('LOAD') left 4 TITLE
trend (space + arrow) ' {:1}'.format(trend_msg(...)) left 2 DEFAULT
cpucore (none) '{:3}core'.format(int(cpucore)) left 7 DEFAULT
min1 1 min '{:7}'.format('1 min') + f'{load:>6.2f}' (or f'{load:>5.1f}%' in Irix mode) right 7+6 get_views(key='min1', option='decoration')
min5 5 min same pattern right 7+6 get_views(key='min5', option='decoration')
min15 15 min '{:7}'.format('15 min') + value right 7+6 get_views(key='min15', option='decoration')

Layout notes: header (title + trend + core count) on line 1; each load average on its own line via curse_new_line(). Label cell is 7 chars left-aligned; value cell is 6 chars right-aligned (>6.2f). No two-column layout — single column.

Irix mode: when args.disable_irix is set and log_core() != 0, load values are divided by log_core() and multiplied by 100, then formatted as {:>5.1f}% (5 chars + %).

Color logic: get_views(key='minN', option='decoration') maps to CAREFUL/WARNING/CRITICAL thresholds based on load vs. core count ratio. min1 trend drives the trend arrow.

Conditional behaviour:

  • cpucore segment only shown when 'cpucore' in self.stats and self.stats['cpucore'] > 0.
  • Irix-mode formatting only when args.disable_irix and cores > 0.

v5 renderer: glances/plugins/load/render_curses_v5.py


network

Source: glances/plugins/network/__init__.py::msg_curse

Guard: returns empty if not self.stats, plugin disabled, or max_width is None (logs a debug message in that case).

Header line example (default — rate, two columns):

NETWORK          Rx/s    Tx/s
eth0             1.2Mb   256Kb
lo                  0       0

name_max_width computation: max_width - 12

Header field table:

field label format alignment width notes
(title) NETWORK '{:{width}}'.format('NETWORK', width=name_max_width) left name_max_width TITLE
Rx/s header Rx/s '{:>7}'.format('Rx/s') right 7 DEFAULT
Tx/s header Tx/s '{:>7}'.format('Tx/s') right 7 DEFAULT

Header variants (controlled by args):

mode labels shown
default (rate, two cols) Rx/s + Tx/s
--network-cumul (cumulative, two cols) Rx + Tx
--network-sum (rate, one col) Rx+Tx/s (width 14)
--network-cumul --network-sum Rx+Tx (width 14)

Per-interface row:

field format alignment width color rule
if_name '{:{width}}'.format(if_name, width=name_max_width) left name_max_width DEFAULT
rx (rate or cumul) f'{rx:>7}' right 7 get_views(item=if_key, key='bytes_recv', option='decoration')
tx (rate or cumul) f'{tx:>7}' right 7 get_views(item=if_key, key='bytes_sent', option='decoration')
ax (sum mode) f'{ax:>14}' right 14 DEFAULT

Rate/unit computation:

  • Default (bits): to_bit=8, unit='b' — multiply bytes by 8, append 'b'
  • --byte flag: to_bit=1, unit='' — display raw bytes, no suffix
  • Value formatted via self.auto_unit(int(value * to_bit)) + unit → e.g. 1.2Mb, 256Kb

Interface name truncation: if len(if_name) > name_max_width, truncated to '_' + if_name[-(name_max_width-1):].

Layout notes: one header line + one line per active interface. Each interface row starts with curse_new_line(). Total row width = name_max_width + 14 (two 7-char columns) or name_max_width + 14 (one 14-char column in sum mode).

Color logic: bytes_recv and bytes_sent decorations come from views thresholds (CAREFUL/WARNING/CRITICAL) set against configured interface speed limits.

Conditional behaviour:

  • Interfaces with is_up == False are skipped (issue #765).
  • Interfaces where both bytes_recv_rate_per_sec and bytes_sent_rate_per_sec are hidden (all-zero, hide_zero=True) are skipped (issue #1787).
  • Interfaces with no first-measurement rate yet (rates is None) are skipped.
  • --network-cumul: shows total bytes transferred instead of per-second rate.
  • --network-sum: collapses Rx+Tx into a single 14-char column.
  • --byte: displays bytes instead of bits.

v5 renderer: glances/plugins/network/render_curses_v5.py (G1 ships the default rate-bits-2col mode only — --byte, --network-cumul and --network-sum deferred to G2+ pending max_width / args plumbing through render().)


percpu

Source: glances/plugins/percpu/__init__.py::msg_curse

Note: This plugin's layout differs fundamentally from the others — it renders a transposed table (fields as rows, CPU cores as columns) rather than a single-entity block. The description below uses prose + representative examples.

Guard: returns empty if not self.stats, not args.percpu (plugin is hidden when args.percpu is False), or plugin is disabled.

Representative output (Linux, 4 cores, quicklook disabled):

CPU    user  system iowait    idle     irq    nice   steal   guest
CPU0   12.5%   3.2%   0.5%  83.8%    0.0%   0.0%    0.0%    0.0%
CPU1    8.1%   2.0%   0.1%  89.8%    0.0%   0.0%    0.0%    0.0%
CPU2   15.0%   4.5%   1.2%  79.3%    0.0%   0.0%    0.0%    0.0%
CPU3    6.3%   1.8%   0.0%  91.9%    0.0%   0.0%    0.0%    0.0%
CPU*    9.9%   2.8%   0.4%  86.2%   ...

Header construction:

  1. Base header list from OS (method define_headers_from_os()):

    • Linux: ['user', 'system', 'iowait', 'idle', 'irq', 'nice', 'steal', 'guest']
    • macOS: ['user', 'system', 'idle', 'nice']
    • BSD: ['user', 'system', 'idle', 'irq', 'nice']
    • Windows: ['user', 'system', 'dpc', 'interrupt']
  2. If quicklook is disabled: prepend 'total' to the header list and output title '{:5}'.format('CPU') as TITLE.

  3. Header row: for each stat name in header list: f'{stat:>7}' — right-aligned, 7 chars.

Per-CPU row:

element format width color rule
CPU id label (quicklook disabled) f'CPU{id:1} ' (id < 10) or f'{id:4} ' (id ≥ 10) 5 DEFAULT
each stat value f'{value:6.1f}%' 7 (6+%) get_alert(value, header=stat_name)

Overflow row (CPU*):

  • Shown only when len(self.stats) > max_cpu_display (default: 4).
  • Label: 'CPU* ' (5 chars).
  • Values: mean of all non-displayed CPUs for each stat, same {:6.1f}% format and same get_alert color rule.

Layout notes:

  • max_cpu_display is configurable (default: 4).
  • When more CPUs exist than max_cpu_display, the top-N by total are shown plus the overflow summary row.
  • Each CPU row starts with curse_new_line().
  • When quicklook is enabled, the CPU title column and total column are omitted — percpu acts as a pure supplementary view.

Color logic: get_alert(value, header=stat_name) — uses the stat name as the alert key, mapping to CAREFUL/WARNING/CRITICAL thresholds configured for each CPU field.

Conditional behaviour:

  • Only active when args.percpu is True; cpu plugin hides itself in that case.
  • max_cpu_display can be overridden in glances.conf under [percpu] max_cpu_display.
  • The set of columns shown is OS-dependent (see header construction above).

v5 renderer: glances/plugins/percpu/render_curses_v5.py (G1 ships the quicklook-disabled mode — CPU title + total column always present. Quicklook-enabled toggle deferred to G2+.)


processcount

Source: glances/plugins/processcount/__init__.py::msg_curse

Layout:

TASKS 215 (1452 thr), 3 run, 195 slp, 17 oth   Threads sorted automatically
                                                by cpu_percent

Header construction:

  • Title: 'TASKS' (TITLE decoration).
  • Total: '{:>4}'.format(processcount['total']).
  • Threads (when 'thread' in stats): ({} thr),.
  • Running / Sleeping: {} run, / {} slp,.
  • Other: {} oth where other = total - running - sleeping.

Sort line:

  • Drives off args.programs ("Programs" / "Threads") and glances_processes.sort_key (mapped through sort_for_human).
  • Adds " sorted automatically" when glances_processes.auto_sort is True.

Conditional behaviour:

  • args.disable_process short-circuits with the single line PROCESSES DISABLED (press 'z' to display).
  • glances_processes.process_filter (when set) prepends a multi-line Processes filter: <expr> on column <key> header with a hint ('ENTER' to edit, 'E' to reset).

v5 renderer: glances/plugins/processcount/render_curses_v5.py (Added in G4-processlist. Sort indicator and filter UI deferred — v5 hardcodes engine sort to cpu_percent; argv/config plumbing comes with G5.)


processlist

Source: glances/plugins/processlist/__init__.py::msg_curse

Layout (excerpt — header + top rows):

CPU%  MEM%      PID USER       THR  NI S Command
78.4   3.1     1234 alice        4   0 S python3 myscript.py
12.5   3.1      512 root         2   0 S sshd
 0.5   0.2       42 bob          1   0 S htop

Header construction:

  • Each column header is produced by msg_curse_header_common(field, label, …) with the layout_header width tokens (e.g. 'cpu': '{:<6} ', 'mem': '{:<5} ', 'pid': '{:>{width}} ').
  • Active sort column is decorated with SORT; the rest get DEFAULT.
  • v4 conditionally shows VIRT/RES (memory_info), TIME+, R/s/W/s and CPU core columns depending on disable_stats + disable_virtual_memory.

Per-process row:

element format color rule
CPU% {:<6.1f} (or {:<6.0f} when no-digit) get_alert(cpu_percent, header='cpu_percent')
MEM% {:<5.1f} get_alert(memory_percent, header='memory_percent')
PID {:>{width}} (width = _max_pid_size) DEFAULT
USER {:<10} (truncated, trailing + on overflow) DEFAULT
THR {:<3} DEFAULT
NI {:>3} DEFAULT
S {:>1} DEFAULT
Command {} (joined cmdline, fallback [name]) DEFAULT

Conditional behaviour:

  • args.programs swaps the per-thread list for an aggregated per-program view (sums of cpu_percent / memory_percent etc.).
  • args.enable_process_extended and cursor_position highlight one process and append a multi-line extended block (open files, threads, TCP/UDP, swap, mean/min/max).
  • The list is pre-sorted by the engine via sort_stats(processlist, sorted_by=sort_key, reverse=sort_reverse).

v5 renderer: glances/plugins/processlist/render_curses_v5.py (Added in G4-processlist. Layout: CPU% MEM% VIRT RES PID USER THR NI S R/s W/s Command — full v4 default column set. Categorical thresholds wired for status and nice via status_<level>=<csv> / nice_<level>=<csv> in [processlist]. Command rendering ports v4's split_cmdline: path + bold cmd + arguments. Top-20 rows, engine sort hardcoded to cpu_percent desc, no extended view, no programs aggregation, no filter UI — these come back with the v5 argv/config plumbing in G5.)