sysvis 1.1.0__py3-none-any.whl

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.
sysvis/__init__.py ADDED
@@ -0,0 +1,48 @@
1
+ """
2
+ sysvis — Arrow-key terminal system monitor with actionable insights.
3
+
4
+ Quick start
5
+ -----------
6
+ import sysvis
7
+ sysvis.run() # launches arrow-key menu
8
+
9
+ # Use the collector directly:
10
+ from sysvis import SystemCollector
11
+ import time
12
+ c = SystemCollector()
13
+ time.sleep(1.5)
14
+ print(c.data["cpu"].percent_total)
15
+ c.stop()
16
+ """
17
+
18
+ from .collectors.system import (
19
+ SystemCollector,
20
+ CPUMetrics,
21
+ MemoryMetrics,
22
+ DiskMetrics,
23
+ NetworkMetrics,
24
+ GPUMetrics,
25
+ ProcessMetrics,
26
+ SystemInfo,
27
+ )
28
+
29
+ __version__ = "1.1.0"
30
+ __author__ = "sysvis contributors"
31
+
32
+ __all__ = [
33
+ "SystemCollector",
34
+ "CPUMetrics",
35
+ "MemoryMetrics",
36
+ "DiskMetrics",
37
+ "NetworkMetrics",
38
+ "GPUMetrics",
39
+ "ProcessMetrics",
40
+ "SystemInfo",
41
+ "run",
42
+ ]
43
+
44
+
45
+ def run():
46
+ """Launch the arrow-key interactive menu."""
47
+ from .views.menu import run as _run
48
+ _run()
sysvis/__main__.py ADDED
@@ -0,0 +1,12 @@
1
+ """
2
+ sysvis/__main__.py
3
+ Allows: python -m sysvis
4
+ """
5
+
6
+ def main():
7
+ import sysvis
8
+ sysvis.run()
9
+
10
+
11
+ if __name__ == "__main__":
12
+ main()
@@ -0,0 +1,3 @@
1
+ """sysvis.collectors — background metric collection."""
2
+ from .system import SystemCollector
3
+ __all__ = ["SystemCollector"]
@@ -0,0 +1,507 @@
1
+ """
2
+ sysvis/collectors/system.py
3
+ ----------------------------
4
+ Non-blocking threaded system metrics collector.
5
+ Runs all psutil calls in a background thread — the menu/views
6
+ never block waiting for data.
7
+
8
+ CPU primed with interval=0.5 once at startup, then sampled
9
+ with interval=None (0 ms cost) on every subsequent call.
10
+ """
11
+
12
+ import os
13
+ import sys
14
+ import socket
15
+ import platform
16
+ import subprocess
17
+ import time
18
+ import threading
19
+ import psutil
20
+ from dataclasses import dataclass, field
21
+
22
+ try:
23
+ import cpuinfo
24
+ HAS_CPUINFO = True
25
+ except ImportError:
26
+ HAS_CPUINFO = False
27
+
28
+ try:
29
+ import GPUtil
30
+ HAS_GPUTIL = True
31
+ except ImportError:
32
+ HAS_GPUTIL = False
33
+
34
+
35
+ # ── Dataclasses ───────────────────────────────────────────────────────────────
36
+
37
+ @dataclass
38
+ class CPUMetrics:
39
+ percent_total: float = 0.0
40
+ percent_per_core: list = field(default_factory=list)
41
+ freq_current: float = 0.0
42
+ freq_max: float = 0.0
43
+ physical_cores: int = 0
44
+ logical_cores: int = 0
45
+ cpu_name: str = ""
46
+ load_avg_1: float = 0.0
47
+ load_avg_5: float = 0.0
48
+ load_avg_15: float = 0.0
49
+
50
+
51
+ @dataclass
52
+ class MemoryMetrics:
53
+ ram_total: int = 0
54
+ ram_used: int = 0
55
+ ram_free: int = 0
56
+ ram_percent: float = 0.0
57
+ swap_total: int = 0
58
+ swap_used: int = 0
59
+ swap_percent: float = 0.0
60
+
61
+
62
+ @dataclass
63
+ class DiskMetrics:
64
+ partitions: list = field(default_factory=list)
65
+ read_bytes: int = 0
66
+ write_bytes: int = 0
67
+ read_speed: float = 0.0
68
+ write_speed: float = 0.0
69
+
70
+
71
+ @dataclass
72
+ class NetworkMetrics:
73
+ interfaces: list = field(default_factory=list)
74
+ bytes_sent: int = 0
75
+ bytes_recv: int = 0
76
+ packets_sent: int = 0
77
+ packets_recv: int = 0
78
+ upload_speed: float = 0.0
79
+ download_speed: float = 0.0
80
+ hostname: str = ""
81
+ local_ip: str = ""
82
+ wifi_ssid: str = ""
83
+ wifi_ip: str = ""
84
+ ethernet_ip: str = ""
85
+
86
+
87
+ @dataclass
88
+ class GPUMetrics:
89
+ available: bool = False
90
+ gpus: list = field(default_factory=list)
91
+
92
+
93
+ @dataclass
94
+ class ProcessMetrics:
95
+ top_processes: list = field(default_factory=list)
96
+ total_processes: int = 0
97
+ running: int = 0
98
+ sleeping: int = 0
99
+
100
+
101
+ @dataclass
102
+ class SystemInfo:
103
+ os_name: str = ""
104
+ os_version: str = ""
105
+ os_release: str = ""
106
+ kernel: str = ""
107
+ architecture: str = ""
108
+ hostname: str = ""
109
+ uptime_seconds: float = 0.0
110
+ installed_apps_count: int = 0
111
+ installed_apps: list = field(default_factory=list)
112
+
113
+
114
+ # ── Collector ─────────────────────────────────────────────────────────────────
115
+
116
+ class SystemCollector:
117
+ """
118
+ Continuously collects system metrics in a background thread.
119
+
120
+ Usage::
121
+
122
+ c = SystemCollector(interval=1.0)
123
+ time.sleep(1.5) # wait for first sample
124
+ data = c.data # always returns latest snapshot instantly
125
+ c.stop()
126
+
127
+ ``data`` keys: "cpu", "memory", "disk", "network",
128
+ "gpu", "processes", "system"
129
+ """
130
+
131
+ def __init__(self, interval: float = 1.0):
132
+ self.interval = interval
133
+ self._lock = threading.Lock()
134
+
135
+ # Static values resolved once
136
+ self._cpu_name = self._get_cpu_name()
137
+ self._physical_cores = psutil.cpu_count(logical=False) or 1
138
+ self._logical_cores = psutil.cpu_count(logical=True) or 1
139
+ self._hostname = socket.gethostname()
140
+ try:
141
+ self._local_ip = socket.gethostbyname(self._hostname)
142
+ except Exception:
143
+ self._local_ip = "127.0.0.1"
144
+
145
+ # Prime CPU sampler — must use a real interval once for accurate first read
146
+ psutil.cpu_percent(interval=0.5)
147
+ psutil.cpu_percent(interval=0.5, percpu=True)
148
+
149
+ # I/O baselines
150
+ self._prev_disk = psutil.disk_io_counters()
151
+ self._prev_net = psutil.net_io_counters()
152
+ self._prev_ts = time.monotonic()
153
+
154
+ # EMA-smoothed speeds (alpha=0.35)
155
+ self._ul = 0.0
156
+ self._dl = 0.0
157
+ self._rd = 0.0
158
+ self._wr = 0.0
159
+ self._EMA = 0.35
160
+
161
+ # Caches for slow operations
162
+ self._apps: list = []
163
+ self._apps_ts: float = 0.0
164
+ self._sys_cache: SystemInfo = SystemInfo()
165
+ self._sys_cache_ts: float = 0.0
166
+ self._wifi_ssid: str = "N/A"
167
+ self._wifi_ssid_ts: float = 0.0
168
+
169
+ # First blocking snapshot, then hand off to background thread
170
+ self._data: dict = self._collect()
171
+
172
+ self._running = True
173
+ self._thread = threading.Thread(target=self._loop, daemon=True)
174
+ self._thread.start()
175
+
176
+ # ── Public API ────────────────────────────────────────────────────────────
177
+
178
+ @property
179
+ def data(self) -> dict:
180
+ """Return the latest snapshot — never blocks."""
181
+ with self._lock:
182
+ return self._data
183
+
184
+ def stop(self):
185
+ """Stop the background thread."""
186
+ self._running = False
187
+
188
+ # ── Background loop ───────────────────────────────────────────────────────
189
+
190
+ def _loop(self):
191
+ while self._running:
192
+ t0 = time.monotonic()
193
+ snapshot = self._collect()
194
+ with self._lock:
195
+ self._data = snapshot
196
+ time.sleep(max(0.0, self.interval - (time.monotonic() - t0)))
197
+
198
+ # ── Parallel collection ───────────────────────────────────────────────────
199
+
200
+ def _collect(self) -> dict:
201
+ now = time.monotonic()
202
+ dt = max(now - self._prev_ts, 0.001)
203
+ self._prev_ts = now
204
+
205
+ results: dict = {}
206
+
207
+ def run(key, fn):
208
+ try:
209
+ results[key] = fn()
210
+ except Exception:
211
+ pass
212
+
213
+ threads = [
214
+ threading.Thread(target=run, args=(k, fn), daemon=True)
215
+ for k, fn in [
216
+ ("cpu", self._collect_cpu),
217
+ ("memory", self._collect_memory),
218
+ ("disk", lambda: self._collect_disk(dt)),
219
+ ("network", lambda: self._collect_network(dt)),
220
+ ("gpu", self._collect_gpu),
221
+ ("processes", self._collect_processes),
222
+ ("system", self._collect_system),
223
+ ]
224
+ ]
225
+ for t in threads: t.start()
226
+ for t in threads: t.join(timeout=3.0)
227
+
228
+ # Fill any failed keys with empty dataclasses
229
+ defaults = {
230
+ "cpu": CPUMetrics(), "memory": MemoryMetrics(),
231
+ "disk": DiskMetrics(), "network": NetworkMetrics(),
232
+ "gpu": GPUMetrics(), "processes": ProcessMetrics(),
233
+ "system": SystemInfo(),
234
+ }
235
+ for k, v in defaults.items():
236
+ results.setdefault(k, v)
237
+ return results
238
+
239
+ # ── Individual collectors ─────────────────────────────────────────────────
240
+
241
+ def _collect_cpu(self) -> CPUMetrics:
242
+ m = CPUMetrics()
243
+ m.cpu_name = self._cpu_name
244
+ m.physical_cores = self._physical_cores
245
+ m.logical_cores = self._logical_cores
246
+ m.percent_total = psutil.cpu_percent(interval=0.1)
247
+ m.percent_per_core = psutil.cpu_percent(interval=0.1, percpu=True)
248
+ freq = psutil.cpu_freq()
249
+ if freq:
250
+ m.freq_current = round(freq.current, 1)
251
+ m.freq_max = round(freq.max, 1)
252
+ try:
253
+ la = os.getloadavg()
254
+ m.load_avg_1, m.load_avg_5, m.load_avg_15 = la
255
+ except (AttributeError, OSError):
256
+ pass # not available on Windows
257
+ return m
258
+
259
+ def _collect_memory(self) -> MemoryMetrics:
260
+ m = MemoryMetrics()
261
+ vm = psutil.virtual_memory()
262
+ sw = psutil.swap_memory()
263
+ m.ram_total, m.ram_used = vm.total, vm.used
264
+ m.ram_free, m.ram_percent = vm.available, vm.percent
265
+ m.swap_total, m.swap_used = sw.total, sw.used
266
+ m.swap_percent = sw.percent
267
+ return m
268
+
269
+ def _collect_disk(self, dt: float) -> DiskMetrics:
270
+ m = DiskMetrics()
271
+ parts = []
272
+ for p in psutil.disk_partitions(all=False):
273
+ try:
274
+ u = psutil.disk_usage(p.mountpoint)
275
+ parts.append({
276
+ "device": p.device, "mountpoint": p.mountpoint,
277
+ "fstype": p.fstype, "total": u.total,
278
+ "used": u.used, "free": u.free,
279
+ "percent": u.percent,
280
+ })
281
+ except (PermissionError, OSError):
282
+ continue
283
+ m.partitions = parts
284
+ curr = psutil.disk_io_counters()
285
+ if curr and self._prev_disk:
286
+ self._rd = self._ema(self._rd,
287
+ (curr.read_bytes - self._prev_disk.read_bytes) / dt)
288
+ self._wr = self._ema(self._wr,
289
+ (curr.write_bytes - self._prev_disk.write_bytes) / dt)
290
+ m.read_bytes = curr.read_bytes
291
+ m.write_bytes = curr.write_bytes
292
+ m.read_speed = self._rd
293
+ m.write_speed = self._wr
294
+ self._prev_disk = curr
295
+ return m
296
+
297
+ def _collect_network(self, dt: float) -> NetworkMetrics:
298
+ m = NetworkMetrics()
299
+ m.hostname = self._hostname
300
+ m.local_ip = self._local_ip
301
+ curr = psutil.net_io_counters()
302
+ self._ul = self._ema(self._ul,
303
+ (curr.bytes_sent - self._prev_net.bytes_sent) / dt)
304
+ self._dl = self._ema(self._dl,
305
+ (curr.bytes_recv - self._prev_net.bytes_recv) / dt)
306
+ m.upload_speed = self._ul
307
+ m.download_speed = self._dl
308
+ m.bytes_sent = curr.bytes_sent
309
+ m.bytes_recv = curr.bytes_recv
310
+ m.packets_sent = curr.packets_sent
311
+ m.packets_recv = curr.packets_recv
312
+ self._prev_net = curr
313
+
314
+ addrs = psutil.net_if_addrs()
315
+ stats = psutil.net_if_stats()
316
+ ifaces = []
317
+ for name, addr_list in addrs.items():
318
+ info = {"name": name, "ipv4": "", "ipv6": "",
319
+ "mac": "", "is_up": False, "speed": 0}
320
+ for a in addr_list:
321
+ if a.family == socket.AF_INET: info["ipv4"] = a.address
322
+ elif a.family == socket.AF_INET6: info["ipv6"] = a.address.split("%")[0]
323
+ elif a.family == psutil.AF_LINK: info["mac"] = a.address
324
+ if name in stats:
325
+ info["is_up"] = stats[name].isup
326
+ info["speed"] = stats[name].speed
327
+ ifaces.append(info)
328
+ lo = name.lower()
329
+ if any(k in lo for k in ("eth","en0","en1","enp","ethernet")):
330
+ m.ethernet_ip = info["ipv4"]
331
+ if any(k in lo for k in ("wlan","wi-fi","wifi","wlp","wlo")):
332
+ m.wifi_ip = info["ipv4"]
333
+ m.interfaces = ifaces
334
+
335
+ now = time.monotonic()
336
+ if now - self._wifi_ssid_ts > 15:
337
+ self._wifi_ssid = self._get_wifi_ssid()
338
+ self._wifi_ssid_ts = now
339
+ m.wifi_ssid = self._wifi_ssid
340
+ return m
341
+
342
+ def _collect_gpu(self) -> GPUMetrics:
343
+ m = GPUMetrics()
344
+ if not HAS_GPUTIL:
345
+ return m
346
+ try:
347
+ gpus = GPUtil.getGPUs()
348
+ m.available = bool(gpus)
349
+ m.gpus = [{
350
+ "id": g.id, "name": g.name,
351
+ "load": g.load * 100,
352
+ "mem_used": g.memoryUsed, "mem_total": g.memoryTotal,
353
+ "mem_percent": (g.memoryUsed / g.memoryTotal * 100)
354
+ if g.memoryTotal else 0,
355
+ "temperature": g.temperature,
356
+ } for g in gpus]
357
+ except Exception:
358
+ pass
359
+ return m
360
+
361
+ def _collect_processes(self, top_n: int = 15) -> ProcessMetrics:
362
+ m = ProcessMetrics()
363
+ procs, running, sleeping = [], 0, 0
364
+ for p in psutil.process_iter(
365
+ ["pid","name","cpu_percent","memory_percent","status","username"]):
366
+ try:
367
+ info = p.info
368
+ procs.append(info)
369
+ s = info.get("status", "")
370
+ if s == "running": running += 1
371
+ elif s in ("sleeping", "idle"): sleeping += 1
372
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
373
+ continue
374
+ m.total_processes = len(procs)
375
+ m.running = running
376
+ m.sleeping = sleeping
377
+ procs.sort(key=lambda x: x.get("cpu_percent") or 0, reverse=True)
378
+ m.top_processes = procs[:top_n]
379
+ return m
380
+
381
+ def _collect_system(self) -> SystemInfo:
382
+ now = time.monotonic()
383
+ if now - self._sys_cache_ts > 60:
384
+ s = SystemInfo()
385
+ s.os_name = platform.system()
386
+ s.os_version = platform.version()
387
+ s.os_release = platform.release()
388
+ s.kernel = platform.uname().version[:80]
389
+ s.architecture = platform.machine()
390
+ s.hostname = self._hostname
391
+ if now - self._apps_ts > 60:
392
+ self._apps = self._get_installed_apps()
393
+ self._apps_ts = now
394
+ s.installed_apps = self._apps
395
+ s.installed_apps_count = len(self._apps)
396
+ self._sys_cache = s
397
+ self._sys_cache_ts = now
398
+ self._sys_cache.uptime_seconds = time.time() - psutil.boot_time()
399
+ return self._sys_cache
400
+
401
+ # ── Utilities ─────────────────────────────────────────────────────────────
402
+
403
+ def _ema(self, prev: float, new: float) -> float:
404
+ return self._EMA * new + (1 - self._EMA) * prev
405
+
406
+ def _get_cpu_name(self) -> str:
407
+ if HAS_CPUINFO:
408
+ try:
409
+ return cpuinfo.get_cpu_info().get("brand_raw", "") \
410
+ or platform.processor()
411
+ except Exception:
412
+ pass
413
+ return platform.processor()
414
+
415
+ def _get_wifi_ssid(self) -> str:
416
+ try:
417
+ if sys.platform == "win32":
418
+ out = subprocess.check_output(
419
+ ["netsh","wlan","show","interfaces"],
420
+ stderr=subprocess.DEVNULL, timeout=3
421
+ ).decode(errors="ignore")
422
+ for line in out.splitlines():
423
+ if "SSID" in line and "BSSID" not in line:
424
+ return line.split(":",1)[-1].strip()
425
+ elif sys.platform == "darwin":
426
+ out = subprocess.check_output(
427
+ ["/System/Library/PrivateFrameworks/Apple80211.framework"
428
+ "/Versions/Current/Resources/airport","-I"],
429
+ stderr=subprocess.DEVNULL, timeout=3
430
+ ).decode(errors="ignore")
431
+ for line in out.splitlines():
432
+ if " SSID:" in line:
433
+ return line.split(":",1)[-1].strip()
434
+ else:
435
+ for cmd in [["iwgetid","-r"],
436
+ ["nmcli","-t","-f","active,ssid","dev","wifi"]]:
437
+ try:
438
+ out = subprocess.check_output(
439
+ cmd, stderr=subprocess.DEVNULL, timeout=3
440
+ ).decode(errors="ignore").strip()
441
+ if out:
442
+ return out.split("yes:")[-1].strip() \
443
+ if "yes:" in out else out.splitlines()[0]
444
+ except FileNotFoundError:
445
+ continue
446
+ except Exception:
447
+ pass
448
+ return "N/A"
449
+
450
+ def _get_installed_apps(self) -> list:
451
+ apps = []
452
+ try:
453
+ if sys.platform == "win32":
454
+ import winreg
455
+ keys = [
456
+ (winreg.HKEY_LOCAL_MACHINE,
457
+ r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"),
458
+ (winreg.HKEY_LOCAL_MACHINE,
459
+ r"SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"),
460
+ (winreg.HKEY_CURRENT_USER,
461
+ r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"),
462
+ ]
463
+ for hive, sub in keys:
464
+ try:
465
+ key = winreg.OpenKey(hive, sub)
466
+ for i in range(winreg.QueryInfoKey(key)[0]):
467
+ try:
468
+ sk = winreg.OpenKey(key, winreg.EnumKey(key, i))
469
+ name = winreg.QueryValueEx(sk,"DisplayName")[0]
470
+ ver = ""
471
+ try: ver = winreg.QueryValueEx(sk,"DisplayVersion")[0]
472
+ except Exception: pass
473
+ if name: apps.append({"name": name, "version": ver})
474
+ except Exception: continue
475
+ except Exception: continue
476
+ elif sys.platform == "darwin":
477
+ out = subprocess.check_output(
478
+ ["ls","/Applications"],
479
+ stderr=subprocess.DEVNULL, timeout=5
480
+ ).decode(errors="ignore")
481
+ for line in out.strip().splitlines():
482
+ if line.endswith(".app"):
483
+ apps.append({"name": line[:-4], "version": ""})
484
+ else:
485
+ for cmd, parser in [
486
+ (["dpkg","--get-selections"],
487
+ lambda l: l.split()[0] if "install" in l else None),
488
+ (["rpm","-qa","--queryformat","%{NAME} %{VERSION}\n"],
489
+ lambda l: l.split()[0] if l.strip() else None),
490
+ (["pacman","-Q"],
491
+ lambda l: l.split()[0] if l.strip() else None),
492
+ ]:
493
+ try:
494
+ out = subprocess.check_output(
495
+ cmd, stderr=subprocess.DEVNULL, timeout=10
496
+ ).decode(errors="ignore")
497
+ for line in out.strip().splitlines():
498
+ name = parser(line)
499
+ if name: apps.append({"name": name, "version": ""})
500
+ break
501
+ except (FileNotFoundError,
502
+ subprocess.CalledProcessError,
503
+ subprocess.TimeoutExpired):
504
+ continue
505
+ except Exception:
506
+ pass
507
+ return apps
@@ -0,0 +1,32 @@
1
+ """
2
+ sysvis.views — individual insight views (public API).
3
+
4
+ Each function takes a SystemCollector and runs until q/Esc.
5
+
6
+ Example::
7
+
8
+ from sysvis import SystemCollector
9
+ from sysvis.views import cpu, health
10
+ import time
11
+
12
+ c = SystemCollector()
13
+ time.sleep(1.5)
14
+ cpu(c) # live CPU view
15
+ health(c) # health report
16
+ c.stop()
17
+ """
18
+
19
+ from .cpu import cpu
20
+ from .memory import memory
21
+ from .disk import disk
22
+ from .network import network
23
+ from .processes import processes
24
+ from .gpu import gpu
25
+ from .sysinfo import sysinfo
26
+ from .live import live
27
+ from .health import health
28
+
29
+ __all__ = [
30
+ "cpu", "memory", "disk", "network",
31
+ "processes", "gpu", "sysinfo", "live", "health",
32
+ ]