patchbai 0.1.0__tar.gz → 0.1.2__tar.gz
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.
- {patchbai-0.1.0 → patchbai-0.1.2}/PKG-INFO +18 -17
- {patchbai-0.1.0 → patchbai-0.1.2}/README.md +17 -16
- patchbai-0.1.2/patchbai/__init__.py +1 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/app.py +12 -0
- patchbai-0.1.2/patchbai/widgets/system_usage.py +244 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/pyproject.toml +1 -1
- patchbai-0.1.0/patchbai/__init__.py +0 -1
- {patchbai-0.1.0 → patchbai-0.1.2}/.gitignore +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/LICENSE +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/__main__.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/actions.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/activity/__init__.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/activity/log.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/agents/__init__.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/agents/child_tools.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/agents/fake_sdk_adapter.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/agents/manager.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/agents/request_inbox.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/agents/sdk_adapter.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/agents/session.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/agents/sort.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/agents/state.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/config.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/events.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/layout/__init__.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/layout/custom_widgets.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/layout/defaults.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/layout/engine.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/layout/local_widgets.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/layout/registry.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/layout/spec.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/layout/splitter.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/layout/titles.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/orchestrator/__init__.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/orchestrator/formatting.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/orchestrator/session.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/orchestrator/tabs_tools.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/orchestrator/tools.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/__init__.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/agents_index.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/atomic.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/layout_store.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/layouts_store.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/orchestrator_sessions.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/paths.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/themes_store.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/transcript_store.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/workspace_store.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/theme/__init__.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/theme/engine.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/theme/spec.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/__init__.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/_file_lang.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/_terminal_keys.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/_terminal_render.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/activity_feed.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/agent_table.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/agent_transcript.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/change_cwd_screen.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/chrome.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/diff_viewer.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/file_editor.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/file_tree.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/file_viewer.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/history_screen.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/layout_switcher.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/log_tail.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/markdown.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/new_tab_screen.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/notebook.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/orchestrator_chat.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/resume_screen.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/rich_transcript.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/terminal.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/theme_switcher.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/transcript_screen.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/workspace/__init__.py +0 -0
- {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/workspace/spec.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchbai
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: A Textual TUI for managing multiple Claude Code agent sessions
|
|
5
5
|
Project-URL: Homepage, https://github.com/jimmymills/patchbai
|
|
6
6
|
Project-URL: Repository, https://github.com/jimmymills/patchbai
|
|
@@ -294,36 +294,37 @@ repo's `.gitignore` and you should add it to yours.
|
|
|
294
294
|
uses your `~/.claude/settings.json` for permissions and tool allowlists.
|
|
295
295
|
- A terminal with TrueColor support (any modern macOS / Linux terminal).
|
|
296
296
|
|
|
297
|
-
###
|
|
297
|
+
### From PyPI (recommended)
|
|
298
298
|
|
|
299
299
|
```bash
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
uv sync # install runtime deps into .venv
|
|
303
|
-
uv run patchbai # or: uv run mt
|
|
300
|
+
pipx install patchbai # isolated, on PATH
|
|
301
|
+
patchbai # or: mt
|
|
304
302
|
```
|
|
305
303
|
|
|
306
|
-
|
|
304
|
+
Or with `uv`:
|
|
307
305
|
|
|
308
306
|
```bash
|
|
309
|
-
uv
|
|
310
|
-
|
|
311
|
-
./scripts/typecheck.sh # canonical pyright invocation
|
|
307
|
+
uv tool install patchbai
|
|
308
|
+
patchbai
|
|
312
309
|
```
|
|
313
310
|
|
|
314
|
-
|
|
311
|
+
Or with plain `pip` into a venv:
|
|
315
312
|
|
|
316
313
|
```bash
|
|
317
|
-
|
|
318
|
-
|
|
314
|
+
python -m venv .venv && source .venv/bin/activate
|
|
315
|
+
pip install patchbai
|
|
316
|
+
patchbai
|
|
319
317
|
```
|
|
320
318
|
|
|
321
|
-
###
|
|
319
|
+
### From source (for hacking on patchbai itself)
|
|
322
320
|
|
|
323
321
|
```bash
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
322
|
+
git clone https://github.com/jimmymills/patchbai.git
|
|
323
|
+
cd patchbai
|
|
324
|
+
uv sync --extra dev # runtime + dev deps (pyright, pytest)
|
|
325
|
+
uv run patchbai # or: uv run mt
|
|
326
|
+
uv run pytest
|
|
327
|
+
./scripts/typecheck.sh # canonical pyright invocation
|
|
327
328
|
```
|
|
328
329
|
|
|
329
330
|
## Running
|
|
@@ -259,36 +259,37 @@ repo's `.gitignore` and you should add it to yours.
|
|
|
259
259
|
uses your `~/.claude/settings.json` for permissions and tool allowlists.
|
|
260
260
|
- A terminal with TrueColor support (any modern macOS / Linux terminal).
|
|
261
261
|
|
|
262
|
-
###
|
|
262
|
+
### From PyPI (recommended)
|
|
263
263
|
|
|
264
264
|
```bash
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
uv sync # install runtime deps into .venv
|
|
268
|
-
uv run patchbai # or: uv run mt
|
|
265
|
+
pipx install patchbai # isolated, on PATH
|
|
266
|
+
patchbai # or: mt
|
|
269
267
|
```
|
|
270
268
|
|
|
271
|
-
|
|
269
|
+
Or with `uv`:
|
|
272
270
|
|
|
273
271
|
```bash
|
|
274
|
-
uv
|
|
275
|
-
|
|
276
|
-
./scripts/typecheck.sh # canonical pyright invocation
|
|
272
|
+
uv tool install patchbai
|
|
273
|
+
patchbai
|
|
277
274
|
```
|
|
278
275
|
|
|
279
|
-
|
|
276
|
+
Or with plain `pip` into a venv:
|
|
280
277
|
|
|
281
278
|
```bash
|
|
282
|
-
|
|
283
|
-
|
|
279
|
+
python -m venv .venv && source .venv/bin/activate
|
|
280
|
+
pip install patchbai
|
|
281
|
+
patchbai
|
|
284
282
|
```
|
|
285
283
|
|
|
286
|
-
###
|
|
284
|
+
### From source (for hacking on patchbai itself)
|
|
287
285
|
|
|
288
286
|
```bash
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
287
|
+
git clone https://github.com/jimmymills/patchbai.git
|
|
288
|
+
cd patchbai
|
|
289
|
+
uv sync --extra dev # runtime + dev deps (pyright, pytest)
|
|
290
|
+
uv run patchbai # or: uv run mt
|
|
291
|
+
uv run pytest
|
|
292
|
+
./scripts/typecheck.sh # canonical pyright invocation
|
|
292
293
|
```
|
|
293
294
|
|
|
294
295
|
## Running
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.1"
|
|
@@ -43,6 +43,7 @@ from patchbai.widgets.log_tail import LogTail
|
|
|
43
43
|
from patchbai.widgets.notebook import Notebook
|
|
44
44
|
from patchbai.widgets.file_viewer import FileViewer
|
|
45
45
|
from patchbai.widgets.markdown import Markdown
|
|
46
|
+
from patchbai.widgets.system_usage import SystemUsage
|
|
46
47
|
from patchbai.persistence.orchestrator_sessions import OrchestratorSessionsIndex
|
|
47
48
|
from patchbai.widgets.history_screen import HistoryScreen
|
|
48
49
|
from patchbai.widgets.layout_switcher import LayoutSwitcherScreen
|
|
@@ -222,6 +223,17 @@ def build_default_registry() -> WidgetRegistry:
|
|
|
222
223
|
),
|
|
223
224
|
props_schema={"command": list, "cwd": str, "env": dict},
|
|
224
225
|
)
|
|
226
|
+
reg.register(
|
|
227
|
+
"SystemUsage", SystemUsage,
|
|
228
|
+
description=(
|
|
229
|
+
"Compact single-row CPU and RAM gauges with colored bars "
|
|
230
|
+
"(green / yellow / red at 50% / 80% thresholds). Auto-refreshes "
|
|
231
|
+
"every `interval` seconds. Uses `psutil` when available; "
|
|
232
|
+
"otherwise shells out non-blockingly to `top` and `vm_stat` on "
|
|
233
|
+
"macOS. `bar_width` controls the width of each progress bar."
|
|
234
|
+
),
|
|
235
|
+
props_schema={"interval": float, "bar_width": int},
|
|
236
|
+
)
|
|
225
237
|
return reg
|
|
226
238
|
|
|
227
239
|
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""SystemUsage widget — compact CPU + RAM gauges with auto-refresh.
|
|
2
|
+
|
|
3
|
+
Single-row layout: ``CPU 23.4% [▰▰▰▱▱…] RAM 8.4/16.0 GiB [▰▰▰▰…]``.
|
|
4
|
+
|
|
5
|
+
`psutil` is preferred when present (cleanest, cross-platform). When absent
|
|
6
|
+
the widget falls back to non-blocking ``top -l 1 -n 0`` + ``vm_stat`` +
|
|
7
|
+
``sysctl hw.memsize`` shell-outs on macOS via
|
|
8
|
+
``asyncio.create_subprocess_exec`` so the Textual event loop is never
|
|
9
|
+
blocked. On unsupported platforms without psutil, the widget renders an
|
|
10
|
+
error banner and stops scheduling refreshes.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
|
|
20
|
+
from textual.widgets import Static
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Try psutil but never require it — patchbai's deps stay minimal. If a user
|
|
24
|
+
# happens to have psutil in their venv (e.g. via a transitive dep), we use
|
|
25
|
+
# it; otherwise we shell out on macOS.
|
|
26
|
+
try:
|
|
27
|
+
import psutil # type: ignore
|
|
28
|
+
_HAS_PSUTIL = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
psutil = None # type: ignore
|
|
31
|
+
_HAS_PSUTIL = False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_FILLED = "▰"
|
|
35
|
+
_EMPTY = "▱"
|
|
36
|
+
_GIB = 1024 ** 3
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class _Sample:
|
|
41
|
+
cpu_pct: float
|
|
42
|
+
ram_used_gib: float
|
|
43
|
+
ram_total_gib: float
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def ram_pct(self) -> float:
|
|
47
|
+
if self.ram_total_gib <= 0:
|
|
48
|
+
return 0.0
|
|
49
|
+
return 100.0 * self.ram_used_gib / self.ram_total_gib
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _color_for(pct: float) -> str:
|
|
53
|
+
if pct < 50:
|
|
54
|
+
return "green"
|
|
55
|
+
if pct < 80:
|
|
56
|
+
return "yellow"
|
|
57
|
+
return "red"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _bar(pct: float, width: int) -> str:
|
|
61
|
+
pct = max(0.0, min(100.0, pct))
|
|
62
|
+
filled = int(round(pct / 100.0 * width))
|
|
63
|
+
filled = max(0, min(width, filled))
|
|
64
|
+
color = _color_for(pct)
|
|
65
|
+
return (
|
|
66
|
+
f"[{color}]{_FILLED * filled}[/]"
|
|
67
|
+
f"[dim]{_EMPTY * (width - filled)}[/]"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# --------------------------------------------------------------- macOS shells
|
|
72
|
+
|
|
73
|
+
_VMSTAT_PAGE_SIZE_RE = re.compile(r"page size of (\d+) bytes")
|
|
74
|
+
_VMSTAT_LINE_RE = re.compile(r"^([^:]+):\s+(\d+)")
|
|
75
|
+
_TOP_CPU_RE = re.compile(
|
|
76
|
+
r"CPU usage:\s+([\d.]+)%\s+user,\s+([\d.]+)%\s+sys,\s+([\d.]+)%\s+idle"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def _run(*argv: str) -> str:
|
|
81
|
+
"""Run a command without blocking the event loop; return stdout text."""
|
|
82
|
+
proc = await asyncio.create_subprocess_exec(
|
|
83
|
+
*argv,
|
|
84
|
+
stdout=asyncio.subprocess.PIPE,
|
|
85
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
86
|
+
)
|
|
87
|
+
out, _ = await proc.communicate()
|
|
88
|
+
return out.decode("utf-8", errors="replace")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def _macos_cpu_pct() -> float:
|
|
92
|
+
"""Parse ``top -l 1 -n 0`` for the CPU usage line.
|
|
93
|
+
|
|
94
|
+
`top` already samples internally; ``user + sys`` is equivalent to
|
|
95
|
+
``100 - idle`` within rounding, and is more robust to spaces/punctuation
|
|
96
|
+
drift than parsing the idle field.
|
|
97
|
+
"""
|
|
98
|
+
text = await _run("top", "-l", "1", "-n", "0")
|
|
99
|
+
m = _TOP_CPU_RE.search(text)
|
|
100
|
+
if not m:
|
|
101
|
+
raise RuntimeError("could not parse CPU usage from `top`")
|
|
102
|
+
return float(m.group(1)) + float(m.group(2))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def _macos_ram_gib() -> tuple[float, float]:
|
|
106
|
+
"""Return ``(used_gib, total_gib)`` using ``vm_stat`` + ``sysctl``.
|
|
107
|
+
|
|
108
|
+
``used = (active + wired_down + occupied_by_compressor) × page_size`` —
|
|
109
|
+
matches Activity Monitor's "Memory Used" closely. Page size is read from
|
|
110
|
+
the ``vm_stat`` header (16 KiB on Apple Silicon, 4 KiB on Intel).
|
|
111
|
+
"""
|
|
112
|
+
vm_text, mem_text = await asyncio.gather(
|
|
113
|
+
_run("vm_stat"),
|
|
114
|
+
_run("sysctl", "-n", "hw.memsize"),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
page_size = 4096
|
|
118
|
+
m = _VMSTAT_PAGE_SIZE_RE.search(vm_text)
|
|
119
|
+
if m:
|
|
120
|
+
page_size = int(m.group(1))
|
|
121
|
+
|
|
122
|
+
pages: dict[str, int] = {}
|
|
123
|
+
for line in vm_text.splitlines():
|
|
124
|
+
mm = _VMSTAT_LINE_RE.match(line)
|
|
125
|
+
if mm:
|
|
126
|
+
pages[mm.group(1).strip().lower()] = int(mm.group(2))
|
|
127
|
+
|
|
128
|
+
active = pages.get("pages active", 0)
|
|
129
|
+
wired = pages.get("pages wired down", 0)
|
|
130
|
+
compressed = pages.get("pages occupied by compressor", 0)
|
|
131
|
+
used_bytes = (active + wired + compressed) * page_size
|
|
132
|
+
|
|
133
|
+
total_bytes = int(mem_text.strip() or "0")
|
|
134
|
+
if total_bytes <= 0:
|
|
135
|
+
raise RuntimeError("hw.memsize returned 0")
|
|
136
|
+
|
|
137
|
+
return used_bytes / _GIB, total_bytes / _GIB
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
async def _sample_shellout() -> _Sample:
|
|
141
|
+
cpu, ram = await asyncio.gather(_macos_cpu_pct(), _macos_ram_gib())
|
|
142
|
+
used, total = ram
|
|
143
|
+
return _Sample(cpu_pct=cpu, ram_used_gib=used, ram_total_gib=total)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
async def _sample_psutil() -> _Sample:
|
|
147
|
+
cpu = psutil.cpu_percent(interval=None) # type: ignore[union-attr]
|
|
148
|
+
vm = psutil.virtual_memory() # type: ignore[union-attr]
|
|
149
|
+
return _Sample(
|
|
150
|
+
cpu_pct=float(cpu),
|
|
151
|
+
ram_used_gib=vm.used / _GIB,
|
|
152
|
+
ram_total_gib=vm.total / _GIB,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ----------------------------------------------------------------- the widget
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class SystemUsage(Static):
|
|
160
|
+
"""Single-row CPU + RAM gauge that refreshes itself.
|
|
161
|
+
|
|
162
|
+
``interval`` (seconds, clamped >= 0.25, default 1.5) controls refresh
|
|
163
|
+
cadence. ``bar_width`` (cells, clamped >= 2, default 12) controls the
|
|
164
|
+
width of each progress bar.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
DEFAULT_CSS = """
|
|
168
|
+
SystemUsage {
|
|
169
|
+
height: auto;
|
|
170
|
+
min-height: 1;
|
|
171
|
+
padding: 0 1;
|
|
172
|
+
content-align: left middle;
|
|
173
|
+
}
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def __init__(
|
|
177
|
+
self,
|
|
178
|
+
*,
|
|
179
|
+
interval: float = 1.5,
|
|
180
|
+
bar_width: int = 12,
|
|
181
|
+
**kwargs,
|
|
182
|
+
) -> None:
|
|
183
|
+
super().__init__("CPU … RAM …", **kwargs)
|
|
184
|
+
self._interval = max(0.25, float(interval))
|
|
185
|
+
self._bar_width = max(2, int(bar_width))
|
|
186
|
+
self._supported = _HAS_PSUTIL or sys.platform == "darwin"
|
|
187
|
+
self._source = "psutil" if _HAS_PSUTIL else "shell"
|
|
188
|
+
self._timer = None
|
|
189
|
+
|
|
190
|
+
def on_mount(self) -> None:
|
|
191
|
+
# Prime psutil so the very first sample isn't 0.0 (psutil's
|
|
192
|
+
# cpu_percent needs a baseline tick to compute deltas against).
|
|
193
|
+
if _HAS_PSUTIL:
|
|
194
|
+
try:
|
|
195
|
+
psutil.cpu_percent(interval=None) # type: ignore[union-attr]
|
|
196
|
+
except Exception:
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
if not self._supported:
|
|
200
|
+
self._show_error(
|
|
201
|
+
f"unsupported platform: {sys.platform} (install psutil)"
|
|
202
|
+
)
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
self.border_title = f"system — {self._source}"
|
|
206
|
+
# Kick one immediate refresh, then schedule periodic ones.
|
|
207
|
+
self.run_worker(self._tick(), exclusive=True)
|
|
208
|
+
self._timer = self.set_interval(self._interval, self._schedule_tick)
|
|
209
|
+
|
|
210
|
+
def _schedule_tick(self) -> None:
|
|
211
|
+
# `set_interval` fires on the event loop; offload the actual sample
|
|
212
|
+
# to a worker so a slow shell-out can never delay the next tick.
|
|
213
|
+
self.run_worker(self._tick(), exclusive=True)
|
|
214
|
+
|
|
215
|
+
async def _tick(self) -> None:
|
|
216
|
+
try:
|
|
217
|
+
if _HAS_PSUTIL:
|
|
218
|
+
sample = await _sample_psutil()
|
|
219
|
+
else:
|
|
220
|
+
sample = await _sample_shellout()
|
|
221
|
+
except Exception as exc: # never raise into the layout
|
|
222
|
+
self._show_error(str(exc))
|
|
223
|
+
return
|
|
224
|
+
self._show_sample(sample)
|
|
225
|
+
|
|
226
|
+
# Named ``_show_*`` rather than ``_render*`` to avoid colliding with
|
|
227
|
+
# ``Widget._render`` (which has a different signature/return shape).
|
|
228
|
+
def _show_sample(self, s: _Sample) -> None:
|
|
229
|
+
cpu_bar = _bar(s.cpu_pct, self._bar_width)
|
|
230
|
+
ram_bar = _bar(s.ram_pct, self._bar_width)
|
|
231
|
+
cpu_str = f"[b]CPU[/b] {s.cpu_pct:5.1f}% {cpu_bar}"
|
|
232
|
+
ram_str = (
|
|
233
|
+
f"[b]RAM[/b] {s.ram_used_gib:5.1f}/{s.ram_total_gib:.1f} GiB "
|
|
234
|
+
f"{ram_bar}"
|
|
235
|
+
)
|
|
236
|
+
self.update(f"{cpu_str} {ram_str}")
|
|
237
|
+
# Recover from a transient error: title might still say "error".
|
|
238
|
+
if self.border_title and "error" in self.border_title:
|
|
239
|
+
self.border_title = f"system — {self._source}"
|
|
240
|
+
|
|
241
|
+
def _show_error(self, msg: str) -> None:
|
|
242
|
+
self.update("CPU ? RAM ?")
|
|
243
|
+
# Truncate so the border title stays compact.
|
|
244
|
+
self.border_title = f"system — error: {msg[:80]}"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|