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>
23 KiB
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_symbolfields (viaauto_unit) - Both cells inherit the same
optionalflag; value cell getsdecorationfrom 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 2–4 start with curse_new_line().
Conditional behaviour:
- Hidden entirely when
args.percpuis True (percpu takes priority). idleshown only when'user' in self.stats(i.e. not Windows;idle_tag=False).- Line 2 shows
useron Unix/Linux,idleon Windows. - Line 3 shows
systemon Unix/Linux,coreon Windows. - Line 4 shows
iowaiton Linux,dpcon Windows. guestshown only on Linux;syscallsshown on non-Linux/non-macOS.- All rate fields (
ctx_switches,interrupts,soft_interrupts,syscalls) areoptional=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
availablewhenself.availableis True (Linux/macOS), otherwiseused. active,inactive,buffers,cachedareoptional=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 showsFree, row value showsauto_unit(free)instead ofauto_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
ramare hidden unless--diskio-show-ramfsis passed (issue #714). Not implemented in v5 G4 — operators can use the show/hide regex filters. --diskio-iops/--diskio-latencyalt modes deferred to a later phase pending args plumbing throughrender().- v4 hides disks that never had non-zero traffic
(
hide_zero_fieldsper-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:
cpucoresegment only shown when'cpucore' in self.stats and self.stats['cpucore'] > 0.- Irix-mode formatting only when
args.disable_irixand 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' --byteflag: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 == Falseare skipped (issue #765). - Interfaces where both
bytes_recv_rate_per_secandbytes_sent_rate_per_secare 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:
-
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']
- Linux:
-
If quicklook is disabled: prepend
'total'to the header list and output title'{:5}'.format('CPU')asTITLE. -
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 sameget_alertcolor rule.
Layout notes:
max_cpu_displayis configurable (default: 4).- When more CPUs exist than
max_cpu_display, the top-N bytotalare shown plus the overflow summary row. - Each CPU row starts with
curse_new_line(). - When quicklook is enabled, the
CPUtitle column andtotalcolumn 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.percpuis True; cpu plugin hides itself in that case. max_cpu_displaycan be overridden inglances.confunder[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'(TITLEdecoration). - Total:
'{:>4}'.format(processcount['total']). - Threads (when
'thread'in stats):({} thr),. - Running / Sleeping:
{} run,/{} slp,. - Other:
{} othwhereother = total - running - sleeping.
Sort line:
- Drives off
args.programs("Programs" / "Threads") andglances_processes.sort_key(mapped throughsort_for_human). - Adds " sorted automatically" when
glances_processes.auto_sortis True.
Conditional behaviour:
args.disable_processshort-circuits with the single linePROCESSES DISABLED (press 'z' to display).glances_processes.process_filter(when set) prepends a multi-lineProcesses 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 thelayout_headerwidth tokens (e.g.'cpu': '{:<6} ','mem': '{:<5} ','pid': '{:>{width}} '). - Active sort column is decorated with
SORT; the rest getDEFAULT. - 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.programsswaps the per-thread list for an aggregated per-program view (sums of cpu_percent / memory_percent etc.).args.enable_process_extendedandcursor_positionhighlight 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.)