Skip to content

Commit e87b623

Browse files
committed
[工具集合更新]: 新增图标生成工具和目录树工具,优化文档结构
- icon-maker: 新增Windows图标生成工具make_ico.py,重构make_icns模块为icon-maker,提取公共函数到icon_common.py - tree: 新增目录树生成工具tree.py,支持彩色输出和跨平台兼容 - README: 更新工具列表和描述,添加分类说明和快速开始指南
1 parent bad22ae commit e87b623

File tree

5 files changed

+198
-43
lines changed

5 files changed

+198
-43
lines changed

README.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,23 @@
55

66
让开发和生活都更快乐的小工具集合 🧰
77

8-
| 模块 | 一句话描述 | 文件 |
8+
## 📦 工具列表
9+
10+
| 模块 | 描述 | 文件 |
911
|---|---|---|
1012
| **cli_logger** | loguru 日志配置示例,控制台 + 文件双通道输出 | [`cli_logger.py`](cli_logger/cli_logger.py) |
11-
| **dirwatch** | 实时监控文件夹变化(增 / 删 / 改 / 重命名) | [`dirwatch.py`](dirwatch/dirwatch.py) |
13+
| **dirwatch** | 实时监控文件夹变化(增/删/改/重命名) | [`dirwatch.py`](dirwatch/dirwatch.py) |
14+
| **icon-maker** | 一键生成 macOS / Windows 应用图标(`.icns` / `.ico`| [`make_icns.py`](icon-maker/make_icns.py) / [`make_ico.py`](icon-maker/make_ico.py) |
1215
| **m3u8_download** | m3u8 下载器,自动合并 ts 为单个视频 | [`m3u8_dl.py`](m3u8_download/m3u8_dl.py) |
13-
| **make_icns** | 一键生成 macOS 应用图标(`.icns`| [`make_icns.py`](make_icns/make_icns.py) |
14-
| **procmon** | 按进程名实时监控 CPU / 内存 / 线程 / 句柄 | [`procmon.py`](procmon/procmon.py) |
15-
| **resolve** | 域名解析,快速获取 IP、端口、协议 | [`resolve.py`](resolve/resolve.py) |
16+
| **procmon** | 按进程名实时监控 CPU/内存/线程/句柄 | [`procmon.py`](procmon/procmon.py) |
17+
| **resolve** | 域名解析工具,快速获取 IP、端口、协议信息 | [`resolve.py`](resolve/resolve.py) |
1618
| **syncthing** | Syncthing API 封装,监控文件夹与设备状态 | [`syncthing_monitor.py`](syncthing/syncthing_monitor.py) |
17-
| **webdav** | 轻量级 WebDAV 客户端,支持上传 / 下载 / 删除 / 移动 | [`webdav.py`](webdav/webdav.py) |
19+
| **tree** | 可视化目录树生成工具 | [`tree.py`](tree/tree.py) |
20+
| **webdav** | 轻量级 WebDAV 客户端,支持上传/下载/删除/移动 | [`webdav.py`](webdav/webdav.py) |
21+
22+
## 🚀 快速开始
1823

19-
## 🚀 一键安装依赖
24+
### 安装依赖
2025

2126
```bash
2227
pip install -r requirements.txt

icon-maker/icon_common.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# -*- coding: utf-8 -*-
2+
from typing import Tuple, Optional
3+
from PIL import Image
4+
5+
6+
def parse_hex_color(hex_color: str) -> Tuple[int, int, int, int]:
7+
hex_color = hex_color.lstrip("#")
8+
n = len(hex_color)
9+
if n == 3:
10+
hex_color = "".join(c * 2 for c in hex_color) + "FF"
11+
elif n == 4:
12+
hex_color = "".join(c * 2 for c in hex_color)
13+
elif n == 6:
14+
hex_color += "FF"
15+
elif n != 8:
16+
raise ValueError("颜色格式错误,支持:#RGB、#RGBA、#RRGGBB、#RRGGBBAA")
17+
return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4, 6))
18+
19+
20+
def square_image(
21+
img: Image.Image, bg_color: Optional[Tuple[int, int, int, int]] = None
22+
) -> Image.Image:
23+
if img.width == img.height:
24+
return img.copy()
25+
side = max(img.width, img.height)
26+
canvas = Image.new("RGBA", (side, side), bg_color or (0, 0, 0, 0))
27+
x, y = (side - img.width) // 2, (side - img.height) // 2
28+
canvas.paste(img, (x, y), img if img.mode == "RGBA" else None)
29+
return canvas
Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#!/usr/bin/env python3
21
# -*- coding: utf-8 -*-
32
"""
43
make-icns.py
@@ -15,46 +14,12 @@
1514
import tempfile
1615
import subprocess
1716
from PIL import Image, ImageOps
17+
from icon_common import parse_hex_color, square_image
1818

1919
# icns 所需全部分辨率
2020
ICNS_SIZES = [16, 32, 64, 128, 256, 512, 1024]
2121

2222

23-
def parse_hex_color(hex_color: str):
24-
"""
25-
支持 #RGB、#RGBA、#RRGGBB、#RRGGBBAA
26-
返回 (R, G, B, A) 0-255
27-
"""
28-
hex_color = hex_color.lstrip("#")
29-
n = len(hex_color)
30-
if n == 3: # #RGB -> #RRGGBB + 不透明
31-
hex_color = "".join(c * 2 for c in hex_color) + "FF"
32-
elif n == 4: # #RGBA -> #RRGGBBAA
33-
hex_color = "".join(c * 2 for c in hex_color)
34-
elif n == 6: # #RRGGBB -> 不透明
35-
hex_color += "FF"
36-
elif n != 8:
37-
raise ValueError("颜色格式错误,支持:#RGB、#RGBA、#RRGGBB、#RRGGBBAA")
38-
39-
return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4, 6))
40-
41-
42-
def square_image(img: Image.Image, bg_color=None) -> Image.Image:
43-
"""将 img 做成最小外接正方形,bg_color=None 代表透明"""
44-
if img.width == img.height:
45-
return img.copy()
46-
side = max(img.width, img.height)
47-
if bg_color is None: # 透明
48-
new_im = Image.new("RGBA", (side, side), (0, 0, 0, 0))
49-
else: # 纯色
50-
new_im = Image.new("RGBA", (side, side), bg_color)
51-
# 居中粘贴
52-
x = (side - img.width) // 2
53-
y = (side - img.height) // 2
54-
new_im.paste(img, (x, y), img if img.mode == "RGBA" else None)
55-
return new_im
56-
57-
5823
def build_icns(source_img: Image.Image, output_icns: str):
5924
"""生成 .icns 文件"""
6025
tempdir = tempfile.mkdtemp()

icon-maker/make_ico.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
make_ico.py
4+
将任意图片快速生成多尺寸 ico 图标。
5+
6+
用法:
7+
./make_ico.py input.png # 透明背景
8+
./make_ico.py input.jpg "#FF00FF" # 指定背景色
9+
./make_ico.py input.tiff -o MyIcon.ico # 指定输出文件名
10+
"""
11+
import os
12+
import sys
13+
import argparse
14+
from PIL import Image, ImageOps
15+
from icon_common import parse_hex_color, square_image
16+
17+
# 常用 ico 尺寸(Windows 推荐倒序存放)
18+
# Pillow 还是会从小到大生成,无视传入的顺序
19+
ICO_SIZES = [256, 128, 64, 48, 32, 16]
20+
21+
22+
def build_ico(source_img: Image.Image, output_ico: str) -> None:
23+
"""生成 .ico 文件"""
24+
25+
icons = []
26+
for size in ICO_SIZES:
27+
icons.append(source_img.resize((size, size), Image.LANCZOS))
28+
29+
# 保存为 ico(Pillow 会自动打包多尺寸)
30+
# 写入为PNG压缩格式,windows 文件属性分辨率只能识别第一张图,即16x16
31+
icons[0].save(
32+
output_ico,
33+
format="ICO",
34+
sizes=[(s, s) for s in ICO_SIZES],
35+
append_images=icons[1:],
36+
)
37+
print(f"✅ 已生成 {output_ico}(含尺寸:{ICO_SIZES})")
38+
39+
40+
def main(argv=None):
41+
parser = argparse.ArgumentParser(description="把任意图片做成 ico 图标")
42+
parser.add_argument("input", help="输入图片路径")
43+
parser.add_argument("bgcolor", nargs="?", help="背景色(十六进制,可选,默认透明)")
44+
parser.add_argument("-o", "--output", help="输出 .ico 文件名(可选)")
45+
args = parser.parse_args()
46+
47+
if not os.path.isfile(args.input):
48+
sys.exit("输入文件不存在")
49+
50+
# 1. 读图
51+
img = Image.open(args.input).convert("RGBA")
52+
53+
# 2. 背景色
54+
if args.bgcolor:
55+
bg_color = parse_hex_color(args.bgcolor)
56+
else:
57+
bg_color = None # 透明
58+
59+
# 3. 正方形化
60+
square = square_image(img, bg_color)
61+
62+
# 4. 输出文件名
63+
if args.output:
64+
out_ico = args.output
65+
else:
66+
base, _ = os.path.splitext(args.input)
67+
out_ico = base + ".ico"
68+
69+
# 5. 生成 ico
70+
build_ico(square, out_ico)
71+
print("已生成 →", out_ico)
72+
73+
74+
if __name__ == "__main__":
75+
main()

tree/tree.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
"""
4+
tree.py
5+
递归或非递归打印目录结构,效果类似 Linux 的 tree 命令。
6+
7+
用法:
8+
python tree.py /path/to/dir # 仅列一层
9+
python tree.py /path/to/dir -r # 递归子目录
10+
python tree.py -h
11+
"""
12+
import os
13+
import sys
14+
import argparse
15+
from pathlib import Path
16+
from typing import List
17+
18+
19+
class Colors:
20+
OK = "\033[92m"
21+
WARN = "\033[93m"
22+
ERR = "\033[91m"
23+
BOLD = "\033[1m"
24+
END = "\033[0m"
25+
26+
@staticmethod
27+
def enable_windows_color() -> None:
28+
if sys.platform == "win32":
29+
os.system("")
30+
31+
32+
Colors.enable_windows_color()
33+
34+
PREFIX_MIDDLE = "├── "
35+
PREFIX_LAST = "└── "
36+
PREFIX_CONT = "│ "
37+
PREFIX_EMPTY = " "
38+
39+
40+
def tree(dir_path: Path, prefix: str = "", recursive: bool = False) -> None:
41+
"""打印目录树,recursive 控制是否进入子目录"""
42+
entries: List[Path] = sorted(
43+
dir_path.iterdir(), key=lambda p: (p.is_file(), p.name.lower())
44+
)
45+
for idx, path in enumerate(entries):
46+
is_last = idx == len(entries) - 1
47+
connector = PREFIX_LAST if is_last else PREFIX_MIDDLE
48+
if path.is_dir():
49+
print(f"{prefix}{connector}{Colors.BOLD}{path.name}{Colors.END}")
50+
if recursive: # 仅递归时继续深入
51+
extension = PREFIX_EMPTY if is_last else PREFIX_CONT
52+
tree(path, prefix + extension, recursive=True)
53+
else:
54+
print(f"{prefix}{connector}{path.name}")
55+
56+
57+
def main(argv: List[str] = None) -> None:
58+
parser = argparse.ArgumentParser(description="打印目录结构")
59+
parser.add_argument("directory", help="要列出的目录路径")
60+
parser.add_argument(
61+
"-r", "--recursive", action="store_true", help="递归子目录(默认不递归)"
62+
)
63+
args = parser.parse_args(argv)
64+
65+
root = Path(args.directory).expanduser().resolve()
66+
if not root.is_dir():
67+
print(f"{Colors.ERR}错误: {root} 不是有效目录{Colors.END}", file=sys.stderr)
68+
sys.exit(2)
69+
70+
print(f"{Colors.OK}{root}{Colors.END}")
71+
try:
72+
tree(root, recursive=args.recursive)
73+
except KeyboardInterrupt:
74+
sys.exit(130)
75+
except Exception as e:
76+
print(f"{Colors.ERR}遍历失败: {e}{Colors.END}", file=sys.stderr)
77+
sys.exit(1)
78+
79+
80+
if __name__ == "__main__":
81+
main()

0 commit comments

Comments
 (0)