evolver-tools 1.4.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.
Files changed (69) hide show
  1. evolver_tools/__init__.py +2 -0
  2. evolver_tools/__main__.py +3 -0
  3. evolver_tools/cli.py +89 -0
  4. evolver_tools/vendor/b64/__init__.py +2 -0
  5. evolver_tools/vendor/b64/b64.py +176 -0
  6. evolver_tools/vendor/cal_tool/__init__.py +1 -0
  7. evolver_tools/vendor/cal_tool/cli.py +234 -0
  8. evolver_tools/vendor/chart_cli/__init__.py +444 -0
  9. evolver_tools/vendor/chart_cli/__main__.py +3 -0
  10. evolver_tools/vendor/colors/__init__.py +5 -0
  11. evolver_tools/vendor/colors/__main__.py +97 -0
  12. evolver_tools/vendor/csv_stats/__init__.py +5 -0
  13. evolver_tools/vendor/csv_stats/__main__.py +4 -0
  14. evolver_tools/vendor/csv_stats/analyzer.py +258 -0
  15. evolver_tools/vendor/csv_stats/cli.py +45 -0
  16. evolver_tools/vendor/dirsize/__init__.py +183 -0
  17. evolver_tools/vendor/envcheck/__init__.py +426 -0
  18. evolver_tools/vendor/ff/__init__.py +427 -0
  19. evolver_tools/vendor/ff/__main__.py +3 -0
  20. evolver_tools/vendor/find_dups/__init__.py +7 -0
  21. evolver_tools/vendor/find_dups/cli.py +392 -0
  22. evolver_tools/vendor/hashsum/__init__.py +211 -0
  23. evolver_tools/vendor/hashsum/__main__.py +5 -0
  24. evolver_tools/vendor/http_live/__init__.py +265 -0
  25. evolver_tools/vendor/http_live/__main__.py +2 -0
  26. evolver_tools/vendor/ipinfo/__init__.py +3 -0
  27. evolver_tools/vendor/ipinfo/__main__.py +30 -0
  28. evolver_tools/vendor/jq_lite/__init__.py +257 -0
  29. evolver_tools/vendor/jq_lite/__main__.py +5 -0
  30. evolver_tools/vendor/json2csv/__init__.py +3 -0
  31. evolver_tools/vendor/json2csv/__main__.py +82 -0
  32. evolver_tools/vendor/jsonql/__init__.py +326 -0
  33. evolver_tools/vendor/jsonql/__main__.py +5 -0
  34. evolver_tools/vendor/license_cli/__init__.py +1 -0
  35. evolver_tools/vendor/license_cli/__main__.py +4 -0
  36. evolver_tools/vendor/license_cli/cli.py +289 -0
  37. evolver_tools/vendor/markdown_check/__init__.py +211 -0
  38. evolver_tools/vendor/nb/__init__.py +319 -0
  39. evolver_tools/vendor/nb/__main__.py +3 -0
  40. evolver_tools/vendor/passgen/__init__.py +224 -0
  41. evolver_tools/vendor/portcheck/__init__.py +2 -0
  42. evolver_tools/vendor/portcheck/__main__.py +66 -0
  43. evolver_tools/vendor/project_doctor/__init__.py +412 -0
  44. evolver_tools/vendor/project_doctor/__main__.py +3 -0
  45. evolver_tools/vendor/ren/__init__.py +283 -0
  46. evolver_tools/vendor/ren/__main__.py +3 -0
  47. evolver_tools/vendor/siege_lite/__init__.py +250 -0
  48. evolver_tools/vendor/siege_lite/__main__.py +3 -0
  49. evolver_tools/vendor/smellfinder/__init__.py +376 -0
  50. evolver_tools/vendor/smellfinder/__main__.py +3 -0
  51. evolver_tools/vendor/sqlite_cli/__init__.py +326 -0
  52. evolver_tools/vendor/sqlite_cli/__main__.py +5 -0
  53. evolver_tools/vendor/sysmon/__init__.py +299 -0
  54. evolver_tools/vendor/sysmon/__main__.py +3 -0
  55. evolver_tools/vendor/timer/__init__.py +127 -0
  56. evolver_tools/vendor/treedir/__init__.py +2 -0
  57. evolver_tools/vendor/treedir/__main__.py +128 -0
  58. evolver_tools/vendor/urlparse_tool/__init__.py +3 -0
  59. evolver_tools/vendor/urlparse_tool/cli.py +212 -0
  60. evolver_tools/vendor/web_summary/__init__.py +341 -0
  61. evolver_tools/vendor/web_summary/__main__.py +3 -0
  62. evolver_tools/vendor/wordcount/__init__.py +2 -0
  63. evolver_tools/vendor/wordcount/__main__.py +101 -0
  64. evolver_tools-1.4.0.dist-info/METADATA +107 -0
  65. evolver_tools-1.4.0.dist-info/RECORD +69 -0
  66. evolver_tools-1.4.0.dist-info/WHEEL +5 -0
  67. evolver_tools-1.4.0.dist-info/entry_points.txt +34 -0
  68. evolver_tools-1.4.0.dist-info/licenses/LICENSE +21 -0
  69. evolver_tools-1.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,326 @@
