agentmon 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +105 -0
- data/exe/agentmon +18 -0
- data/lib/agentmon/commands/cli_options.rb +21 -0
- data/lib/agentmon/commands/footprint.rb +81 -0
- data/lib/agentmon/commands/memory.rb +110 -0
- data/lib/agentmon/commands/record.rb +118 -0
- data/lib/agentmon/commands/report.rb +127 -0
- data/lib/agentmon/commands/sessions.rb +98 -0
- data/lib/agentmon/commands/top.rb +87 -0
- data/lib/agentmon/commands/tree.rb +96 -0
- data/lib/agentmon/darwin.rb +235 -0
- data/lib/agentmon/engine.rb +115 -0
- data/lib/agentmon/focus.rb +88 -0
- data/lib/agentmon/metrics/memory.rb +76 -0
- data/lib/agentmon/metrics/network.rb +245 -0
- data/lib/agentmon/metrics/pressure_drivers.rb +57 -0
- data/lib/agentmon/metrics/process_rates.rb +65 -0
- data/lib/agentmon/metrics/process_rows.rb +41 -0
- data/lib/agentmon/metrics/session_ledger.rb +160 -0
- data/lib/agentmon/metrics/session_memory.rb +84 -0
- data/lib/agentmon/metrics/session_names.rb +169 -0
- data/lib/agentmon/metrics/sessions.rb +80 -0
- data/lib/agentmon/metrics/system.rb +114 -0
- data/lib/agentmon/model.rb +263 -0
- data/lib/agentmon/probes/cwd.rb +32 -0
- data/lib/agentmon/probes/memory.rb +139 -0
- data/lib/agentmon/probes/network.rb +326 -0
- data/lib/agentmon/probes/processes.rb +72 -0
- data/lib/agentmon/probes/system.rb +134 -0
- data/lib/agentmon/program.rb +55 -0
- data/lib/agentmon/reading.rb +83 -0
- data/lib/agentmon/recorders/memory.rb +19 -0
- data/lib/agentmon/recorders/sessions.rb +40 -0
- data/lib/agentmon/registry.rb +155 -0
- data/lib/agentmon/sampler.rb +50 -0
- data/lib/agentmon/store.rb +86 -0
- data/lib/agentmon/ui/connections.rb +75 -0
- data/lib/agentmon/ui/interaction.rb +94 -0
- data/lib/agentmon/ui/memory.rb +125 -0
- data/lib/agentmon/ui/process_actions.rb +49 -0
- data/lib/agentmon/ui/process_detail.rb +148 -0
- data/lib/agentmon/ui/process_network.rb +13 -0
- data/lib/agentmon/ui/process_scopes.rb +32 -0
- data/lib/agentmon/ui/process_waits.rb +99 -0
- data/lib/agentmon/ui/processes.rb +42 -0
- data/lib/agentmon/ui/session_focus.rb +126 -0
- data/lib/agentmon/ui/session_memory.rb +78 -0
- data/lib/agentmon/ui/sessions.rb +130 -0
- data/lib/agentmon/ui/theme.rb +49 -0
- data/lib/agentmon/ui.rb +80 -0
- data/lib/agentmon/version.rb +5 -0
- data/lib/agentmon/views/dense.rb +157 -0
- data/lib/agentmon/views/history.rb +62 -0
- data/lib/agentmon/views/signals.rb +279 -0
- data/lib/agentmon/views/visual.rb +172 -0
- data/lib/agentmon/views/widgets.rb +352 -0
- data/lib/agentmon/views.rb +875 -0
- data/lib/agentmon.rb +38 -0
- metadata +135 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# visual: gauges, areas and heat, sessions first (docs/views.md). Home is the machine's breakdown
|
|
4
|
+
# over every agent session, by name when Claude Code or Codex knows one (Enter on one drills into
|
|
5
|
+
# it: its CPU gauge and area beside the machine's RAM with the session's slice, network and disk
|
|
6
|
+
# areas, processes beside the detail, connections; Escape returns). Its real home frame, from
|
|
7
|
+
# `COLUMNS=100 LINES=50 bundle exec exe/agentmon --layout visual | cat` (one sample, so areas and
|
|
8
|
+
# sparklines are still empty and network rates still settling):
|
|
9
|
+
#
|
|
10
|
+
# ╭─ CPU ──────────────────────────────────────────╮╭─ Memory ───────────────────────────────────────╮
|
|
11
|
+
# │CPU ▕███████████████▏ ▏ 41.8%││used ▕████████████████▌ ▏ 25G / 32G 79%│
|
|
12
|
+
# │last 4 min 41.8%││pressure ▕██████████▏ ▏ 48.0%│
|
|
13
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀││swap ▕█████▎ ▏ 255M / 1.0G 25%│
|
|
14
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀││app 10G wired 3.5G│
|
|
15
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀││compressed 11G cached 5.7G│
|
|
16
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠││used, last 4 min 25G│
|
|
17
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸│
|
|
18
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸│
|
|
19
|
+
# │load 1m 5.5 load 5m 6.2 load 15m 6.0 ││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸│
|
|
20
|
+
# ╰────────────────────────────────────────────────╯╰────────────────────────────────────────────────╯
|
|
21
|
+
# ╭─ Network ──────────────────────────────────────╮╭─ Disk ─────────────────────────────────────────╮
|
|
22
|
+
# │net in 1.7K/s││disk read 933K/s│
|
|
23
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸│
|
|
24
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸│
|
|
25
|
+
# │net out 1.8K/s││disk write 0B/s│
|
|
26
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
|
|
27
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
|
|
28
|
+
# ╰────────────────────────────────────────────────╯╰────────────────────────────────────────────────╯
|
|
29
|
+
# ╭─ Sessions ─[Live] All ───────────────────────────────────────────────────────────────────────────╮
|
|
30
|
+
# │ Session CPU▼ Footprint Read Write Net in Net out│
|
|
31
|
+
# │ Claude 21354 ⠀⠀⠀⠀⢀ 23.4% 3.7G 0B/s 0B/s 0B/s ⠀⠀⠀⠀⢀ 0B/s│
|
|
32
|
+
# │ Agentmon r2ui integration… ⠀⠀⠀⠀⢀ 14.6% 372M 0B/s 0B/s 0B/s ⠀⠀⠀⠀⢀ 0B/s│
|
|
33
|
+
# │ ChatGPT 2388 ⠀⠀⠀⠀⢀ 10.0% 3.3G 0B/s 0B/s 0B/s ⠀⠀⠀⠀⢀ 0B/s│
|
|
34
|
+
# │ Project history · claude… ⠀⠀⠀⠀⢀ 1.0% 285M 0B/s 0B/s ⠀⠀⠀⠀⢀ │
|
|
35
|
+
# │ EPK component review · claude… ⠀⠀⠀⠀⢀ 0.5% 262M 0B/s 0B/s ⠀⠀⠀⠀⢀ │
|
|
36
|
+
# │ Mods in Desktop vs Code… ⠀⠀⠀⠀⢀ 0.3% 226M 0B/s 0B/s ⠀⠀⠀⠀⢀ │
|
|
37
|
+
# │ Set up a Homebrew tap for… ⠀⠀⠀⠀⢀ 0.1% 200M 0B/s 0B/s ⠀⠀⠀⠀⢀ │
|
|
38
|
+
# │ Weekly session themes review… ⠀⠀⠀⠀⢀ 0.1% 181M 0B/s 0B/s 0B/s ⠀⠀⠀⠀⢀ 0B/s│
|
|
39
|
+
# │ codex 3064 · Resources ⠀⠀⠀⠀⢀ 0.0% 76M 0B/s 0B/s 0B/s ⠀⠀⠀⠀⢀ 0B/s│
|
|
40
|
+
# │ Build Magrathea local POC +8… ⠀⠀⠀⠀⢀ 0.0% 1022M 0B/s 0B/s 0B/s ⠀⠀⠀⠀⢀ 0B/s│
|
|
41
|
+
# │ │
|
|
42
|
+
# ... 6 more empty table lines ...
|
|
43
|
+
# ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
44
|
+
# ╭─ All agents ───────────────────────────────────╮╭─ Agents network ───────────────────────────────╮
|
|
45
|
+
# │CPU 5.0%││net in 0B/s│
|
|
46
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
|
|
47
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
|
|
48
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
|
|
49
|
+
# │footprint 9.6G││net out 0B/s│
|
|
50
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
|
|
51
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
|
|
52
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
|
|
53
|
+
# ╰────────────────────────────────────────────────╯╰────────────────────────────────────────────────╯
|
|
54
|
+
# sessions · ⏎ open a session
|
|
55
|
+
#
|
|
56
|
+
# Drilled into "Agentmon r2ui integration" (a pty at 100×50: `/` integration, Enter). The RAM
|
|
57
|
+
# meter is the machine (25G of 32G used) with this session's footprint as the accent slice at the
|
|
58
|
+
# left, the rest of used memory dim and free memory empty:
|
|
59
|
+
#
|
|
60
|
+
# ╭─ CPU ──────────────────────────────────────────╮╭─ Memory ───────────────────────────────────────╮
|
|
61
|
+
# │CPU ▕ ▏ 0.1%││RAM ▕▏█████████████▉ ▏ 323M 1% · 25G/32G│
|
|
62
|
+
# │last 4 min 0.1%││pressure ▕█████████▏ ▏ 48.0%│
|
|
63
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀││resident 439M wired 0B│
|
|
64
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀││growth 0B/s pageins 0/s│
|
|
65
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀││footprint, last 4 min 323M│
|
|
66
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿│
|
|
67
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿│
|
|
68
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿│
|
|
69
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿│
|
|
70
|
+
# ╰────────────────────────────────────────────────╯╰────────────────────────────────────────────────╯
|
|
71
|
+
# ╭─ Network ──────────────────────────────────────╮╭─ Disk ─────────────────────────────────────────╮
|
|
72
|
+
# │net in 337B/s││read 0B/s│
|
|
73
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
|
|
74
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡀⢸││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
|
|
75
|
+
# │net out 647B/s││write 0B/s│
|
|
76
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
|
|
77
|
+
# │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸││⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
|
|
78
|
+
# ╰────────────────────────────────────────────────╯╰────────────────────────────────────────────────╯
|
|
79
|
+
# ╭─ Processes ─[Agents] All Busy Heavy Writing Waiting ─╮╭─ Detail ─────────────────────────────╮
|
|
80
|
+
# │ Name CPU▼ Footprint Net out ││claude pid 22602 │
|
|
81
|
+
# │ claude ⣀⣀⣀⣀⣀ 1.0% 320M 647B/s ││Command /Users/ryan/Library… │
|
|
82
|
+
# │▴sleep ⠀⠀⠀⠀⣀ 0.0% 832K 0B/s ││Directory ~/The Source/agentmon │
|
|
83
|
+
# │▾zsh ⣀⣀⣀⣀⣀ 0.0% 2.5M 0B/s ││Session Agentmon r2ui integration… │
|
|
84
|
+
# │ ││Footprint 320M │
|
|
85
|
+
# ... 16 more lines: the Detail pairs beside empty table lines ...
|
|
86
|
+
# ╰──────────────────────────────────────────────────────────╯╰──────────────────────────────────────╯
|
|
87
|
+
# ╭─ Connections ────────────────────────────────────────────────────────────────────────────────────╮
|
|
88
|
+
# │ Process Remote State In Out▼ Recv Sent│
|
|
89
|
+
# │▴claude 22602 160.79.104.10:443 Established 337B/s 647B/s 15K 28K│
|
|
90
|
+
# │▾claude 22602 160.79.104.10:443 Established 0B/s 0B/s 39K 2.9M│
|
|
91
|
+
# │▾claude 22602 160.79.104.10:443 Established 0B/s 0B/s 918K 4.5M│
|
|
92
|
+
# │▾claude 22602 160.79.104.10:443 Established 0B/s 0B/s 234K 540K│
|
|
93
|
+
# │▾claude 22602 ….bc.googleusercontent.com:443 Established 0B/s 0B/s 26K 8.3M│
|
|
94
|
+
# ╰───────────────────────────────────────────────────────────────────────────────────────────── 2/6 ╯
|
|
95
|
+
# ▸ Agentmon r2ui integration · claude 22602 · agentmon · busy · esc back to sessions
|
|
96
|
+
#
|
|
97
|
+
# The Sessions table leaves Procs off so disk read and write both fit at 100 columns (its label,
|
|
98
|
+
# the session's name first, is 30 wide); footprint has no sparkline (a flat footprint draws a full
|
|
99
|
+
# braille column, not a baseline).
|
|
100
|
+
module Agentmon
|
|
101
|
+
view :visual, title: "agentmon" do
|
|
102
|
+
# Home: the machine's breakdown, then every agent session.
|
|
103
|
+
row height: 11 do
|
|
104
|
+
panel :cpu, title: "CPU" do
|
|
105
|
+
meter "system.cpu"
|
|
106
|
+
trend "system.cpu", height: 6, label: "last 4 min"
|
|
107
|
+
stat "system.load1", "system.load5", "system.load15", columns: 3
|
|
108
|
+
end
|
|
109
|
+
panel :memory, title: "Memory" do
|
|
110
|
+
meter "memory.used"
|
|
111
|
+
meter "memory.pressure"
|
|
112
|
+
meter "memory.swap_used"
|
|
113
|
+
stat "memory.app", "memory.wired", "memory.compressed", "memory.cached", columns: 2
|
|
114
|
+
trend "memory.used", height: nil, label: "used, last 4 min"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
row height: 8 do
|
|
118
|
+
panel :network, title: "Network" do
|
|
119
|
+
trend "system.net_in_rate", "system.net_out_rate", height: nil
|
|
120
|
+
end
|
|
121
|
+
panel :disk, title: "Disk" do
|
|
122
|
+
trend "system.disk_read_rate", "system.disk_write_rate", height: nil
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
row do
|
|
126
|
+
top :session, title: "Sessions", by: :cpu, spark: %i[cpu net_out_rate],
|
|
127
|
+
columns: %i[label cpu footprint read_rate write_rate net_in_rate net_out_rate],
|
|
128
|
+
widths: { label: 30 }
|
|
129
|
+
end
|
|
130
|
+
row height: 10 do
|
|
131
|
+
panel :agents, title: "All agents" do
|
|
132
|
+
trend "focus.cpu", "focus.footprint", height: nil
|
|
133
|
+
end
|
|
134
|
+
panel :agents_net, title: "Agents network" do
|
|
135
|
+
trend "focus.net_in_rate", "focus.net_out_rate", height: nil
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Drilled into one session: its CPU, memory, network and disk, processes, connections.
|
|
140
|
+
focused do
|
|
141
|
+
row height: 11 do
|
|
142
|
+
panel :s_cpu, title: "CPU" do
|
|
143
|
+
meter "focus.cpu"
|
|
144
|
+
trend "focus.cpu", height: nil, label: "last 4 min"
|
|
145
|
+
stat "focus.processes", "focus.read_rate", "focus.write_rate", columns: 3
|
|
146
|
+
end
|
|
147
|
+
panel :s_memory, title: "Memory" do
|
|
148
|
+
meter "memory.used", part: "focus.footprint", label: "RAM" # the session's slice of the machine
|
|
149
|
+
meter "memory.pressure"
|
|
150
|
+
stat "focus.resident", "focus.wired", "focus.growth_rate", "focus.pagein_rate", columns: 2
|
|
151
|
+
trend "focus.footprint", height: nil, label: "footprint, last 4 min"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
row height: 8 do
|
|
155
|
+
panel :s_net, title: "Network" do
|
|
156
|
+
trend "focus.net_in_rate", "focus.net_out_rate", height: nil
|
|
157
|
+
end
|
|
158
|
+
panel :s_disk, title: "Disk" do
|
|
159
|
+
trend "focus.read_rate", "focus.write_rate", height: nil
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
row do
|
|
163
|
+
top :process, span: 3, by: :cpu, columns: %i[name cpu footprint net_out_rate], widths: { name: 20 }
|
|
164
|
+
detail :process, span: 2
|
|
165
|
+
end
|
|
166
|
+
row height: 8 do
|
|
167
|
+
top :connection, title: "Connections", by: :out_rate, spark: false, widths: { process: 18 },
|
|
168
|
+
columns: %i[process remote_text state in_rate out_rate bytes_in bytes_out]
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "r2ui"
|
|
4
|
+
|
|
5
|
+
module Agentmon
|
|
6
|
+
module Views
|
|
7
|
+
# The drawing layer of the performance-view DSL (docs/views.md, "The DSL"): pure functions,
|
|
8
|
+
# Strings in, Strings out, no app, engine or state. Every line returned is exactly `width`
|
|
9
|
+
# visible cells (SGR escapes count zero, wide characters two), padded with spaces, never more.
|
|
10
|
+
# Colour is 24-bit SGR through R2UI's Glyphs: percents take heat, bytes the accent, unknown
|
|
11
|
+
# values show "–" in dim.
|
|
12
|
+
#
|
|
13
|
+
# The examples below are the plain text (escapes stripped).
|
|
14
|
+
#
|
|
15
|
+
# cut("claude 74432 · agentmon", 15) # => "claude 74432…"
|
|
16
|
+
# cut("bare-modifier-monitor", 16) # => "bare-modifier…"
|
|
17
|
+
# cut("~/src/very/long/repo", 12, from: :left) # => "…/long/repo"
|
|
18
|
+
# meter(label: "busy", text: "42%", fraction: 0.42, width: 20)
|
|
19
|
+
# # => "busy ▕███▊ ▏ 42%"
|
|
20
|
+
# meter(label: "busy", text: nil, fraction: nil, width: 16)
|
|
21
|
+
# # => "busy ▕ ▏ –"
|
|
22
|
+
# stacked_meter(label: "RAM", text: "1G", part: 0.25, fraction: 0.5, width: 16)
|
|
23
|
+
# # => "RAM ▕█▊█▌ ▏ 1G" (█▊ accent, █▌ dim)
|
|
24
|
+
# spark(label: "cpu", values: [1, 2, 3, 4], text: "4%", width: 12)
|
|
25
|
+
# # => "cpu ⠀⠀⠀⣠⣾ 4%"
|
|
26
|
+
# trend(label: "CPU", text: "42%", values: [1, 2, 3, 4], width: 10, height: 1)
|
|
27
|
+
# # => ["CPU 42%", "⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾"]
|
|
28
|
+
# stat(pairs: [["load1", "2.1", nil], ["load5", nil, nil]], width: 24)
|
|
29
|
+
# # => ["load1 2.1 load5 –"]
|
|
30
|
+
# tile(title: "claude 4242", body: ["cpu 12%"], width: 18, height: 3)
|
|
31
|
+
# # => ["╭─ claude 4242 ──╮",
|
|
32
|
+
# # "│cpu 12% │",
|
|
33
|
+
# # "╰────────────────╯"]
|
|
34
|
+
#
|
|
35
|
+
# value_sgr picks the SGR params for a value: heat for a percent with a fraction, the accent
|
|
36
|
+
# for bytes and byte rates, nil otherwise, dim when unknown; a pulse (0..1) mixes the colour
|
|
37
|
+
# toward PULSE and turns bold above 0.3.
|
|
38
|
+
module Widgets
|
|
39
|
+
G = R2UI::Widgets::Glyphs
|
|
40
|
+
ACCENT = "#D97757"
|
|
41
|
+
DIM = "#555555"
|
|
42
|
+
TROUGH = "#303030"
|
|
43
|
+
PULSE = "#FFE0C2"
|
|
44
|
+
PLAIN = "#BCBCBC"
|
|
45
|
+
MUTED = "#8A8A8A"
|
|
46
|
+
UNKNOWN = "–"
|
|
47
|
+
ELLIPSIS = "…"
|
|
48
|
+
SEPARATORS = [" ", "·", "-", ",", ":", "/"].freeze
|
|
49
|
+
BYTE_UNITS = %i[bytes bytes_per_sec signed_bytes_per_sec].freeze
|
|
50
|
+
SGR = /\e\[[0-9;?]*[ -\/]*[@-~]/
|
|
51
|
+
TOKEN = /\e\[[0-9;?]*[ -\/]*[@-~]|\X/
|
|
52
|
+
|
|
53
|
+
module_function
|
|
54
|
+
|
|
55
|
+
# Visible cells of `str`: SGR escapes are zero-width, wide characters two.
|
|
56
|
+
def visible_width(str)
|
|
57
|
+
R2UI::Compat::Tea::ANSI.string_width(str.to_s.gsub(SGR, ""))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# `str` padded with spaces to `width` cells (right-aligned with align: :right). Never cuts.
|
|
61
|
+
def pad(str, width, align: :left)
|
|
62
|
+
str = str.to_s
|
|
63
|
+
gap = width - visible_width(str)
|
|
64
|
+
return str if gap <= 0
|
|
65
|
+
|
|
66
|
+
align == :right ? (" " * gap) + str : str + (" " * gap)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Plain `text` cut to at most `width` cells, the ellipsis at a word boundary when one exists.
|
|
70
|
+
def cut(text, width, from: :right)
|
|
71
|
+
text = text.to_s
|
|
72
|
+
return "" if width <= 0
|
|
73
|
+
return text if visible_width(text) <= width
|
|
74
|
+
return ELLIPSIS if width == 1
|
|
75
|
+
|
|
76
|
+
from == :left ? cut_left(text, width) : cut_right(text, width)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# SGR params (or nil) for a value of `unit`; see the module comment.
|
|
80
|
+
def value_sgr(unit:, fraction: nil, pulse: 0.0, known: true)
|
|
81
|
+
return G.fg(DIM) unless known
|
|
82
|
+
|
|
83
|
+
heat = unit == :percent && !fraction.nil?
|
|
84
|
+
bytes = BYTE_UNITS.include?(unit)
|
|
85
|
+
pulse = pulse.to_f.clamp(0.0, 1.0)
|
|
86
|
+
if pulse.positive?
|
|
87
|
+
base = if heat then G.gradient(G::HEAT, fraction.to_f.clamp(0.0, 1.0))
|
|
88
|
+
elsif bytes then ACCENT
|
|
89
|
+
else PLAIN
|
|
90
|
+
end
|
|
91
|
+
sgr = G.fg(R2UI::Motion.mix_hex(base, PULSE, pulse))
|
|
92
|
+
return pulse > 0.3 ? "#{sgr};1" : sgr
|
|
93
|
+
end
|
|
94
|
+
return G.heat(fraction) if heat
|
|
95
|
+
return G.fg(ACCENT) if bytes
|
|
96
|
+
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# One line: `label ▕bar▏ text`; without room for a 3-cell bar, label left and text right.
|
|
101
|
+
def meter(label:, text:, fraction:, width:, label_width: nil, text_width: nil, bar_sgr: nil, text_sgr: nil)
|
|
102
|
+
return "" if width <= 0
|
|
103
|
+
|
|
104
|
+
known = !fraction.nil? && !text.nil?
|
|
105
|
+
text, text_sgr = UNKNOWN, G.fg(DIM) unless known
|
|
106
|
+
label = label.to_s
|
|
107
|
+
text = text.to_s
|
|
108
|
+
label_w = label_width || visible_width(label)
|
|
109
|
+
text_w = text_width || visible_width(text)
|
|
110
|
+
bw = width - label_w - text_w - 4
|
|
111
|
+
return sides(label, nil, text, text_sgr, width) if bw < 3
|
|
112
|
+
|
|
113
|
+
fraction = known ? fraction.to_f.clamp(0.0, 1.0) : 0.0
|
|
114
|
+
bar_sgr ||= known ? G.heat(fraction) : G.fg(DIM)
|
|
115
|
+
meter_line(label, label_w, G.paint(G.bar(fraction, bw), "#{bar_sgr};#{G.bg(TROUGH)}"), text, text_sgr,
|
|
116
|
+
text_w, width)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# A meter whose bar stacks a part of the whole: `label ▕PPrrr ▏ text`, from the left `part`
|
|
120
|
+
# (0..1 of the whole) in `part_hex`, the rest of `fraction` in `rest_hex`, the trough after
|
|
121
|
+
# (stacked_bar). Unknown part, fraction or text draws as an unknown meter.
|
|
122
|
+
#
|
|
123
|
+
# stacked_meter(label: "RAM", text: "1G 6% · 9G/16G", part: 0.25, fraction: 0.5, width: 30)
|
|
124
|
+
# # => "RAM ▕██▎█▌ ▏ 1G 6% · 9G/16G" (plain)
|
|
125
|
+
def stacked_meter(label:, text:, part:, fraction:, width:, label_width: nil, text_width: nil,
|
|
126
|
+
part_hex: ACCENT, rest_hex: DIM, text_sgr: nil)
|
|
127
|
+
return "" if width <= 0
|
|
128
|
+
if part.nil? || fraction.nil? || text.nil?
|
|
129
|
+
return meter(label:, text: nil, fraction: nil, width:, label_width:, text_width:)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
label = label.to_s
|
|
133
|
+
text = text.to_s
|
|
134
|
+
label_w = label_width || visible_width(label)
|
|
135
|
+
text_w = text_width || visible_width(text)
|
|
136
|
+
bw = width - label_w - text_w - 4
|
|
137
|
+
return sides(label, nil, text, text_sgr, width) if bw < 3
|
|
138
|
+
|
|
139
|
+
meter_line(label, label_w, stacked_bar(part, fraction, bw, part_hex:, rest_hex:), text, text_sgr, text_w,
|
|
140
|
+
width)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# `width` cells: `part` then the rest of `fraction` (both 0..1 of the whole; part is clamped to
|
|
144
|
+
# fraction) then the trough. Eighth-block precise: a boundary cell draws a left partial block
|
|
145
|
+
# (▏..▉) in the left segment's colour over the right segment's colour as its background. A
|
|
146
|
+
# part above zero shows at least one eighth. When both boundaries fall in one cell the part
|
|
147
|
+
# keeps its eighths and the cell's background is whichever of rest or trough covers more of it.
|
|
148
|
+
def stacked_bar(part, fraction, width, part_hex: ACCENT, rest_hex: DIM)
|
|
149
|
+
return "" if width <= 0
|
|
150
|
+
|
|
151
|
+
whole = fraction.to_f.clamp(0.0, 1.0)
|
|
152
|
+
part = part.to_f.clamp(0.0, whole)
|
|
153
|
+
used_e = (whole * width * 8).round
|
|
154
|
+
part_e = (part * width * 8).round
|
|
155
|
+
part_e = 1 if part_e.zero? && part.positive?
|
|
156
|
+
used_e = part_e if used_e < part_e
|
|
157
|
+
runs = []
|
|
158
|
+
width.times do |i|
|
|
159
|
+
glyph, fg, bg = stacked_cell((part_e - (8 * i)).clamp(0, 8), (used_e - (8 * i)).clamp(0, 8),
|
|
160
|
+
part_hex, rest_hex)
|
|
161
|
+
sgr = [fg && G.fg(fg), G.bg(bg)].compact.join(";")
|
|
162
|
+
if runs.last && runs.last[1] == sgr
|
|
163
|
+
runs.last[0] << glyph
|
|
164
|
+
else
|
|
165
|
+
runs << [+glyph, sgr]
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
runs.map { |glyphs, sgr| G.paint(glyphs, sgr) }.join
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# One stacked_bar cell with `a` eighths of part and `b` eighths of part + rest: [glyph, fg, bg].
|
|
172
|
+
def stacked_cell(a, b, part_hex, rest_hex)
|
|
173
|
+
if a == 8 then ["█", part_hex, TROUGH]
|
|
174
|
+
elsif a.positive? then [G::PARTIAL[a - 1], part_hex, b - a > 8 - b ? rest_hex : TROUGH]
|
|
175
|
+
elsif b == 8 then ["█", rest_hex, TROUGH]
|
|
176
|
+
elsif b.positive? then [G::PARTIAL[b - 1], rest_hex, TROUGH]
|
|
177
|
+
else [" ", nil, TROUGH]
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# `label ▕bar▏ text` with the label and text padded to their widths, fitted to `width`.
|
|
182
|
+
def meter_line(label, label_w, bar, text, text_sgr, text_w, width)
|
|
183
|
+
line = pad(cut(label, label_w), label_w) + " " + G.paint("▕", G.fg(DIM)) + bar + G.paint("▏", G.fg(DIM)) +
|
|
184
|
+
" " + pad(G.paint(text, text_sgr), text_w, align: :right)
|
|
185
|
+
fit(line, width)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# One line: `label ⣀⣠⣴⣾ text` (braille sparkline, newest at the right).
|
|
189
|
+
def spark(label:, values:, text:, width:, max: nil, label_width: nil, text_width: nil, chart_sgr: nil, text_sgr: nil)
|
|
190
|
+
return "" if width <= 0
|
|
191
|
+
|
|
192
|
+
text, text_sgr = UNKNOWN, G.fg(DIM) if text.nil?
|
|
193
|
+
label = label.to_s
|
|
194
|
+
text = text.to_s
|
|
195
|
+
label_w = label_width || visible_width(label)
|
|
196
|
+
text_w = text_width || visible_width(text)
|
|
197
|
+
sw = width - label_w - text_w - 2
|
|
198
|
+
return sides(label, nil, text, text_sgr, width) if sw < 2
|
|
199
|
+
|
|
200
|
+
values = Array(values)
|
|
201
|
+
values = Array.new(sw * 2, 0) if values.size < 2
|
|
202
|
+
chart = G.braille_line(values, sw, max: max)
|
|
203
|
+
line = pad(cut(label, label_w), label_w) + " " + G.paint(chart, chart_sgr || G.fg(ACCENT)) +
|
|
204
|
+
" " + pad(G.paint(text, text_sgr), text_w, align: :right)
|
|
205
|
+
fit(line, width)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# height + 1 lines: a header (label left, text right) then a braille area chart.
|
|
209
|
+
def trend(label:, text:, values:, width:, height:, max: nil, chart_sgr: nil, text_sgr: nil)
|
|
210
|
+
return [] if width <= 0
|
|
211
|
+
|
|
212
|
+
text, text_sgr = UNKNOWN, G.fg(DIM) if text.nil?
|
|
213
|
+
header = sides(label.to_s, G.fg(MUTED), text.to_s, text_sgr, width)
|
|
214
|
+
return [header] if height <= 0
|
|
215
|
+
|
|
216
|
+
area = G.braille_area(Array(values), width, height, max: max)
|
|
217
|
+
[header] + area.map { |row| fit(G.paint(row, chart_sgr || G.fg(ACCENT)), width) }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# A label/value grid, `columns` cells per line; pairs are [label, text_or_nil, sgr_or_nil].
|
|
221
|
+
def stat(pairs:, width:, columns: 2, gap: 2)
|
|
222
|
+
return [] if width <= 0 || pairs.nil? || pairs.empty?
|
|
223
|
+
|
|
224
|
+
columns = [columns.to_i, 1].max
|
|
225
|
+
gap = [gap.to_i, 0].max
|
|
226
|
+
cell_w = (width - (gap * (columns - 1))) / columns
|
|
227
|
+
pairs.each_slice(columns).map do |row|
|
|
228
|
+
next " " * width if cell_w <= 0
|
|
229
|
+
|
|
230
|
+
cells = row.map do |label, text, sgr|
|
|
231
|
+
text, sgr = UNKNOWN, G.fg(DIM) if text.nil?
|
|
232
|
+
sides(label.to_s, G.fg(MUTED), text.to_s, sgr, cell_w)
|
|
233
|
+
end
|
|
234
|
+
fit(cells.join(" " * gap), width)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Exactly `height` lines: a rounded border with `title` in the top edge around `body`.
|
|
239
|
+
def tile(title:, body:, width:, height:, border_sgr: nil, title_sgr: nil)
|
|
240
|
+
return [] if height < 2
|
|
241
|
+
return Array.new(height, " " * [width, 0].max) if width < 4
|
|
242
|
+
|
|
243
|
+
inner = width - 2
|
|
244
|
+
room = inner - 3
|
|
245
|
+
title = room.positive? ? cut(title.to_s, room) : ""
|
|
246
|
+
top = if title.empty?
|
|
247
|
+
G.paint("╭#{"─" * inner}╮", border_sgr)
|
|
248
|
+
else
|
|
249
|
+
G.paint("╭─ ", border_sgr) + G.paint(title, title_sgr) +
|
|
250
|
+
G.paint(" #{"─" * (room - visible_width(title))}╮", border_sgr)
|
|
251
|
+
end
|
|
252
|
+
body = Array(body)
|
|
253
|
+
rows = Array.new(height - 2) do |i|
|
|
254
|
+
G.paint("│", border_sgr) + fit(body[i].to_s, inner) + G.paint("│", border_sgr)
|
|
255
|
+
end
|
|
256
|
+
[top, *rows, G.paint("╰#{"─" * inner}╯", border_sgr)]
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# ANSI-aware: the first `width` visible cells of `str`, SGR runs kept, re-closed with a reset.
|
|
260
|
+
def truncate(str, width)
|
|
261
|
+
str = str.to_s
|
|
262
|
+
return str if visible_width(str) <= width
|
|
263
|
+
|
|
264
|
+
out = +""
|
|
265
|
+
used = 0
|
|
266
|
+
styled = false
|
|
267
|
+
str.scan(TOKEN) do |tok|
|
|
268
|
+
if tok.start_with?("\e[")
|
|
269
|
+
out << tok
|
|
270
|
+
styled = true
|
|
271
|
+
next
|
|
272
|
+
end
|
|
273
|
+
w = R2UI::Compat::Tea::ANSI.string_width(tok)
|
|
274
|
+
break if used + w > width
|
|
275
|
+
|
|
276
|
+
out << tok
|
|
277
|
+
used += w
|
|
278
|
+
end
|
|
279
|
+
styled ? out << "\e[0m" : out
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# `str` truncated then padded to exactly `width` cells.
|
|
283
|
+
def fit(str, width)
|
|
284
|
+
return "" if width <= 0
|
|
285
|
+
|
|
286
|
+
pad(truncate(str, width), width)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# `left` and `right` on one `width`-cell line; the right side always shows (cut only when it
|
|
290
|
+
# alone is wider than the line), the left is cut by the word rule to fit beside it.
|
|
291
|
+
def sides(left, left_sgr, right, right_sgr, width)
|
|
292
|
+
return "" if width <= 0
|
|
293
|
+
|
|
294
|
+
right = cut(right, width)
|
|
295
|
+
rw = visible_width(right)
|
|
296
|
+
left = cut(left, width - rw - 1)
|
|
297
|
+
lw = visible_width(left)
|
|
298
|
+
gap = width - lw - rw
|
|
299
|
+
fit(G.paint(left, left.empty? ? nil : left_sgr) + (" " * gap) + G.paint(right, right_sgr), width)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def cut_right(text, width)
|
|
303
|
+
best = nil
|
|
304
|
+
text.each_char.with_index do |ch, i|
|
|
305
|
+
next unless SEPARATORS.include?(ch) # "bare-modifier-monitor" → "bare-modifier…"
|
|
306
|
+
|
|
307
|
+
prefix = strip_tail(text[0, i])
|
|
308
|
+
best = prefix if !prefix.empty? && visible_width(prefix) + 1 <= width
|
|
309
|
+
end
|
|
310
|
+
(best || take(text, width - 1)) + ELLIPSIS
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def cut_left(text, width)
|
|
314
|
+
best = nil
|
|
315
|
+
text.each_char.with_index do |ch, i|
|
|
316
|
+
next unless ch == "/" || ch == " "
|
|
317
|
+
|
|
318
|
+
suffix = ch == "/" ? text[i..] : strip_head(text[i..])
|
|
319
|
+
next if suffix.empty? || visible_width(suffix) + 1 > width
|
|
320
|
+
|
|
321
|
+
best = suffix
|
|
322
|
+
break
|
|
323
|
+
end
|
|
324
|
+
ELLIPSIS + (best || take(text.reverse, width - 1).reverse)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def strip_tail(str)
|
|
328
|
+
str = str.chop while !str.empty? && SEPARATORS.include?(str[-1])
|
|
329
|
+
str
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def strip_head(str)
|
|
333
|
+
str = str[1..] while !str.empty? && SEPARATORS.include?(str[0])
|
|
334
|
+
str
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# The longest leading run of grapheme clusters of plain `str` within `width` cells.
|
|
338
|
+
def take(str, width)
|
|
339
|
+
out = +""
|
|
340
|
+
used = 0
|
|
341
|
+
str.each_grapheme_cluster do |g|
|
|
342
|
+
w = R2UI::Compat::Tea::ANSI.string_width(g)
|
|
343
|
+
break if used + w > width
|
|
344
|
+
|
|
345
|
+
out << g
|
|
346
|
+
used += w
|
|
347
|
+
end
|
|
348
|
+
out
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|