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
evolver_tools/cli.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""evolver CLI - Unified interface for all EVOLVER tools."""
|
|
3
|
+
|
|
4
|
+
import sys, importlib, os
|
|
5
|
+
|
|
6
|
+
# Tool registry
|
|
7
|
+
TOOLS = {
|
|
8
|
+
"b64": {"module": "evolver_tools.vendor.b64", "func": "main", "desc": "b64"},
|
|
9
|
+
"cal": {"module": "evolver_tools.vendor.cal_tool.cli", "func": "main", "desc": "Cal"},
|
|
10
|
+
"chart-cli": {"module": "evolver_tools.vendor.chart_cli", "func": "main", "desc": "Chart CLI"},
|
|
11
|
+
"colors": {"module": "evolver_tools.vendor.colors", "func": "main", "desc": "Colors"},
|
|
12
|
+
"csv-stats": {"module": "evolver_tools.vendor.csv_stats.cli", "func": "main", "desc": "csv-stats"},
|
|
13
|
+
"dirsize": {"module": "evolver_tools.vendor.dirsize", "func": "entry", "desc": "Dirsize"},
|
|
14
|
+
"dt": {"module": "evolver_tools.vendor.dt_convert", "func": "main", "desc": "Dt"},
|
|
15
|
+
"ff": {"module": "evolver_tools.vendor.ff", "func": "main", "desc": "Fuzzy Finder"},
|
|
16
|
+
"envcheck": {"module": "evolver_tools.vendor.envcheck", "func": "main", "desc": "Envcheck"},
|
|
17
|
+
"find-dups": {"module": "evolver_tools.vendor.find_dups.cli", "func": "main", "desc": "Find Dups"},
|
|
18
|
+
"hashsum": {"module": "evolver_tools.vendor.hashsum", "func": "main", "desc": "Hashsum"},
|
|
19
|
+
"http-live": {"module": "evolver_tools.vendor.http_live", "func": "main", "desc": "HTTP Live Server"},
|
|
20
|
+
"ipinfo": {"module": "evolver_tools.vendor.ipinfo", "func": "main", "desc": "Ipinfo"},
|
|
21
|
+
"jq-lite": {"module": "evolver_tools.vendor.jq_lite", "func": "main", "desc": "Jq Lite"},
|
|
22
|
+
"json2csv": {"module": "evolver_tools.vendor.json2csv", "func": "main", "desc": "Json2Csv"},
|
|
23
|
+
"jsonql": {"module": "evolver_tools.vendor.jsonql", "func": "main", "desc": "JSONQL"},
|
|
24
|
+
"license-cli": {"module": "evolver_tools.vendor.license_cli.cli", "func": "main", "desc": "License CLI"},
|
|
25
|
+
"markdown-check": {"module": "evolver_tools.vendor.markdown_check", "func": "main", "desc": "Markdown Check"},
|
|
26
|
+
"nb": {"module": "evolver_tools.vendor.nb", "func": "main", "desc": "nb"},
|
|
27
|
+
"passgen": {"module": "evolver_tools.vendor.passgen", "func": "entry", "desc": "Passgen"},
|
|
28
|
+
"portcheck": {"module": "evolver_tools.vendor.portcheck.__main__", "func": "main", "desc": "Portcheck"},
|
|
29
|
+
"project-doctor": {"module": "evolver_tools.vendor.project_doctor", "func": "main", "desc": "Project Doctor"},
|
|
30
|
+
"ren": {"module": "evolver_tools.vendor.ren", "func": "main", "desc": "Ren"},
|
|
31
|
+
"siege-lite": {"module": "evolver_tools.vendor.siege_lite", "func": "main", "desc": "Siege Lite"},
|
|
32
|
+
"smellfinder": {"module": "evolver_tools.vendor.smellfinder", "func": "main", "desc": "Smellfinder"},
|
|
33
|
+
"sqlite-cli": {"module": "evolver_tools.vendor.sqlite_cli", "func": "main", "desc": "Sqlite CLI"},
|
|
34
|
+
"sysmon": {"module": "evolver_tools.vendor.sysmon", "func": "entry", "desc": "Sysmon"},
|
|
35
|
+
"timer": {"module": "evolver_tools.vendor.timer", "func": "entry", "desc": "Timer"},
|
|
36
|
+
"treedir": {"module": "evolver_tools.vendor.treedir.__main__", "func": "main", "desc": "Treedir"},
|
|
37
|
+
"urlparse": {"module": "evolver_tools.vendor.urlparse_tool.cli", "func": "main", "desc": "URL Parse"},
|
|
38
|
+
"web-summary": {"module": "evolver_tools.vendor.web_summary", "func": "main", "desc": "Web Summary"},
|
|
39
|
+
"wordcount": {"module": "evolver_tools.vendor.wordcount.__main__", "func": "main", "desc": "Wordcount"},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def list_tools():
|
|
43
|
+
"""Display all available tools."""
|
|
44
|
+
print('\x1b[1;36m===== EVOLVER Tools v1.4.0 =====\x1b[0m')
|
|
45
|
+
print()
|
|
46
|
+
for name, info in sorted(TOOLS.items()):
|
|
47
|
+
print(f' \033[1;33m{name:<18}\033[0m {info["desc"]}')
|
|
48
|
+
print()
|
|
49
|
+
print(f' Total: {len(TOOLS)} tools')
|
|
50
|
+
print()
|
|
51
|
+
print('Usage: evolver <toolname> [args...]')
|
|
52
|
+
print(' evolver list')
|
|
53
|
+
|
|
54
|
+
def run_tool(tool_name, args):
|
|
55
|
+
if tool_name not in TOOLS:
|
|
56
|
+
print(f'Unknown tool: {tool_name}')
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
info = TOOLS[tool_name]
|
|
59
|
+
mod_path = info["module"]
|
|
60
|
+
func_name = info["func"]
|
|
61
|
+
old_argv = sys.argv
|
|
62
|
+
sys.argv = [tool_name] + args
|
|
63
|
+
try:
|
|
64
|
+
mod = importlib.import_module(mod_path)
|
|
65
|
+
func = getattr(mod, func_name)
|
|
66
|
+
result = func()
|
|
67
|
+
if result is not None:
|
|
68
|
+
print(result)
|
|
69
|
+
except KeyboardInterrupt:
|
|
70
|
+
pass
|
|
71
|
+
except Exception as e:
|
|
72
|
+
print(f'Error running {tool_name}: {e}', file=sys.stderr)
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
finally:
|
|
75
|
+
sys.argv = old_argv
|
|
76
|
+
|
|
77
|
+
def main():
|
|
78
|
+
if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"):
|
|
79
|
+
list_tools()
|
|
80
|
+
return
|
|
81
|
+
tool_name = sys.argv[1]
|
|
82
|
+
args = sys.argv[2:]
|
|
83
|
+
if tool_name == "list":
|
|
84
|
+
list_tools()
|
|
85
|
+
return
|
|
86
|
+
run_tool(tool_name, args)
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
main()
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""b64 — 零依赖 Base64 编解码工具
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
echo "hello" | b64 encode # 从 stdin 编码
|
|
6
|
+
echo "aGVsbG8K" | b64 decode # 从 stdin 解码
|
|
7
|
+
b64 encode file.txt # 从文件编码
|
|
8
|
+
b64 decode file.b64 # 从文件解码
|
|
9
|
+
b64 -n "hello" encode # 从参数编码
|
|
10
|
+
b64 -n "aGVsbG8K" decode # 从参数解码
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
import base64
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
STDIN_MODE_AUTO = 'auto' # read stdin if no file and no -n
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def encode_bytes(data: bytes) -> str:
|
|
22
|
+
return base64.b64encode(data).decode('ascii')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def decode_bytes(data: bytes, strict: bool = False) -> bytes:
|
|
26
|
+
try:
|
|
27
|
+
if strict:
|
|
28
|
+
# Strict mode: reject non-base64 characters
|
|
29
|
+
return base64.b64decode(data.strip(), validate=True)
|
|
30
|
+
return base64.b64decode(data.strip())
|
|
31
|
+
except Exception as e:
|
|
32
|
+
print(f"Error: invalid base64 input — {e}", file=sys.stderr)
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
def is_valid_base64(data: bytes) -> bool:
|
|
36
|
+
"""Check if data is valid base64 by testing decode+re-encode roundtrip."""
|
|
37
|
+
try:
|
|
38
|
+
# Use strict validation to reject non-base64 chars
|
|
39
|
+
decoded = base64.b64decode(data.strip(), validate=True)
|
|
40
|
+
# Verify roundtrip: re-encode and compare (stripped)
|
|
41
|
+
reencoded = base64.b64encode(decoded).rstrip(b'=')
|
|
42
|
+
cleaned = data.strip().rstrip(b'=')
|
|
43
|
+
return reencoded == cleaned
|
|
44
|
+
except Exception:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def read_stdin() -> bytes:
|
|
49
|
+
try:
|
|
50
|
+
return sys.stdin.buffer.read()
|
|
51
|
+
except KeyboardInterrupt:
|
|
52
|
+
sys.exit(1)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main():
|
|
56
|
+
args = sys.argv[1:]
|
|
57
|
+
|
|
58
|
+
# Parse --help / -h
|
|
59
|
+
if not args:
|
|
60
|
+
# Check for piped stdin — auto-detect mode
|
|
61
|
+
if not sys.stdin.isatty():
|
|
62
|
+
raw_stdin = read_stdin()
|
|
63
|
+
if not raw_stdin:
|
|
64
|
+
sys.exit(0)
|
|
65
|
+
if is_valid_base64(raw_stdin):
|
|
66
|
+
result = base64.b64decode(raw_stdin.strip()).decode('utf-8', errors='replace')
|
|
67
|
+
sys.stdout.write(result)
|
|
68
|
+
if not result.endswith('\n'):
|
|
69
|
+
sys.stdout.write('\n')
|
|
70
|
+
else:
|
|
71
|
+
sys.stdout.write(base64.b64encode(raw_stdin).decode('ascii') + '\n')
|
|
72
|
+
return
|
|
73
|
+
print(__doc__.strip())
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
if args[0] in ('-h', '--help'):
|
|
77
|
+
print(__doc__.strip())
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
# Check for -n (inline value)
|
|
81
|
+
if args[0] == '-n':
|
|
82
|
+
if len(args) < 3:
|
|
83
|
+
print("Error: -n requires both <value> and <action>", file=sys.stderr)
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
value = args[1]
|
|
86
|
+
action = args[2]
|
|
87
|
+
|
|
88
|
+
if action == 'encode':
|
|
89
|
+
result = encode_bytes(value.encode('utf-8'))
|
|
90
|
+
elif action == 'decode':
|
|
91
|
+
result = decode_bytes(value)
|
|
92
|
+
result = result.decode('utf-8', errors='replace')
|
|
93
|
+
else:
|
|
94
|
+
print(f"Error: unknown action '{action}' (use encode/decode)", file=sys.stderr)
|
|
95
|
+
sys.exit(1)
|
|
96
|
+
|
|
97
|
+
sys.stdout.write(result)
|
|
98
|
+
if not result.endswith('\n'):
|
|
99
|
+
sys.stdout.write('\n')
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
# Parse <file> <action> or stdin
|
|
103
|
+
action = None
|
|
104
|
+
file_path = None
|
|
105
|
+
|
|
106
|
+
for i, arg in enumerate(args):
|
|
107
|
+
if arg in ('encode', 'decode'):
|
|
108
|
+
action = arg
|
|
109
|
+
elif arg.startswith('-'):
|
|
110
|
+
print(f"Error: unknown option '{arg}'", file=sys.stderr)
|
|
111
|
+
sys.exit(1)
|
|
112
|
+
else:
|
|
113
|
+
file_path = arg
|
|
114
|
+
|
|
115
|
+
if action is None and file_path:
|
|
116
|
+
if not os.path.isfile(file_path):
|
|
117
|
+
print(f"Error: file not found: {file_path}", file=sys.stderr)
|
|
118
|
+
sys.exit(1)
|
|
119
|
+
# Auto-detect: read file, check if it's valid base64
|
|
120
|
+
raw = open(file_path, 'rb').read()
|
|
121
|
+
if is_valid_base64(raw):
|
|
122
|
+
action = 'decode'
|
|
123
|
+
else:
|
|
124
|
+
action = 'encode'
|
|
125
|
+
|
|
126
|
+
if action is None:
|
|
127
|
+
# Remaining args are just the action
|
|
128
|
+
for arg in args:
|
|
129
|
+
if arg in ('encode', 'decode'):
|
|
130
|
+
action = arg
|
|
131
|
+
else:
|
|
132
|
+
file_path = arg
|
|
133
|
+
|
|
134
|
+
if action is None:
|
|
135
|
+
# Try to auto-detect from stdin content if piped
|
|
136
|
+
if not sys.stdin.isatty():
|
|
137
|
+
raw_stdin = read_stdin()
|
|
138
|
+
if not raw_stdin:
|
|
139
|
+
sys.exit(0)
|
|
140
|
+
# Heuristic: if looks like base64, decode; else encode
|
|
141
|
+
try:
|
|
142
|
+
base64.b64decode(raw_stdin.strip())
|
|
143
|
+
result = base64.b64decode(raw_stdin.strip()).decode('utf-8', errors='replace')
|
|
144
|
+
sys.stdout.write(result)
|
|
145
|
+
if not result.endswith('\n'):
|
|
146
|
+
sys.stdout.write('\n')
|
|
147
|
+
except Exception:
|
|
148
|
+
sys.stdout.write(base64.b64encode(raw_stdin).decode('ascii') + '\n')
|
|
149
|
+
return
|
|
150
|
+
print(__doc__.strip())
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
# Read source
|
|
154
|
+
if file_path:
|
|
155
|
+
if not os.path.isfile(file_path):
|
|
156
|
+
print(f"Error: file not found: {file_path}", file=sys.stderr)
|
|
157
|
+
sys.exit(1)
|
|
158
|
+
raw = open(file_path, 'rb').read()
|
|
159
|
+
else:
|
|
160
|
+
raw = read_stdin()
|
|
161
|
+
if not raw:
|
|
162
|
+
sys.exit(0)
|
|
163
|
+
|
|
164
|
+
if action == 'encode':
|
|
165
|
+
result = encode_bytes(raw)
|
|
166
|
+
else:
|
|
167
|
+
decoded = decode_bytes(raw)
|
|
168
|
+
result = decoded.decode('utf-8', errors='replace')
|
|
169
|
+
|
|
170
|
+
sys.stdout.write(result)
|
|
171
|
+
if not result.endswith('\n'):
|
|
172
|
+
sys.stdout.write('\n')
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == '__main__':
|
|
176
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""cal — 终端日历与日期计算器
|
|
3
|
+
|
|
4
|
+
零依赖,基于 Python stdlib calendar 和 datetime。
|
|
5
|
+
支持日历展示、日期差计算、日期加减、年份概览。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import calendar
|
|
10
|
+
import datetime
|
|
11
|
+
import sys
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_terminal_width():
|
|
16
|
+
"""获取终端宽度,默认 80"""
|
|
17
|
+
try:
|
|
18
|
+
return os.get_terminal_size().columns
|
|
19
|
+
except (ValueError, OSError):
|
|
20
|
+
return 80
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def print_calendar(year, month, highlight_today=True):
|
|
24
|
+
"""打印单月日历"""
|
|
25
|
+
cal = calendar.TextCalendar()
|
|
26
|
+
month_cal = cal.formatmonth(year, month)
|
|
27
|
+
|
|
28
|
+
today = datetime.date.today()
|
|
29
|
+
today_str = f" {today.day} "
|
|
30
|
+
today_hl = f"[{today.day:2d}]"
|
|
31
|
+
|
|
32
|
+
lines = month_cal.split("\n")
|
|
33
|
+
output_lines = []
|
|
34
|
+
|
|
35
|
+
# Header: " January 2026"
|
|
36
|
+
header = lines[0].strip()
|
|
37
|
+
output_lines.append(f"\n \033[1;36m{header}\033[0m")
|
|
38
|
+
output_lines.append("")
|
|
39
|
+
|
|
40
|
+
# Day headers: "Mo Tu We Th Fr Sa Su"
|
|
41
|
+
output_lines.append(" " + lines[1].strip())
|
|
42
|
+
|
|
43
|
+
# Week rows
|
|
44
|
+
for line in lines[2:]:
|
|
45
|
+
if not line.strip():
|
|
46
|
+
continue
|
|
47
|
+
formatted = ""
|
|
48
|
+
# Each day occupies 3 chars: " 1 " or " 10"
|
|
49
|
+
for i in range(0, len(line), 3):
|
|
50
|
+
day_str = line[i : i + 3]
|
|
51
|
+
day = day_str.strip()
|
|
52
|
+
if not day:
|
|
53
|
+
formatted += " "
|
|
54
|
+
continue
|
|
55
|
+
day_num = int(day)
|
|
56
|
+
if highlight_today and today.year == year and today.month == month and today.day == day_num:
|
|
57
|
+
formatted += f"\033[1;33m{day_str}\033[0m"
|
|
58
|
+
else:
|
|
59
|
+
formatted += day_str
|
|
60
|
+
output_lines.append(" " + formatted)
|
|
61
|
+
|
|
62
|
+
sys.stdout.write("\n".join(output_lines))
|
|
63
|
+
sys.stdout.write("\n")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def print_year_calendar(year, highlight_today=True):
|
|
67
|
+
"""打印全年日历,每行3个月"""
|
|
68
|
+
today = datetime.date.today()
|
|
69
|
+
cal = calendar.TextCalendar()
|
|
70
|
+
|
|
71
|
+
months_data = []
|
|
72
|
+
for m in range(1, 13):
|
|
73
|
+
month_lines = cal.formatmonth(year, m).split("\n")
|
|
74
|
+
months_data.append(month_lines)
|
|
75
|
+
|
|
76
|
+
# Pad all months to same height
|
|
77
|
+
max_height = max(len(m) for m in months_data)
|
|
78
|
+
for m in months_data:
|
|
79
|
+
while len(m) < max_height:
|
|
80
|
+
m.append("")
|
|
81
|
+
|
|
82
|
+
# Print in groups of 3
|
|
83
|
+
for group_start in range(0, 12, 3):
|
|
84
|
+
group = months_data[group_start : group_start + 3]
|
|
85
|
+
for row_idx in range(max_height):
|
|
86
|
+
parts = []
|
|
87
|
+
for month_idx, month_lines in enumerate(group):
|
|
88
|
+
line = month_lines[row_idx]
|
|
89
|
+
actual_month = group_start + month_idx + 1
|
|
90
|
+
if row_idx == 0:
|
|
91
|
+
# Month name header - center in 20 chars
|
|
92
|
+
month_name = line.strip()
|
|
93
|
+
parts.append(f" {month_name:^20}")
|
|
94
|
+
elif row_idx == 1:
|
|
95
|
+
parts.append(f" {'Mo Tu We Th Fr Sa Su':20}")
|
|
96
|
+
else:
|
|
97
|
+
# Process day numbers with highlight
|
|
98
|
+
formatted = ""
|
|
99
|
+
if highlight_today and today.year == year:
|
|
100
|
+
for i in range(0, len(line), 3):
|
|
101
|
+
day_str = line[i : i + 3] if i + 3 <= len(line) else line[i:]
|
|
102
|
+
day = day_str.strip()
|
|
103
|
+
if day and today.month == actual_month and today.day == int(day):
|
|
104
|
+
formatted += f"\033[1;33m{day_str}\033[0m"
|
|
105
|
+
else:
|
|
106
|
+
formatted += day_str
|
|
107
|
+
else:
|
|
108
|
+
formatted = line
|
|
109
|
+
parts.append(f" {formatted:20}")
|
|
110
|
+
sys.stdout.write("".join(parts) + "\n")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def cmd_calendar(args):
|
|
114
|
+
"""日历展示子命令"""
|
|
115
|
+
today = datetime.date.today()
|
|
116
|
+
year = args.year or today.year
|
|
117
|
+
month = args.month or today.month
|
|
118
|
+
|
|
119
|
+
if month:
|
|
120
|
+
print_calendar(year, month, highlight_today=not args.no_highlight)
|
|
121
|
+
else:
|
|
122
|
+
print_year_calendar(year, highlight_today=not args.no_highlight)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def cmd_diff(args):
|
|
126
|
+
"""日期差计算"""
|
|
127
|
+
try:
|
|
128
|
+
d1 = datetime.date.fromisoformat(args.diff[0])
|
|
129
|
+
d2 = datetime.date.fromisoformat(args.diff[1])
|
|
130
|
+
except ValueError:
|
|
131
|
+
sys.stderr.write(f"错误: 日期格式无效,请使用 YYYY-MM-DD 格式\n")
|
|
132
|
+
sys.exit(1)
|
|
133
|
+
|
|
134
|
+
delta = abs((d2 - d1).days)
|
|
135
|
+
years = delta // 365
|
|
136
|
+
months_rem = (delta % 365) // 30
|
|
137
|
+
weeks = delta // 7
|
|
138
|
+
days_rem = delta % 7
|
|
139
|
+
|
|
140
|
+
print(f" {args.diff[0]} → {args.diff[1]}")
|
|
141
|
+
print(f" ─{'─' * 40}")
|
|
142
|
+
print(f" 📅 {delta} 天")
|
|
143
|
+
if years or months_rem:
|
|
144
|
+
print(f" ≈ {years} 年 {months_rem} 月")
|
|
145
|
+
if weeks:
|
|
146
|
+
print(f" = {weeks} 周 {days_rem} 天")
|
|
147
|
+
print(f" = {delta * 24} 小时")
|
|
148
|
+
print(f" = {delta * 24 * 60} 分钟")
|
|
149
|
+
print()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def cmd_add(args):
|
|
153
|
+
"""日期加减"""
|
|
154
|
+
try:
|
|
155
|
+
d = datetime.date.fromisoformat(args.date)
|
|
156
|
+
except ValueError:
|
|
157
|
+
sys.stderr.write(f"错误: 日期格式无效,请使用 YYYY-MM-DD\n")
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
|
|
160
|
+
result = d + datetime.timedelta(days=args.days)
|
|
161
|
+
direction = "后" if args.days >= 0 else "前"
|
|
162
|
+
|
|
163
|
+
print(f" {args.date} + {args.days}天 = {result.isoformat()}")
|
|
164
|
+
print(f" {args.date} 的 {abs(args.days)}天{direction}是 {result.isoformat()}")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def main():
|
|
168
|
+
parser = argparse.ArgumentParser(
|
|
169
|
+
description="终端日历与日期计算器",
|
|
170
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
171
|
+
epilog="""
|
|
172
|
+
示例:
|
|
173
|
+
cal 显示本月日历
|
|
174
|
+
cal 2025 3 显示2025年3月
|
|
175
|
+
cal --year 2026 显示2026全年日历
|
|
176
|
+
cal --diff 2024-01-01 2024-12-31 计算日期差
|
|
177
|
+
cal --add 2024-01-01 30 增加30天
|
|
178
|
+
cal --add 2024-03-01 -7 减7天
|
|
179
|
+
""",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Diff mode
|
|
183
|
+
parser.add_argument("--diff", nargs=2, metavar=("DATE1", "DATE2"), help="计算两个日期之间的天数")
|
|
184
|
+
# Add mode
|
|
185
|
+
parser.add_argument("--add", nargs=2, metavar=("DATE", "DAYS"), help="日期加减天数")
|
|
186
|
+
|
|
187
|
+
# Calendar display args
|
|
188
|
+
parser.add_argument("year", nargs="?", type=int, default=None, help="年份")
|
|
189
|
+
parser.add_argument("month", nargs="?", type=int, default=None, help="月份 (1-12)")
|
|
190
|
+
parser.add_argument("--year", dest="show_year", action="store_true", help="显示全年日历")
|
|
191
|
+
parser.add_argument("--no-highlight", action="store_true", help="不突出显示今天")
|
|
192
|
+
|
|
193
|
+
args = parser.parse_args()
|
|
194
|
+
|
|
195
|
+
# Route to subcommands
|
|
196
|
+
if args.diff:
|
|
197
|
+
cmd_diff(args)
|
|
198
|
+
elif args.add:
|
|
199
|
+
# Parse days from string
|
|
200
|
+
try:
|
|
201
|
+
date_str, days_str = args.add
|
|
202
|
+
days = int(days_str)
|
|
203
|
+
except ValueError:
|
|
204
|
+
sys.stderr.write("错误: DAYS 必须为整数\n")
|
|
205
|
+
sys.exit(1)
|
|
206
|
+
args.date = date_str
|
|
207
|
+
args.days = days
|
|
208
|
+
cmd_add(args)
|
|
209
|
+
else:
|
|
210
|
+
# Calendar mode
|
|
211
|
+
if args.month is not None and (args.month < 1 or args.month > 12):
|
|
212
|
+
sys.stderr.write("错误: 月份必须在 1-12 之间\n")
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
|
|
215
|
+
calendar.setfirstweekday(calendar.MONDAY)
|
|
216
|
+
|
|
217
|
+
today = datetime.date.today()
|
|
218
|
+
year = args.year or today.year
|
|
219
|
+
month = args.month
|
|
220
|
+
|
|
221
|
+
if args.show_year:
|
|
222
|
+
print_year_calendar(year, highlight_today=not args.no_highlight)
|
|
223
|
+
elif month:
|
|
224
|
+
print_calendar(year, month, highlight_today=not args.no_highlight)
|
|
225
|
+
else:
|
|
226
|
+
# Just year provided? Show whole year
|
|
227
|
+
if args.year:
|
|
228
|
+
print_year_calendar(args.year, highlight_today=not args.no_highlight)
|
|
229
|
+
else:
|
|
230
|
+
print_calendar(today.year, today.month, highlight_today=not args.no_highlight)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
if __name__ == "__main__":
|
|
234
|
+
main()
|