1
+ """sqlite-cli: Zero-dependency SQLite query tool (pure Python stdlib).
2
+
3
+ A user-friendly SQLite CLI that works without installing sqlite3 separately.
4
+ Python's sqlite3 module is always available (stdlib since Python 2.5).
5
+ """
6
+
7
+ import sys
8
+ import os
9
+ import json
10
+ import textwrap
11
+ import sqlite3
12
+ from pathlib import Path
13
+
14
+
15
+ __version__ = "1.0.0"
16
+
17
+
18
+ def detect_columns(cursor):
19
+ """Get column names and types from cursor description."""
20
+ if cursor.description:
21
+ return [desc[0] for desc in cursor.description]
22
+ return []
23
+
24
+
25
+ def format_table(rows, headers, fmt="table"):
26
+ """Format query results in various modes."""
27
+ if not rows and not headers:
28
+ return "(no results)"
29
+
30
+ if fmt == "json":
31
+ if not headers:
32
+ return json.dumps(rows, ensure_ascii=False) if rows else "[]"
33
+ result = [dict(zip(headers, row)) for row in rows]
34
+ return json.dumps(result, ensure_ascii=False, indent=2)
35
+
36
+ if fmt == "csv":
37
+ output = ",".join(headers) if headers else ""
38
+ for row in rows:
39
+ output += "\n" + ",".join(
40
+ str(v) if v is not None else "NULL"
41
+ for v in row
42
+ )
43
+ return output
44
+
45
+ if fmt == "line":
46
+ lines = []
47
+ for i, row in enumerate(rows):
48
+ if headers:
49
+ lines.append(f" row {i+1}:")
50
+ for h, v in zip(headers, row):
51
+ lines.append(f" {h} = {v}")
52
+ else:
53
+ lines.append(f" row {i+1}: {row}")
54
+ lines.append("")
55
+ return "\n".join(lines).rstrip()
56
+
57
+ if fmt == "tabs":
58
+ output = "\t".join(headers) if headers else ""
59
+ for row in rows:
60
+ output += "\n" + "\t".join(
61
+ str(v) if v is not None else "NULL"
62
+ for v in row
63
+ )
64
+ return output
65
+
66
+ # Default: table format
67
+ if not headers:
68
+ lines = [str(row) for row in rows]
69
+ return "\n".join(lines)
70
+
71
+ # Calculate column widths
72
+ col_widths = [len(h) for h in headers]
73
+ for row in rows:
74
+ for i, v in enumerate(row):
75
+ if i < len(col_widths):
76
+ s = str(v) if v is not None else "NULL"
77
+ col_widths[i] = max(col_widths[i], len(s))
78
+
79
+ # Cap widths at 80 for readability
80
+ max_width = 80
81
+ total = sum(col_widths) + 3 * len(col_widths) - 1
82
+ if total > 120:
83
+ for i in range(len(col_widths)):
84
+ col_widths[i] = min(col_widths[i], max_width)
85
+
86
+ # Build separator
87
+ sep = "+" + "+".join("-" * (w + 2) for w in col_widths) + "+"
88
+
89
+ def fmt_row(vals):
90
+ parts = []
91
+ for i, val in enumerate(vals):
92
+ s = str(val) if val is not None else "NULL"
93
+ if i < len(col_widths):
94
+ w = col_widths[i]
95
+ if len(s) > w:
96
+ s = s[:w - 3] + "..."
97
+ parts.append(f" {s:<{w}} ")
98
+ else:
99
+ parts.append(f" {s} ")
100
+ return "|" + "|".join(parts) + "|"
101
+
102
+ lines = [sep, fmt_row(headers), sep]
103
+ for row in rows:
104
+ lines.append(fmt_row(row))
105
+ lines.append(sep)
106
+ lines.append(f"({len(rows)} rows)")
107
+
108
+ return "\n".join(lines)
109
+
110
+
111
+ def run_dot_command(cursor, cmd: str, db_path: str):
112
+ """Handle SQLite dot commands."""
113
+ cmd = cmd.strip().lower()
114
+
115
+ if cmd == ".tables":
116
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
117
+ tables = [r[0] for r in cursor.fetchall()]
118
+ if tables:
119
+ # Print in columns
120
+ cols = 4
121
+ for i in range(0, len(tables), cols):
122
+ print(" ".join(tables[i:i+cols]))
123
+ else:
124
+ print("(no tables)")
125
+ return True
126
+
127
+ if cmd == ".schema":
128
+ cursor.execute("SELECT sql FROM sqlite_master WHERE sql IS NOT NULL")
129
+ schemas = [r[0] for r in cursor.fetchall()]
130
+ for s in schemas:
131
+ print(s + ";")
132
+ print()
133
+ if not schemas:
134
+ print("(no schema)")
135
+ return True
136
+
137
+ if cmd == ".databases":
138
+ print(f" {db_path or ':memory:'}")
139
+ return True
140
+
141
+ if cmd == ".indexes":
142
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='index' ORDER BY name")
143
+ indexes = [r[0] for r in cursor.fetchall()]
144
+ if indexes:
145
+ print("\n".join(indexes))
146
+ else:
147
+ print("(no indexes)")
148
+ return True
149
+
150
+ if cmd.startswith(".schema "):
151
+ table = cmd[8:].strip()
152
+ cursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name=? AND sql IS NOT NULL", (table,))
153
+ row = cursor.fetchone()
154
+ if row:
155
+ print(row[0] + ";")
156
+ else:
157
+ print(f"Error: no such table: {table}", file=sys.stderr)
158
+ return True
159
+
160
+ if cmd.startswith(".mode "):
161
+ # This is handled before execution, not here
162
+ return True
163
+
164
+ if cmd == ".help":
165
+ print("""Dot commands:
166
+ .tables List all tables
167
+ .schema [table] Show CREATE statement(s)
168
+ .indexes List all indexes
169
+ .databases Show connected database
170
+ .mode MODE Set output mode (table|csv|json|line|tabs|column)
171
+ .exit Quit
172
+ .help Show this help""")
173
+ return True
174
+
175
+ return False
176
+
177
+
178
+ def exec_query(cursor, sql: str, mode: str = "table"):
179
+ """Execute a SQL query and print results."""
180
+ sql = sql.strip().rstrip(";")
181
+ if not sql:
182
+ return
183
+
184
+ try:
185
+ cursor.execute(sql)
186
+ except sqlite3.Error as e:
187
+ print(f"Error: {e}", file=sys.stderr)
188
+ return
189
+
190
+ # For SELECT-like queries, fetch and display results
191
+ if sql.upper().lstrip().startswith(("SELECT", "PRAGMA", "EXPLAIN")):
192
+ rows = cursor.fetchall()
193
+ headers = detect_columns(cursor)
194
+ output = format_table(rows, headers, fmt=mode)
195
+ if output:
196
+ print(output)
197
+ else:
198
+ # DML/DDL: show affected rows
199
+ print(f"({cursor.rowcount} rows affected)" if cursor.rowcount >= 0 else "OK")
200
+
201
+
202
+ def main():
203
+ import argparse
204
+
205
+ parser = argparse.ArgumentParser(
206
+ description="sqlite-cli — Zero-dependency SQLite query tool",
207
+ formatter_class=argparse.RawDescriptionHelpFormatter,
208
+ epilog="""
209
+ Examples:
210
+ sqlite-cli db.sqlite3 "SELECT * FROM users"
211
+ sqlite-cli db.sqlite3 --mode json "SELECT id, name FROM users"
212
+ echo "SELECT count(*) FROM users" | sqlite-cli db.sqlite3
213
+ sqlite-cli --init schema.sql db.sqlite3
214
+ sqlite-cli --tables db.sqlite3
215
+ sqlite-cli db.sqlite3 ".tables"
216
+ sqlite-cli db.sqlite3 ".schema users"
217
+ sqlite-cli --schema users db.sqlite3
218
+ sqlite-cli --schema-all db.sqlite3
219
+ """
220
+ )
221
+ parser.add_argument("database", help="Path to SQLite database file")
222
+ parser.add_argument("query", nargs="*", help="SQL query or dot command")
223
+ parser.add_argument("--mode", "-m", default="table", choices=["table", "csv", "json", "line", "tabs", "column"],
224
+ help="Output format (default: table)")
225
+ parser.add_argument("--init", "-i", help="SQL file to execute before query (e.g., CREATE TABLE)")
226
+ parser.add_argument("--tables", action="store_true", help="List all tables (shortcut)")
227
+ parser.add_argument("--schema", nargs=1, metavar="TABLE", default=None,
228
+ help="Show schema for a specific table (omit for all)")
229
+ parser.add_argument("--schema-all", action="store_true",
230
+ help="Show schema for all tables")
231
+
232
+ args = parser.parse_intermixed_args()
233
+
234
+ # Resolve DB path
235
+ db_path = args.database
236
+
237
+ # Handle .sqlite or .db shortcuts
238
+ if not os.path.exists(db_path) and not db_path == ":memory:":
239
+ print(f"Warning: database not found: {db_path}", file=sys.stderr)
240
+ print("Creating new database...", file=sys.stderr)
241
+
242
+ # Connect
243
+ try:
244
+ conn = sqlite3.connect(db_path)
245
+ conn.execute("PRAGMA journal_mode=WAL")
246
+ except sqlite3.Error as e:
247
+ print(f"Error connecting to database: {e}", file=sys.stderr)
248
+ sys.exit(1)
249
+
250
+ cursor = conn.cursor()
251
+
252
+ # Run init file if provided
253
+ if args.init:
254
+ try:
255
+ with open(args.init) as f:
256
+ init_sql = f.read()
257
+ cursor.executescript(init_sql)
258
+ conn.commit()
259
+ print(f"Executed init script: {args.init}", file=sys.stderr)
260
+ except FileNotFoundError:
261
+ print(f"Error: init file not found: {args.init}", file=sys.stderr)
262
+ sys.exit(1)
263
+ except sqlite3.Error as e:
264
+ print(f"Error executing init script: {e}", file=sys.stderr)
265
+ sys.exit(1)
266
+
267
+ # Handle --tables shortcut
268
+ if args.tables:
269
+ run_dot_command(cursor, ".tables", db_path)
270
+ conn.close()
271
+ return
272
+
273
+ # Handle --schema shortcut
274
+ if args.schema is not None:
275
+ run_dot_command(cursor, f".schema {args.schema[0]}", db_path)
276
+ conn.close()
277
+ return
278
+
279
+ # Handle --schema-all
280
+ if args.schema_all:
281
+ run_dot_command(cursor, ".schema", db_path)
282
+ conn.close()
283
+ return
284
+
285
+ # Get query
286
+ if args.query:
287
+ query_text = " ".join(args.query)
288
+ elif args.init:
289
+ # init only - no query needed
290
+ conn.close()
291
+ return
292
+ else:
293
+ # Read from stdin (non-interactive only)
294
+ stdin_data = sys.stdin.read()
295
+ if stdin_data and stdin_data.strip():
296
+ query_text = stdin_data.strip()
297
+ else:
298
+ print("Error: no query provided (provide as argument or pipe from stdin)", file=sys.stderr)
299
+ print("Usage: sqlite-cli database.sqlite3 'SELECT * FROM table'", file=sys.stderr)
300
+ conn.close()
301
+ sys.exit(1)
302
+
303
+ # Handle dot commands
304
+ if query_text.startswith("."):
305
+ if query_text.startswith(".mode "):
306
+ mode = query_text[6:].strip()
307
+ print(f" Mode set to: {mode}")
308
+ elif not run_dot_command(cursor, query_text, db_path):
309
+ print(f"Unknown dot command: {query_text.split()[0]}", file=sys.stderr)
310
+ sys.exit(1)
311
+ conn.close()
312
+ return
313
+
314
+ # Execute SQL query
315
+ try:
316
+ exec_query(cursor, query_text, mode=args.mode)
317
+ conn.commit()
318
+ except sqlite3.Error as e:
319
+ print(f"Error: {e}", file=sys.stderr)
320
+ sys.exit(1)
321
+ finally:
322
+ conn.close()
323
+
324
+
325
+ if __name__ == "__main__":
326
+ main()
@@ -0,0 +1,5 @@
1
+ """sqlite-cli entry point."""
2
+ from . import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ sysmon — 终端系统监控仪
4
+ 实时显示 CPU、内存、磁盘、进程、网络、uptime
5
+ 使用 curses TUI 渲染
6
+ """
7
+
8
+ import curses
9
+ import psutil
10
+ import time
11
+ import os
12
+ import socket
13
+ import subprocess
14
+ from datetime import datetime, timedelta
15
+
16
+ def get_hostname():
17
+ try:
18
+ return socket.gethostname()
19
+ except:
20
+ return "unknown"
21
+
22
+ def get_uptime():
23
+ try:
24
+ with open('/proc/uptime') as f:
25
+ seconds = float(f.read().split()[0])
26
+ days = int(seconds // 86400)
27
+ hours = int((seconds % 86400) // 3600)
28
+ mins = int((seconds % 3600) // 60)
29
+ parts = []
30
+ if days > 0: parts.append(f"{days}d")
31
+ if hours > 0: parts.append(f"{hours}h")
32
+ parts.append(f"{mins}m")
33
+ return ''.join(parts)
34
+ except:
35
+ return "N/A"
36
+
37
+ def get_sysinfo():
38
+ return {
39
+ 'hostname': get_hostname(),
40
+ 'uptime': get_uptime(),
41
+ 'boot_time': datetime.fromtimestamp(psutil.boot_time()).strftime('%m/%d %H:%M'),
42
+ }
43
+
44
+ def get_cpu():
45
+ return {
46
+ 'percent': psutil.cpu_percent(interval=0.1),
47
+ 'per_cpu': psutil.cpu_percent(interval=0.05, percpu=True),
48
+ 'count': psutil.cpu_count(),
49
+ 'freq': psutil.cpu_freq(),
50
+ 'load': psutil.getloadavg(),
51
+ }
52
+
53
+ def get_memory():
54
+ mem = psutil.virtual_memory()
55
+ swap = psutil.swap_memory()
56
+ return {
57
+ 'total': mem.total,
58
+ 'used': mem.used,
59
+ 'percent': mem.percent,
60
+ 'swap_total': swap.total,
61
+ 'swap_used': swap.used,
62
+ 'swap_percent': swap.percent,
63
+ }
64
+
65
+ def get_disk():
66
+ parts = []
67
+ for p in psutil.disk_partitions():
68
+ try:
69
+ usage = psutil.disk_usage(p.mountpoint)
70
+ parts.append({
71
+ 'mount': p.mountpoint,
72
+ 'device': p.device,
73
+ 'total': usage.total,
74
+ 'used': usage.used,
75
+ 'percent': usage.percent,
76
+ })
77
+ except:
78
+ pass
79
+ return parts
80
+
81
+ def get_network():
82
+ net = psutil.net_io_counters()
83
+ return {
84
+ 'sent': net.bytes_sent,
85
+ 'recv': net.bytes_recv,
86
+ }
87
+
88
+ def get_top_processes(n=8):
89
+ procs = []
90
+ for p in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent', 'status']):
91
+ try:
92
+ info = p.info
93
+ if info['cpu_percent'] is None: continue
94
+ procs.append({
95
+ 'pid': info['pid'],
96
+ 'name': info['name'][:20] if info['name'] else '?',
97
+ 'cpu': info['cpu_percent'],
98
+ 'mem': info['memory_percent'] or 0,
99
+ 'status': info['status'],
100
+ })
101
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
102
+ pass
103
+ procs.sort(key=lambda x: x['cpu'], reverse=True)
104
+ return procs[:n]
105
+
106
+ def format_bytes(b):
107
+ for unit in ['B', 'K', 'M', 'G', 'T']:
108
+ if b < 1024:
109
+ return f"{b:.1f}{unit}"
110
+ b /= 1024
111
+ return f"{b:.1f}P"
112
+
113
+ def draw_bar(win, y, x, width, percent, color_pair=2):
114
+ """Draw a horizontal bar"""
115
+ fill = int(width * percent / 100)
116
+ bar = '█' * fill + '░' * (width - fill)
117
+ try:
118
+ win.addstr(y, x, bar[:width], curses.color_pair(color_pair))
119
+ except:
120
+ pass
121
+
122
+ def main(stdscr):
123
+ curses.curs_set(0)
124
+ curses.use_default_colors()
125
+ curses.init_pair(1, curses.COLOR_CYAN, -1) # headers
126
+ curses.init_pair(2, curses.COLOR_GREEN, -1) # good
127
+ curses.init_pair(3, curses.COLOR_YELLOW, -1) # warning
128
+ curses.init_pair(4, curses.COLOR_RED, -1) # critical
129
+ curses.init_pair(5, curses.COLOR_WHITE, -1) # normal
130
+ curses.init_pair(6, curses.COLOR_MAGENTA, -1) # labels
131
+
132
+ prev_net = get_network()
133
+ prev_time = time.time()
134
+
135
+ while True:
136
+ h, w = stdscr.getmaxyx()
137
+ stdscr.erase()
138
+
139
+ sysinfo = get_sysinfo()
140
+ cpu = get_cpu()
141
+ mem = get_memory()
142
+ disks = get_disk()
143
+ net = get_network()
144
+ procs = get_top_processes()
145
+ now = time.time()
146
+
147
+ # Network rate
148
+ dt = now - prev_time
149
+ net_speed_sent = (net['sent'] - prev_net['sent']) / dt if dt > 0 else 0
150
+ net_speed_recv = (net['recv'] - prev_net['recv']) / dt if dt > 0 else 0
151
+ prev_net = net
152
+ prev_time = now
153
+
154
+ line = 0
155
+
156
+ # ---- Header ----
157
+ if line < h:
158
+ stdscr.addstr(line, 2, f"╔══════════════════════════════════════╗", curses.color_pair(5))
159
+ line += 1
160
+ if line < h:
161
+ stdscr.addstr(line, 2, f"║ sysmon — {sysinfo['hostname']:<15} ║", curses.color_pair(1))
162
+ line += 1
163
+ if line < h:
164
+ stdscr.addstr(line, 2, f"╚══════════════════════════════════════╝", curses.color_pair(5))
165
+ line += 1
166
+
167
+ # ---- Uptime & Load ----
168
+ if line < h:
169
+ stdscr.addstr(line, 2, f"Up {sysinfo['uptime']} ", curses.color_pair(6))
170
+ stdscr.addstr(f"(since {sysinfo['boot_time']}) ", curses.color_pair(5))
171
+ stdscr.addstr(f"Load: {cpu['load'][0]:.2f} {cpu['load'][1]:.2f} {cpu['load'][2]:.2f}")
172
+ line += 1
173
+
174
+ line += 1 # blank
175
+
176
+ # ---- CPU ----
177
+ if line < h:
178
+ stdscr.addstr(line, 2, "CPU", curses.color_pair(1))
179
+ line += 1
180
+ cpu_color = 2 if cpu['percent'] < 50 else (3 if cpu['percent'] < 80 else 4)
181
+ if line < h:
182
+ stdscr.addstr(line, 2, f" {cpu['percent']:5.1f}% ", curses.color_pair(cpu_color))
183
+ draw_bar(stdscr, line, 12, min(w - 16, 30), cpu['percent'], cpu_color)
184
+ if cpu['freq']:
185
+ stdscr.addstr(f" {cpu['freq'].current/1000:.1f}GHz")
186
+ line += 1
187
+
188
+ # Per-core
189
+ if line < h:
190
+ n_cols = min(4, cpu['count'])
191
+ n_rows = (cpu['count'] + n_cols - 1) // n_cols
192
+ for r in range(n_rows):
193
+ if line >= h: break
194
+ stdscr.addstr(line, 2, " ")
195
+ for c in range(n_cols):
196
+ idx = r * n_cols + c
197
+ if idx >= len(cpu['per_cpu']): break
198
+ p = cpu['per_cpu'][idx]
199
+ pc = 2 if p < 50 else (3 if p < 80 else 4)
200
+ stdscr.addstr(f" [{idx}]", curses.color_pair(pc))
201
+ draw_bar(stdscr, line, 8 + c * 14, 8, p, pc)
202
+ stdscr.addstr(f"{p:4.0f}%", curses.color_pair(pc))
203
+ line += 1
204
+
205
+ line += 1
206
+
207
+ # ---- Memory ----
208
+ if line < h:
209
+ stdscr.addstr(line, 2, "MEM", curses.color_pair(1))
210
+ line += 1
211
+ mem_color = 2 if mem['percent'] < 50 else (3 if mem['percent'] < 80 else 4)
212
+ if line < h:
213
+ used_s = format_bytes(mem['used'])
214
+ total_s = format_bytes(mem['total'])
215
+ stdscr.addstr(line, 2, f" {used_s:>7} / {total_s:<7}", curses.color_pair(mem_color))
216
+ draw_bar(stdscr, line, 20, min(w - 28, 30), mem['percent'], mem_color)
217
+ stdscr.addstr(f" {mem['percent']:.0f}%")
218
+ line += 1
219
+
220
+ if mem['swap_total'] > 0 and line < h:
221
+ swap_s = format_bytes(mem['swap_used'])
222
+ swap_t = format_bytes(mem['swap_total'])
223
+ sw_color = 2 if mem['swap_percent'] < 50 else (3 if mem['swap_percent'] < 80 else 4)
224
+ stdscr.addstr(line, 2, f" SWP {swap_s:>7} / {swap_t:<7}", curses.color_pair(sw_color))
225
+ line += 1
226
+
227
+ line += 1
228
+
229
+ # ---- Disk ----
230
+ if line < h:
231
+ stdscr.addstr(line, 2, "DISK", curses.color_pair(1))
232
+ line += 1
233
+ for dsk in disks:
234
+ if line >= h: break
235
+ d_color = 2 if dsk['percent'] < 50 else (3 if dsk['percent'] < 80 else 4)
236
+ mount = dsk['mount']
237
+ if len(mount) > 8: mount = mount[:7] + '~'
238
+ used_s = format_bytes(dsk['used'])
239
+ total_s = format_bytes(dsk['total'])
240
+ stdscr.addstr(line, 2, f" {mount:>8}", curses.color_pair(6))
241
+ stdscr.addstr(f" {used_s:>7} / {total_s:<7}", curses.color_pair(d_color))
242
+ draw_bar(stdscr, line, 30, min(w - 38, 25), dsk['percent'], d_color)
243
+ stdscr.addstr(f" {dsk['percent']:.0f}%")
244
+ line += 1
245
+
246
+ line += 1
247
+
248
+ # ---- Network ----
249
+ if line < h:
250
+ stdscr.addstr(line, 2, "NET", curses.color_pair(1))
251
+ line += 1
252
+ if line < h:
253
+ stdscr.addstr(line, 4, f"↓ {format_bytes(net_speed_recv)}/s ↑ {format_bytes(net_speed_sent)}/s", curses.color_pair(5))
254
+ stdscr.addstr(f" Total: ↓{format_bytes(net['recv'])} ↑{format_bytes(net['sent'])}")
255
+ line += 1
256
+
257
+ line += 1
258
+
259
+ # ---- Top Processes ----
260
+ if line < h:
261
+ stdscr.addstr(line, 2, "PROCESSES", curses.color_pair(1))
262
+ line += 1
263
+ if line < h:
264
+ stdscr.addstr(line, 2, f" {'PID':>5} {'CPU':>5} {'MEM':>5} STATUS NAME", curses.color_pair(6))
265
+ line += 1
266
+ for proc in procs:
267
+ if line >= h: break
268
+ p_cpu_color = 2 if proc['cpu'] < 10 else (3 if proc['cpu'] < 30 else 4)
269
+ stdscr.addstr(line, 2, f" {proc['pid']:>5}", curses.color_pair(5))
270
+ stdscr.addstr(f" {proc['cpu']:4.1f}%", curses.color_pair(p_cpu_color))
271
+ stdscr.addstr(f" {proc['mem']:4.1f}%", curses.color_pair(5))
272
+ stdscr.addstr(f" {proc['status'][:6]:<6}", curses.color_pair(6))
273
+ stdscr.addstr(f" {proc['name']}", curses.color_pair(5))
274
+ line += 1
275
+
276
+ # ---- Footer ----
277
+ if line < h - 1:
278
+ stdscr.addstr(h - 1, 2, f" {datetime.now().strftime('%H:%M:%S')} | q:quit r:refresh", curses.color_pair(6))
279
+
280
+ stdscr.refresh()
281
+
282
+ # Wait for key with timeout (update interval ~1s)
283
+ stdscr.timeout(1000)
284
+ key = stdscr.getch()
285
+ if key == ord('q'):
286
+ break
287
+ elif key == ord('r'):
288
+ continue
289
+
290
+ def entry():
291
+ try:
292
+ curses.wrapper(main)
293
+ except KeyboardInterrupt:
294
+ pass
295
+ except Exception as e:
296
+ print(f"sysmon error: {e}")
297
+
298
+ if __name__ == '__main__':
299
+ entry()
@@ -0,0 +1,3 @@
1
+ """CLI entry point for `python -m sysmon`"""
2
+ from sysmon import main
3
+ main()