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.
- evolver_tools/__init__.py +2 -0
- evolver_tools/__main__.py +3 -0
- evolver_tools/cli.py +89 -0
- evolver_tools/vendor/b64/__init__.py +2 -0
- evolver_tools/vendor/b64/b64.py +176 -0
- evolver_tools/vendor/cal_tool/__init__.py +1 -0
- evolver_tools/vendor/cal_tool/cli.py +234 -0
- evolver_tools/vendor/chart_cli/__init__.py +444 -0
- evolver_tools/vendor/chart_cli/__main__.py +3 -0
- evolver_tools/vendor/colors/__init__.py +5 -0
- evolver_tools/vendor/colors/__main__.py +97 -0
- evolver_tools/vendor/csv_stats/__init__.py +5 -0
- evolver_tools/vendor/csv_stats/__main__.py +4 -0
- evolver_tools/vendor/csv_stats/analyzer.py +258 -0
- evolver_tools/vendor/csv_stats/cli.py +45 -0
- evolver_tools/vendor/dirsize/__init__.py +183 -0
- evolver_tools/vendor/envcheck/__init__.py +426 -0
- evolver_tools/vendor/ff/__init__.py +427 -0
- evolver_tools/vendor/ff/__main__.py +3 -0
- evolver_tools/vendor/find_dups/__init__.py +7 -0
- evolver_tools/vendor/find_dups/cli.py +392 -0
- evolver_tools/vendor/hashsum/__init__.py +211 -0
- evolver_tools/vendor/hashsum/__main__.py +5 -0
- evolver_tools/vendor/http_live/__init__.py +265 -0
- evolver_tools/vendor/http_live/__main__.py +2 -0
- evolver_tools/vendor/ipinfo/__init__.py +3 -0
- evolver_tools/vendor/ipinfo/__main__.py +30 -0
- evolver_tools/vendor/jq_lite/__init__.py +257 -0
- evolver_tools/vendor/jq_lite/__main__.py +5 -0
- evolver_tools/vendor/json2csv/__init__.py +3 -0
- evolver_tools/vendor/json2csv/__main__.py +82 -0
- evolver_tools/vendor/jsonql/__init__.py +326 -0
- evolver_tools/vendor/jsonql/__main__.py +5 -0
- evolver_tools/vendor/license_cli/__init__.py +1 -0
- evolver_tools/vendor/license_cli/__main__.py +4 -0
- evolver_tools/vendor/license_cli/cli.py +289 -0
- evolver_tools/vendor/markdown_check/__init__.py +211 -0
- evolver_tools/vendor/nb/__init__.py +319 -0
- evolver_tools/vendor/nb/__main__.py +3 -0
- evolver_tools/vendor/passgen/__init__.py +224 -0
- evolver_tools/vendor/portcheck/__init__.py +2 -0
- evolver_tools/vendor/portcheck/__main__.py +66 -0
- evolver_tools/vendor/project_doctor/__init__.py +412 -0
- evolver_tools/vendor/project_doctor/__main__.py +3 -0
- evolver_tools/vendor/ren/__init__.py +283 -0
- evolver_tools/vendor/ren/__main__.py +3 -0
- evolver_tools/vendor/siege_lite/__init__.py +250 -0
- evolver_tools/vendor/siege_lite/__main__.py +3 -0
- evolver_tools/vendor/smellfinder/__init__.py +376 -0
- evolver_tools/vendor/smellfinder/__main__.py +3 -0
- evolver_tools/vendor/sqlite_cli/__init__.py +326 -0
- evolver_tools/vendor/sqlite_cli/__main__.py +5 -0
- evolver_tools/vendor/sysmon/__init__.py +299 -0
- evolver_tools/vendor/sysmon/__main__.py +3 -0
- evolver_tools/vendor/timer/__init__.py +127 -0
- evolver_tools/vendor/treedir/__init__.py +2 -0
- evolver_tools/vendor/treedir/__main__.py +128 -0
- evolver_tools/vendor/urlparse_tool/__init__.py +3 -0
- evolver_tools/vendor/urlparse_tool/cli.py +212 -0
- evolver_tools/vendor/web_summary/__init__.py +341 -0
- evolver_tools/vendor/web_summary/__main__.py +3 -0
- evolver_tools/vendor/wordcount/__init__.py +2 -0
- evolver_tools/vendor/wordcount/__main__.py +101 -0
- evolver_tools-1.4.0.dist-info/METADATA +107 -0
- evolver_tools-1.4.0.dist-info/RECORD +69 -0
- evolver_tools-1.4.0.dist-info/WHEEL +5 -0
- evolver_tools-1.4.0.dist-info/entry_points.txt +34 -0
- evolver_tools-1.4.0.dist-info/licenses/LICENSE +21 -0
- 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,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()
|