xstt 2026.4.23__tar.gz

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.
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: xstt
3
+ Version: 2026.4.23
4
+ Summary: 一些终端实用命令行工具
5
+ Requires-Python: >=3.8
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: python-pyper
8
+ Requires-Dist: rich
9
+ Requires-Dist: typer-slim[standard]
10
+ Requires-Dist: rapidfuzz
11
+ Requires-Dist: pypinyin
File without changes
@@ -0,0 +1,208 @@
1
+ """
2
+ Author: xiash$
3
+ Date: 2025/10/19$
4
+ Project: terminal_tools$
5
+ Description: $
6
+ ***
7
+ * _ooOoo_
8
+ * o8888888o
9
+ * 88" . "88
10
+ * (| -_- |)
11
+ * O\ = /O
12
+ * ___/`---'\____
13
+ * . ' \\| |// `.
14
+ * / \\||| : |||// \
15
+ * / _||||| -:- |||||- \
16
+ * | | \\\ - /// | |
17
+ * | \_| ''\---/'' | |
18
+ * \ .-\__ `-` ___/-. /
19
+ * ___`. .' /--.--\ `. . __
20
+ * ."" '< `.___\_<|>_/___.' >'"".
21
+ * | | : `- \`.;`\ _ /`;.`/ - ` : | |
22
+ * \ \ `-. \_ __\ /__ _/ .-` / /
23
+ * ======`-.____`-.___\_____/___.-`____.-'======
24
+ * `=---='
25
+ * .............................................
26
+ * 佛曰:bug泛滥,我已瘫痪!
27
+ """
28
+ import json
29
+ import typer
30
+ from pypinyin import lazy_pinyin, Style
31
+ from rapidfuzz import process, fuzz
32
+ from pathlib import Path
33
+ from rich import print
34
+ from typing import Iterable
35
+
36
+ CONFIG_PATH = Path.home() / '.bookmarks/config.ini'
37
+
38
+
39
+ def parse_browser_bookmarks(bookmark_files_path: str | Path | Iterable[Path | str]) -> list[dict]:
40
+ if isinstance(bookmark_files_path, (Path, str)):
41
+ bookmark_files_path = [bookmark_files_path]
42
+ bookmarks = []
43
+
44
+ def traverse_nodes(node):
45
+ if node.get('type') == 'url':
46
+ bookmarks.append({
47
+ 'title': node.get('name', ''),
48
+ 'url': node.get('url', '')
49
+ })
50
+ elif node.get('type') == 'folder':
51
+ for child in node.get('children', []):
52
+ traverse_nodes(child)
53
+
54
+ for bookmark_file in bookmark_files_path:
55
+ bookmark_file = Path(str(bookmark_file).strip())
56
+ if not bookmark_file.exists():
57
+ continue
58
+ with open(bookmark_file.as_posix().strip(), 'r', encoding='utf-8') as f:
59
+ data = json.load(f)
60
+
61
+ # 遍历所有根节点 (书签栏, 其他书签等)
62
+ for root_key in data.get('roots', {}):
63
+ traverse_nodes(data['roots'][root_key])
64
+
65
+ return bookmarks
66
+
67
+
68
+ def get_search_keys(text: str) -> list[str]:
69
+ """
70
+ 为一段文本(例如书签标题)生成所有可能的搜索键。
71
+ """
72
+ if not text:
73
+ return []
74
+
75
+ # 确保文本是字符串,pypinyin 处理非中文字符时会原样返回
76
+ text_lower = str(text).lower()
77
+
78
+ # 1. 原始关键字 (小写)
79
+ keys = [text_lower]
80
+
81
+ # 2. 全拼音 (例如: "zhongguoyinhang")
82
+ # style=Style.NORMAL: 普通拼音,不带声调
83
+ # errors='ignore': 忽略无法转换的字符(如特殊符号)
84
+ pinyin_full = "".join(lazy_pinyin(text, style=Style.NORMAL, errors='ignore'))
85
+ if pinyin_full != text_lower:
86
+ keys.append(pinyin_full)
87
+
88
+ # 3. 拼音首字母 (例如: "zgyh")
89
+ # style=Style.FIRST_LETTER: 拼音首字母
90
+ pinyin_initials = "".join(lazy_pinyin(text, style=Style.FIRST_LETTER, errors='ignore'))
91
+ if pinyin_initials != text_lower:
92
+ keys.append(pinyin_initials)
93
+
94
+ return keys
95
+
96
+
97
+ def build_search_index(bookmarks: list[dict]):
98
+ """
99
+ 构建用于 rapidfuzz 搜索的扁平化索引。
100
+
101
+ 返回:
102
+ - search_choices (list[str]): 所有可能的搜索键的扁平列表
103
+ - index_map (dict[int, int]): search_choices的索引 -> bookmarks列表的索引
104
+ """
105
+ search_choices = []
106
+ index_map = {} # key: 索引 in search_choices, value: 索引 in bookmarks
107
+
108
+ for bookmark_index, bookmark in enumerate(bookmarks):
109
+ # 我们主要搜索标题,也可以加入URL
110
+ title = bookmark.get('title', '')
111
+ url = bookmark.get('url', '')
112
+
113
+ # 为标题生成搜索键
114
+ title_keys = get_search_keys(title)
115
+
116
+ # (可选) 为URL的域名部分生成搜索键
117
+ # url_keys = get_search_keys(url.split('/')[2] if '/' in url else url)
118
+ # all_keys = title_keys + url_keys
119
+
120
+ all_keys = title_keys
121
+
122
+ for key in all_keys:
123
+ if key: # 确保键不为空
124
+ search_choices.append(key)
125
+ index_map[len(search_choices) - 1] = bookmark_index
126
+
127
+ return search_choices, index_map
128
+
129
+
130
+ def search_bookmarks(
131
+ query: str,
132
+ bookmarks: list[dict],
133
+ search_choices: list[str],
134
+ index_map: dict[int, int],
135
+ limit: int = 10,
136
+ score_threshold: int = 50
137
+ ):
138
+ """
139
+ 使用 rapidfuzz 进行模糊搜索
140
+ """
141
+ query_lower = query.lower()
142
+
143
+ # rapidfuzz.process.extract() 是核心!
144
+ # 它会返回一个列表: [(匹配到的键, 匹配分数, 在search_choices中的索引), ...]
145
+ # 我们选择一个适合路径和缩写的计分器,如 WPath
146
+ # 我们多搜索一些结果 (limit * 3),因为多个键可能指向同一个书签
147
+ results = process.extract(
148
+ query_lower,
149
+ search_choices,
150
+ scorer=fuzz.WRatio, # WPath 计分器对缩写和顺序错乱有较好支持
151
+ limit=limit
152
+ )
153
+ for (matched_key, score, key_index) in results:
154
+ # 1. 通过映射表找到原始书签的索引
155
+ bookmark_index = index_map[key_index]
156
+
157
+ # 2. 检查是否已经添加过这个书签(避免重复)
158
+ if score > score_threshold:
159
+ # 3. 获取原始书签
160
+ bookmark = bookmarks[bookmark_index]
161
+ yield f'{bookmark["title"]}:{bookmark["url"]}'
162
+
163
+
164
+ def bookmarks_search(
165
+ query: str = typer.Argument('', help="书签查询关键字"),
166
+ import_bookmarks_file: Path = typer.Option(None, '-i', '--import', help="书签文件路径"),
167
+ limit: int = typer.Option(10, '-l', '--limit', min=1, help='限制查询得数量'),
168
+ score: int = typer.Option(50, '-s', '--score', min=0, max=99, help='分数阈值,[0,99]之间')
169
+ ):
170
+ """
171
+ 浏览器书签搜索
172
+ """
173
+ default_bookmarks_file = Path.home() / r'AppData/Local/Microsoft/Edge/User Data/Default/Bookmarks'
174
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
175
+ f = open(CONFIG_PATH, 'a+')
176
+ f.seek(0)
177
+ bookmarks_files = set(line.strip() for line in f if line.strip())
178
+ if not bookmarks_files:
179
+ if default_bookmarks_file.exists():
180
+ bookmarks_files.add(default_bookmarks_file)
181
+ f.write(default_bookmarks_file.as_posix() + '\n')
182
+ else:
183
+ print(f'[yellow]{default_bookmarks_file} 不存在,请通过 --import 参数手动导入浏览器书签文件路径!')
184
+
185
+ if import_bookmarks_file:
186
+ if import_bookmarks_file.as_posix() in bookmarks_files:
187
+ print(f'[yellow]{import_bookmarks_file} 已配置过,无需重复导入!')
188
+ elif import_bookmarks_file.exists():
189
+ bookmarks_files.add(import_bookmarks_file)
190
+ f.write(import_bookmarks_file.as_posix() + '\n')
191
+ print(f'[green]{import_bookmarks_file} 导入成功!')
192
+ else:
193
+ print(f'[yellow]{import_bookmarks_file} 不存在!')
194
+ f.close()
195
+ if query:
196
+ bookmarks = parse_browser_bookmarks(bookmarks_files)
197
+ choices, mapping = build_search_index(bookmarks)
198
+ results = search_bookmarks(query, bookmarks, choices, mapping, limit, score)
199
+ for result in results:
200
+ print(result)
201
+
202
+
203
+ def main():
204
+ typer.run(bookmarks_search)
205
+
206
+
207
+ if __name__ == '__main__':
208
+ main()
@@ -0,0 +1,305 @@
1
+ """
2
+ Author: xiash$
3
+ Date: 2025/5/11$
4
+ Project: terminal_tools$
5
+ Description: $
6
+ ***
7
+ * _ooOoo_
8
+ * o8888888o
9
+ * 88" . "88
10
+ * (| -_- |)
11
+ * O\ = /O
12
+ * ___/`---'\____
13
+ * . ' \\| |// `.
14
+ * / \\||| : |||// \
15
+ * / _||||| -:- |||||- \
16
+ * | | \\\ - /// | |
17
+ * | \_| ''\---/'' | |
18
+ * \ .-\__ `-` ___/-. /
19
+ * ___`. .' /--.--\ `. . __
20
+ * ."" '< `.___\_<|>_/___.' >'"".
21
+ * | | : `- \`.;`\ _ /`;.`/ - ` : | |
22
+ * \ \ `-. \_ __\ /__ _/ .-` / /
23
+ * ======`-.____`-.___\_____/___.-`____.-'======
24
+ * `=---='
25
+ * .............................................
26
+ * 佛曰:bug泛滥,我已瘫痪!
27
+ """
28
+ import argparse
29
+ import json
30
+ import os
31
+ import os.path as osp
32
+ import platform
33
+ from copy import deepcopy
34
+ from pathlib import Path
35
+ from pyper import task
36
+ from typing import Tuple, Iterable
37
+ from rich import print
38
+ from rich.text import Text
39
+ from rich.progress import Progress, TimeElapsedColumn, MofNCompleteColumn
40
+
41
+ cache_dir = os.environ.get('DISK_USAGE_CACHE_DIR', Path.home() / '.disk_usage')
42
+ disk_usage_cache = {}
43
+
44
+
45
+ def total_st_size(path: Path | str) -> Tuple[int, Path]:
46
+ """
47
+ 获取文件或目录(包括所有子目录及文件)大小
48
+ :param path:
49
+ :return:
50
+ """
51
+ path = Path(path)
52
+ if path.is_file():
53
+ return file_st_size(path), path
54
+ elif path.is_symlink():
55
+ return 0, Path(f"{path}(-> {path.readlink()})")
56
+ elif path.is_dir():
57
+ st_mtime = path.stat().st_mtime
58
+ key_path = osp.abspath(path)
59
+ cache_info = disk_usage_cache.get(key_path)
60
+ if cache_info and st_mtime == cache_info[1]:
61
+ size = cache_info[0]
62
+ else:
63
+ # size = sum(st_size(file) for file in path.iter_dir(recursive=True))
64
+ try:
65
+ # size = dir_total_size(path)
66
+ size = dir_total_size_parallel(path)
67
+ except Exception as e:
68
+ # print(f"[red]警告:无法访问 {path},错误:{e}")
69
+ size = 0
70
+ disk_usage_cache[key_path] = (size, st_mtime)
71
+ return size, path
72
+ return 0, path
73
+
74
+
75
+ def dir_total_size_parallel(src: str | Path) -> int:
76
+ """
77
+ 获取目录下所有文件的大小总和,一级子目录并行进行统计
78
+ :param src:
79
+ :return:
80
+ """
81
+ src = Path(src)
82
+ total_num = sum(file_st_size(file) for file in src.iterdir() if file.is_file())
83
+ workers = min(4, sum(1 for file in os.scandir(src) if file.is_dir()))
84
+ if workers == 0:
85
+ return total_num
86
+ pipeline = (
87
+ task(lambda path: (file for file in path.iterdir() if file.is_dir()), branch=True) |
88
+ task(dir_total_size, workers=workers, multiprocess=True)
89
+ )
90
+ for num in pipeline(src):
91
+ total_num += num
92
+ return total_num
93
+
94
+
95
+ def dir_total_size(src_dir):
96
+ src_dir = Path(src_dir)
97
+ if src_dir.is_symlink():
98
+ return 0
99
+ st_mtime = src_dir.stat().st_mtime
100
+ key_path = osp.abspath(src_dir)
101
+ cache_info = disk_usage_cache.get(key_path)
102
+ if cache_info and st_mtime == cache_info[1]:
103
+ return cache_info[0]
104
+ return sum(
105
+ sum(map(lambda file: file_st_size(osp.join(walk_out[0], file)), walk_out[-1])) for walk_out in os.walk(src_dir)
106
+ )
107
+
108
+
109
+ def file_st_size(path: Path | str):
110
+ path = Path(path)
111
+ if path.is_symlink():
112
+ return 0
113
+ try:
114
+ return path.stat().st_size
115
+ except Exception:
116
+ return 0
117
+
118
+
119
+ def convert_bytes(size_in_bytes) -> str:
120
+ """
121
+ 将字节数转换为易读的单位(KB, MB, GB)
122
+ :param size_in_bytes: 字节数
123
+ :return: 转换后的易读大小
124
+ """
125
+ units = ['B', 'KB', 'MB', 'GB', 'TB']
126
+ index = 0
127
+ while size_in_bytes >= 1024 and index < len(units) - 1:
128
+ size_in_bytes /= 1024.0
129
+ index += 1
130
+ return f"{size_in_bytes:.2f} {units[index]}"
131
+
132
+
133
+ def get_disk_usage_in_one_depth(path: str | Path) -> Iterable[Tuple[int, Path]]:
134
+ """
135
+ 获取目标目录下的所有文件和子目录(深度为1)的大小
136
+ :param path:
137
+ :return:
138
+ """
139
+ path = Path(path)
140
+ num_tasks = sum(1 for _ in os.scandir(path))
141
+ if not num_tasks:
142
+ yield 0, path
143
+ return
144
+ workers = min(4, num_tasks)
145
+ pipeline = (
146
+ task(path.iterdir, branch=True) |
147
+ task(total_st_size, workers=workers)
148
+ )
149
+ total_size = 0
150
+ for size, sub_path in pipeline():
151
+ total_size += size
152
+ if not path.is_absolute() and not str(path).startswith('.'):
153
+ sub_path = osp.join('.', sub_path)
154
+ yield size, sub_path
155
+ disk_usage_cache[osp.abspath(path)] = [total_size, osp.getmtime(path)]
156
+ yield total_size, path
157
+
158
+
159
+ def get_disk_usage_all(path: str | Path) -> Iterable[Tuple[int, str]]:
160
+ """
161
+ 获取当前文件或目录及其所有子目录或者文件大小
162
+ :param path:
163
+ :return:
164
+ """
165
+ pipeline = (
166
+ task(os.walk, branch=True) |
167
+ task(_walk_step, workers=4)
168
+ )
169
+ for out in pipeline(path, topdown=False):
170
+ yield from out
171
+
172
+
173
+ def _walk_step(args):
174
+ root, dirs, files = args
175
+ root_st_size = 0
176
+ for file in files:
177
+ file_path = osp.join(root, file)
178
+ file_size = file_st_size(str(file_path))
179
+ root_st_size += file_size
180
+ yield file_size, file_path
181
+ for _dir in dirs:
182
+ dir_path = osp.join(root, _dir)
183
+ dir_st_size = total_st_size(str(dir_path))[0]
184
+ root_st_size += dir_st_size
185
+ disk_usage_cache[osp.abspath(root)] = [root_st_size, osp.getmtime(root)]
186
+ yield root_st_size, root
187
+
188
+
189
+ def format_show_info_rich(size: int, path: str | Path) -> Text:
190
+ """
191
+ 格式化显示信息,rich库
192
+ :param size:
193
+ :param path:
194
+ :return:
195
+ """
196
+ format_size = convert_bytes(size)
197
+ map_color = {
198
+ 'B': 'white',
199
+ 'KB': 'white',
200
+ 'MB': 'green',
201
+ 'GB': 'yellow',
202
+ 'TB': 'red',
203
+ }
204
+ text = Text(f"{format_size:<10}{' ' * 10}{path}", style=map_color[format_size.split()[1]])
205
+ return text
206
+
207
+
208
+ def update_disk_usage_cache_file(disk_usage_cache_file_path: str | Path, cache: dict):
209
+ Path(disk_usage_cache_file_path).parent.mkdir(parents=True, exist_ok=True)
210
+ with open(disk_usage_cache_file_path, 'w', encoding='utf-8') as f:
211
+ json.dump(cache, f, indent=4, ensure_ascii=False)
212
+
213
+
214
+ def show_disk_usage(path: str, recursive: bool = False):
215
+ """
216
+ 获取当前文件或目录及其所有子目录或者文件大小
217
+ :param path:
218
+ :param recursive: 是否递归查询子目录
219
+ :return:
220
+ """
221
+ path = Path(path.removesuffix('"'))
222
+ if not path.exists():
223
+ raise FileNotFoundError(f"{path} does not exist")
224
+ if path.is_file():
225
+ print(format_show_info_rich(file_st_size(path), path))
226
+ return
227
+ global disk_usage_cache
228
+ disk_usage_cache_file_path = get_disk_usage_cache_file(path)
229
+ disk_usage_cache = get_disk_usage_cache(disk_usage_cache_file_path)
230
+ if not recursive:
231
+ results = get_disk_usage_in_one_depth(path)
232
+ else:
233
+ results = get_disk_usage_all(path)
234
+ process = Progress(*Progress.get_default_columns(), TimeElapsedColumn())
235
+ pb = process.add_task(description="[green]正在统计文件及目录...", total=None)
236
+ process.start()
237
+ for sub_size, sub_path in results:
238
+ if not osp.isabs(sub_path) and not str(sub_path).startswith('.'):
239
+ sub_path = osp.join('.', sub_path)
240
+ print(format_show_info_rich(sub_size, sub_path))
241
+ process.update(pb, description='[purple]已统计完成!', )
242
+ process.stop()
243
+ update_disk_usage_cache_file(disk_usage_cache_file_path, disk_usage_cache)
244
+
245
+
246
+ def get_disk_usage_cache_file(path: str | Path) -> Path:
247
+ path = osp.abspath(path)
248
+ if platform.system() == 'Windows':
249
+ disk_usage_cache_file_path = cache_dir / f'{osp.splitdrive(path)[0].strip(":")}.json'
250
+ else:
251
+ disk_usage_cache_file_path = cache_dir / f'{path.split("/")[1]}.json'
252
+ return disk_usage_cache_file_path
253
+
254
+
255
+ def get_disk_usage_cache(disk_usage_cache_file_path: str | Path) -> dict:
256
+ if not osp.exists(disk_usage_cache_file_path):
257
+ return {}
258
+ try:
259
+ with open(disk_usage_cache_file_path, 'r', encoding='utf-8') as f:
260
+ return json.load(f)
261
+ except Exception as e:
262
+ print(f"[yellow]警告:无法读取缓存文件 {disk_usage_cache_file_path},错误:{e}")
263
+ return {}
264
+
265
+
266
+ def clear_unused_cache(path):
267
+ global disk_usage_cache
268
+ disk_usage_cache_file_path = get_disk_usage_cache_file(path)
269
+ disk_usage_cache = get_disk_usage_cache(disk_usage_cache_file_path)
270
+ if not disk_usage_cache:
271
+ print("[yellow]未发现缓存!")
272
+ return
273
+ new_cache = deepcopy(disk_usage_cache)
274
+ progress = Progress(*Progress.get_default_columns(), MofNCompleteColumn(), TimeElapsedColumn())
275
+ pb = progress.add_task(description="[green]正在清除无效缓存...", total=len(disk_usage_cache))
276
+ progress.start()
277
+ pipeline = (
278
+ task(disk_usage_cache.keys, branch=True) |
279
+ task(lambda key: (key, osp.exists(key)), workers=os.cpu_count())
280
+ )
281
+ for cache_key, is_valid in pipeline():
282
+ if not is_valid:
283
+ del new_cache[cache_key]
284
+ print(f"[green]已清除无效缓存:{cache_key}")
285
+ progress.advance(pb)
286
+ update_disk_usage_cache_file(disk_usage_cache_file_path, new_cache)
287
+ progress.update(pb, description='[purple]已清除完成!')
288
+ progress.stop()
289
+
290
+
291
+ def main():
292
+ parser = argparse.ArgumentParser()
293
+ parser.add_argument('path', type=str, nargs='?', default='.', help='要查询的文件或目录路径')
294
+ parser.add_argument('-a', '--all', action='store_true', help='递归查询所有子目录')
295
+ parser.add_argument('-c', '--clear-cache', action='store_true', help='清除无效缓存')
296
+ args = parser.parse_args()
297
+ if args.clear_cache:
298
+ clear_unused_cache(args.path)
299
+
300
+ else:
301
+ show_disk_usage(args.path, args.all)
302
+
303
+
304
+ if __name__ == '__main__':
305
+ main()
@@ -0,0 +1,176 @@
1
+ # @Time: 2025/5/9 上午10:17
2
+ # @Autor: XS
3
+ # @File: fast_cp.py
4
+ # @Software: PyCharm
5
+
6
+ """
7
+ /**
8
+ * _ooOoo_
9
+ * o8888888o
10
+ * 88" . "88
11
+ * (| -_- |)
12
+ * O\ = /O
13
+ * ___/`---'\____
14
+ * . ' \\| |// `.
15
+ * / \\||| : |||// \
16
+ * / _||||| -:- |||||- \
17
+ * | | \\\ - /// | |
18
+ * | \_| ''\---/'' | |
19
+ * \ .-\__ `-` ___/-. /
20
+ * ___`. .' /--.--\ `. . __
21
+ * ."" '< `.___\_<|>_/___.' >'"".
22
+ * | | : `- \`.;`\ _ /`;.`/ - ` : | |
23
+ * \ \ `-. \_ __\ /__ _/ .-` / /
24
+ * ======`-.____`-.___\_____/___.-`____.-'======
25
+ * `=---=' bug泛滥 佛已瘫痪
26
+ """
27
+ import argparse
28
+ import os
29
+ import os.path as osp
30
+ from shutil import copymode
31
+ from pyper import task
32
+ from pathlib import Path
33
+ from rich.progress import Progress, TimeElapsedColumn, TransferSpeedColumn, DownloadColumn
34
+ from rich import print
35
+
36
+ COPY_BUFSIZE = 1024 * 1024 if os.name == 'nt' else 64 * 1024
37
+ progress = Progress(*Progress.get_default_columns(), TransferSpeedColumn(), DownloadColumn(), TimeElapsedColumn())
38
+ task_id = progress.add_task('[green]文件大小统计中...', total=None)
39
+
40
+
41
+ # from typing import Iterable
42
+
43
+
44
+ # def get_all_files(src: (str, MyPath)) -> Iterable[MyPath]:
45
+ # src = MyPath(src)
46
+ # if not src.exists():
47
+ # raise FileNotFoundError(f'{src}不存在')
48
+ # return filter(lambda file: file.is_file(), src.iter_dir(recursive=True))
49
+
50
+
51
+ def is_file_in_snapshot(file: Path, snapshot_time: float):
52
+ try:
53
+ return file.stat().st_mtime <= snapshot_time
54
+ except Exception:
55
+ return False
56
+
57
+
58
+ def _copy(src: str | Path, dst: str | Path) -> str:
59
+ src, dst = Path(src), Path(dst)
60
+ if dst.is_dir():
61
+ dst = dst / src.name
62
+ dst.parent.mkdir(parents=True, exist_ok=True)
63
+ if the_same_file_exists(src, dst):
64
+ progress.advance(task_id, src.stat().st_size)
65
+ return f'[yellow]{src}已存在,直接跳过![/yellow]'
66
+ with open(src, 'rb') as src_file:
67
+ with open(dst, 'wb') as dst_file:
68
+ while True:
69
+ data = src_file.read(COPY_BUFSIZE)
70
+ if not data:
71
+ break
72
+ dst_file.write(data)
73
+ progress.advance(task_id, len(data))
74
+ copymode(src, dst)
75
+ return dst.as_posix()
76
+
77
+
78
+ def _copy_file(file_path: Path, src_dir: str, dst_dir: str) -> str:
79
+ dst_path = Path(str(file_path).replace(src_dir, dst_dir))
80
+ dst_path.parent.mkdir(parents=True, exist_ok=True)
81
+ try:
82
+ return _copy(file_path, dst_path)
83
+ except Exception as e:
84
+ return f'[red]{file_path}拷贝失败,错误信息为:{e}'
85
+
86
+
87
+ def the_same_file_exists(src_file: str | Path, dst_file: str | Path) -> bool:
88
+ src_file, dst_file = Path(src_file), Path(dst_file)
89
+ return dst_file.exists() and src_file.stat().st_size == dst_file.stat().st_size
90
+
91
+
92
+ def fast_copy(src: str, dst: str, workers: int = 20, verbose: bool = False) -> None:
93
+ src, dst = Path(src).absolute(), Path(dst).absolute()
94
+ if not src.exists():
95
+ raise FileNotFoundError(f'{src}不存在!')
96
+
97
+ progress.start()
98
+ desc = f'[green]{src} [yellow]--->[/yellow] {dst}'
99
+ if dst.is_dir() and src.name != dst.name:
100
+ dst = dst / src.name
101
+ if src.is_file():
102
+ total = src.stat().st_size
103
+ progress.update(task_id, total=total, description=desc)
104
+ out = _copy(src, dst)
105
+ verbose and print(out)
106
+
107
+ else:
108
+ dst.parent.mkdir(parents=True, exist_ok=True)
109
+ total = get_dir_total_size_parallel(src)
110
+ # snapshot_time = time.time()
111
+ # total = sum(
112
+ # sum(map(lambda file: osp.getsize(osp.join(walk_out[0], file)), walk_out[-1])) for walk_out in os.walk(src)
113
+ # )
114
+ progress.update(task_id, total=total, description=desc)
115
+ pipeline = (
116
+ task(lambda path: (file for file in path.rglob('*') if file.is_file()), branch=True) |
117
+ task(_copy_file, bind=task.bind(src_dir=str(src), dst_dir=str(dst)), workers=workers)
118
+ )
119
+
120
+ for out in pipeline(src):
121
+ out = str(out)
122
+ verbose and print(out)
123
+ '错误' in out and print(out)
124
+ progress.update(task_id, completed=total)
125
+ progress.stop()
126
+ print(f'[green]拷贝完成![/green]'.center(100, '*'))
127
+
128
+
129
+ def get_file_size(src_file: str | Path) -> int:
130
+ try:
131
+ file_size = osp.getsize(src_file)
132
+ except Exception as e:
133
+ # print(f'[yellow]{src_file} 获取文件大小失败,错误信息为:{e}')
134
+ file_size = 0
135
+ return file_size
136
+
137
+
138
+ def get_dir_total_size(src_dir):
139
+ return sum(
140
+ sum(map(lambda file: get_file_size(str(osp.join(walk_out[0], file))), walk_out[-1])) for walk_out in
141
+ os.walk(src_dir)
142
+ )
143
+
144
+
145
+ def get_dir_total_size_parallel(src: str | Path) -> int:
146
+ """
147
+ 获取目录下所有文件的大小总和,一级子目录并行进行统计
148
+ :param src:
149
+ :return:
150
+ """
151
+ src = Path(src)
152
+ total_num = sum(get_file_size(file) for file in src.iterdir() if file.is_file())
153
+ workers = min(4, sum(1 for file in os.scandir(src) if file.is_dir()))
154
+ if workers == 0:
155
+ return total_num
156
+ pipeline = (
157
+ task(lambda path: (file for file in path.iterdir() if file.is_dir()), branch=True) |
158
+ task(get_dir_total_size, workers=workers, multiprocess=True)
159
+ )
160
+ for num in pipeline(src):
161
+ total_num += num
162
+ return total_num
163
+
164
+
165
+ def main():
166
+ parser = argparse.ArgumentParser()
167
+ parser.add_argument('src', type=str, help='源路径')
168
+ parser.add_argument('dst', type=str, help='目标路径')
169
+ parser.add_argument('-t', '--threads', type=int, default=4, help='线程数,默认值为2倍cpu核心数')
170
+ parser.add_argument('-v', '--verbose', action='store_true', help='显示详细拷贝信息')
171
+ args = parser.parse_args()
172
+ fast_copy(args.src, args.dst, args.threads, args.verbose)
173
+
174
+
175
+ if __name__ == '__main__':
176
+ main()
xstt-2026.4.23/ls.py ADDED
@@ -0,0 +1,167 @@
1
+ """
2
+ Author: xiash$
3
+ Date: 2025/5/16$
4
+ Project: terminal_tools$
5
+ Description: $
6
+ ***
7
+ * _ooOoo_
8
+ * o8888888o
9
+ * 88" . "88
10
+ * (| -_- |)
11
+ * O\ = /O
12
+ * ___/`---'\____
13
+ * . ' \\| |// `.
14
+ * / \\||| : |||// \
15
+ * / _||||| -:- |||||- \
16
+ * | | \\\ - /// | |
17
+ * | \_| ''\---/'' | |
18
+ * \ .-\__ `-` ___/-. /
19
+ * ___`. .' /--.--\ `. . __
20
+ * ."" '< `.___\_<|>_/___.' >'"".
21
+ * | | : `- \`.;`\ _ /`;.`/ - ` : | |
22
+ * \ \ `-. \_ __\ /__ _/ .-` / /
23
+ * ======`-.____`-.___\_____/___.-`____.-'======
24
+ * `=---='
25
+ * .............................................
26
+ * 佛曰:bug泛滥,我已瘫痪!
27
+ """
28
+ import argparse
29
+ import os
30
+
31
+ from rich.console import Console
32
+ from rich.table import Table
33
+ from rich.panel import Panel
34
+ from rich.tree import Tree
35
+ from datetime import datetime
36
+ from pathlib import Path
37
+ from disk_usage import convert_bytes
38
+
39
+ console = Console()
40
+ executable_extensions = {".exe", ".bat", ".cmd", ".ps1", ".msi", ".com", '.vbs'}
41
+ archive_files = ['.zip', '.rar', '.tar.gz', '.tar.bz2', '.7z', '.tar', '.gz', '.gzip', '.bz2', '.xz', '.iso',
42
+ '.cab', 'jar', '.war', '.ear']
43
+
44
+
45
+ def get_permissions(path: str | Path) -> str:
46
+ path = Path(path)
47
+ mode = path.lstat().st_mode # 获取权限位
48
+ perm_list = []
49
+ map_who = {
50
+ '所有者': mode >> 6 & 0o7,
51
+ '组': mode >> 3 & 0o7,
52
+ '其他': mode & 0o7
53
+ }
54
+ # 解析权限
55
+ for name, who in map_who.items():
56
+ temp = []
57
+ if who & 0o4:
58
+ temp.append('读')
59
+ if who & 0o2:
60
+ temp.append('写')
61
+ if who & 0o1:
62
+ temp.append('执行')
63
+ perm_list.append(f"{name}: {','.join(temp)}")
64
+
65
+ return '\n'.join(perm_list)
66
+
67
+
68
+ def list_directory():
69
+ """
70
+ 列出目录下的所有文件及子目录信息(类似ls命令)
71
+ """
72
+ args = parse_args()
73
+ target_dir = args.target_dir
74
+ target_dir = Path(target_dir)
75
+ abs_path = str(target_dir.resolve())
76
+ icon_map = {
77
+ '文件': '📄',
78
+ '目录': '📁',
79
+ '符号链接': '🔗',
80
+ '归档文件(压缩包)': '📦',
81
+ '可执行文件': '💻',
82
+ 'python脚本': '🐍',
83
+ }
84
+ tree = Tree(
85
+ f"{icon_map['目录']} [cyan]{abs_path}",
86
+ guide_style="bold",
87
+ )
88
+
89
+ for file in target_dir.iterdir():
90
+ if file.is_symlink():
91
+ tree.add(f'{icon_map["符号链接"]} [yellow]{file.name}')
92
+ elif file.is_dir():
93
+ tree.add(f'{icon_map["目录"]} [cyan]{file.name}')
94
+ elif file.suffix in archive_files:
95
+ tree.add(f'{icon_map["归档文件(压缩包)"]} [red]{file.name}')
96
+ elif file.suffix == '.py':
97
+ tree.add(f'{icon_map["python脚本"]} [green]{file.name}')
98
+ elif is_executable(file):
99
+ tree.add(f'{icon_map["可执行文件"]} [green]{file.name}')
100
+ else:
101
+ tree.add(f'{icon_map["文件"]} [white]{file.name}')
102
+ console.print(Panel(tree, expand=False))
103
+
104
+
105
+ def is_executable(path: Path) -> bool:
106
+ path = Path(path)
107
+ if os.name == 'nt':
108
+ return path.suffix in executable_extensions
109
+ return path.is_file() and os.access(path, os.X_OK)
110
+
111
+
112
+ def long_list():
113
+ """
114
+ 列出目录下的所有文件及子目录信息(类似ll命令)
115
+ """
116
+ args = parse_args()
117
+ target_dir = args.target_dir
118
+
119
+ target_dir = Path(target_dir)
120
+
121
+ table_title = f"目录 '{target_dir.resolve()}' 的内容"
122
+ table = Table(title=f"[bold cyan]{table_title}[/bold cyan]", show_lines=True)
123
+ table.add_column("名称", justify='center', vertical='middle', no_wrap=True)
124
+ table.add_column("类型", justify='center', vertical='middle')
125
+ table.add_column("权限", justify='center', vertical='middle')
126
+ table.add_column("大小", justify='center', vertical='middle')
127
+ table.add_column("最后修改时间", justify='center', vertical='middle')
128
+ for file in target_dir.iterdir():
129
+ item_stat = file.stat()
130
+ file_st_size = convert_bytes(item_stat.st_size)
131
+ file_name = file.name
132
+ if file.is_symlink():
133
+ file_st_size = '-'
134
+ txt_color = 'yellow'
135
+ item_type = "符号链接"
136
+ file_name = f'{file}(-> {file.readlink()})'
137
+ elif file.is_dir():
138
+ file_st_size = '-'
139
+ txt_color = 'cyan'
140
+ item_type = '目录'
141
+ elif file.is_file() and file.suffix in archive_files:
142
+ txt_color = 'red'
143
+ item_type = '归档文件(压缩包)'
144
+ elif is_executable(file):
145
+ txt_color = 'green'
146
+ item_type = '可执行文件'
147
+ else:
148
+ txt_color = 'white'
149
+ item_type = '文件'
150
+
151
+ mtime_timestamp = item_stat.st_mtime
152
+ mtime_str = datetime.fromtimestamp(mtime_timestamp).strftime('%Y-%m-%d %H:%M:%S')
153
+ item_permissions = get_permissions(file)
154
+ table.add_row(f"[bold {txt_color}]{file_name}", f'[{txt_color}]{item_type}', f'[{txt_color}]{item_permissions}',
155
+ f'[{txt_color}]{file_st_size}', f'[{txt_color}]{mtime_str}')
156
+ console.print(table)
157
+
158
+
159
+ def parse_args():
160
+ parser = argparse.ArgumentParser(description='列出目录下的所有文件及子目录信息')
161
+ parser.add_argument('target_dir', type=str, nargs='?', default='.', help='目标目录')
162
+ args = parser.parse_args()
163
+ return args
164
+
165
+
166
+ if __name__ == '__main__':
167
+ list_directory()
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "xstt"
7
+ version = "2026.04.23"
8
+ description = "一些终端实用命令行工具"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ dependencies = [
12
+ "python-pyper",
13
+ "rich",
14
+ "typer-slim[standard]",
15
+ 'rapidfuzz',
16
+ 'pypinyin'
17
+ ]
18
+
19
+ [project.scripts]
20
+ wcp = "fast_cp:main"
21
+ wdu = "disk_usage:main"
22
+ wll = "ls:long_list"
23
+ wls = "ls:list_directory"
24
+ wrm = "rm:main"
25
+ sq = "book_mark:main"
26
+
27
+ [tool.setuptools]
28
+ py-modules = ["fast_cp", "disk_usage", 'ls', 'rm', 'book_mark']
29
+
30
+ [tool.pixi.workspace]
31
+ channels = ["conda-forge"]
32
+ platforms = ["win-64", 'linux-64']
33
+
34
+ [tool.pixi.pypi-dependencies]
35
+ ttxs = { path = ".", editable = true }
36
+
37
+ [tool.pixi.dependencies]
38
+ python = "3.10.*"
xstt-2026.4.23/rm.py ADDED
@@ -0,0 +1,131 @@
1
+ """
2
+ Author: xiash$
3
+ Date: 2025/5/17$
4
+ Project: terminal_tools$
5
+ Description: $
6
+ ***
7
+ * _ooOoo_
8
+ * o8888888o
9
+ * 88" . "88
10
+ * (| -_- |)
11
+ * O\ = /O
12
+ * ___/`---'\____
13
+ * . ' \\| |// `.
14
+ * / \\||| : |||// \
15
+ * / _||||| -:- |||||- \
16
+ * | | \\\ - /// | |
17
+ * | \_| ''\---/'' | |
18
+ * \ .-\__ `-` ___/-. /
19
+ * ___`. .' /--.--\ `. . __
20
+ * ."" '< `.___\_<|>_/___.' >'"".
21
+ * | | : `- \`.;`\ _ /`;.`/ - ` : | |
22
+ * \ \ `-. \_ __\ /__ _/ .-` / /
23
+ * ======`-.____`-.___\_____/___.-`____.-'======
24
+ * `=---='
25
+ * .............................................
26
+ * 佛曰:bug泛滥,我已瘫痪!
27
+ """
28
+ import argparse
29
+ import shutil
30
+ import os
31
+ import stat
32
+
33
+ from rich.progress import Progress, TimeElapsedColumn
34
+ from rich import print
35
+ from pyper import task
36
+ from pathlib import Path
37
+
38
+ progress = Progress(
39
+ *Progress.get_default_columns(),
40
+ TimeElapsedColumn(),
41
+ )
42
+
43
+
44
+ def remove_force(path: Path | str):
45
+ """
46
+ 删除某个文件或整个目录
47
+ """
48
+ path = Path(path)
49
+ if path.is_file() or path.is_symlink():
50
+ try:
51
+ path.unlink()
52
+ except PermissionError:
53
+ os.chmod(path, stat.S_IWRITE)
54
+ try:
55
+ path.unlink()
56
+ except Exception as e:
57
+ print(f'注意:[yellow]{e}')
58
+ else:
59
+ try:
60
+ shutil.rmtree(path, onerror=remove_readonly)
61
+ except Exception as e:
62
+ print(f'注意:[yellow]{e}')
63
+
64
+
65
+ def fast_rm(path: Path | str, workers: int = 4):
66
+ """
67
+ 快速删除文件或整个目录
68
+ """
69
+ path = Path(path)
70
+ progress_bar = progress.add_task('[green]删除中...', total=None)
71
+ progress.start()
72
+ final_desc = f'[purple]{path} 删除完成!'
73
+ if path.is_file() or path.is_symlink():
74
+ remove_force(path)
75
+ progress.update(progress_bar, total=1, advance=1, description=final_desc)
76
+ else:
77
+ total = len(os.listdir(path))
78
+ progress.update(progress_bar, total=total + 1)
79
+ if total > 0:
80
+ workers = min(total, workers)
81
+ pipeline = (
82
+ task(path.iterdir, branch=True) |
83
+ task(remove_force, workers=workers)
84
+ )
85
+ for _ in pipeline():
86
+ progress.advance(progress_bar)
87
+ shutil.rmtree(path, onerror=remove_readonly)
88
+ progress.update(progress_bar, advance=1, description=final_desc)
89
+
90
+
91
+ def remove_readonly(func, path, exc_info):
92
+ """
93
+ 用于处理权限问题
94
+ """
95
+ try:
96
+ os.chmod(path, stat.S_IWRITE)
97
+ func(path)
98
+ except Exception as e:
99
+ print(f'注意:[yellow]{e}')
100
+
101
+
102
+ def gather_paths(paths: list):
103
+ """
104
+ 将路径列表中的路径进行展开,并返回一个列表
105
+ """
106
+ for path in paths:
107
+ path = Path(path)
108
+ yield path
109
+
110
+
111
+ def main():
112
+ parser = argparse.ArgumentParser()
113
+ parser.add_argument('path', nargs='+', type=str, help='要删除的文件或目录路径')
114
+ parser.add_argument('-t', '--threads', type=int, default=4, help='删除文件时并发线程数')
115
+ args = parser.parse_args()
116
+ num_paths = len(args.path)
117
+ if num_paths == 0:
118
+ print('[red]没有要删除的文件或目录!')
119
+ else:
120
+ workers = min(num_paths, os.cpu_count() * 2)
121
+ pipeline = (
122
+ task(gather_paths, branch=True) |
123
+ task(fast_rm, workers=workers)
124
+ )
125
+ for _ in pipeline(args.path):
126
+ pass
127
+ progress.stop()
128
+
129
+
130
+ if __name__ == '__main__':
131
+ main()
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: xstt
3
+ Version: 2026.4.23
4
+ Summary: 一些终端实用命令行工具
5
+ Requires-Python: >=3.8
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: python-pyper
8
+ Requires-Dist: rich
9
+ Requires-Dist: typer-slim[standard]
10
+ Requires-Dist: rapidfuzz
11
+ Requires-Dist: pypinyin
@@ -0,0 +1,13 @@
1
+ README.md
2
+ book_mark.py
3
+ disk_usage.py
4
+ fast_cp.py
5
+ ls.py
6
+ pyproject.toml
7
+ rm.py
8
+ xstt.egg-info/PKG-INFO
9
+ xstt.egg-info/SOURCES.txt
10
+ xstt.egg-info/dependency_links.txt
11
+ xstt.egg-info/entry_points.txt
12
+ xstt.egg-info/requires.txt
13
+ xstt.egg-info/top_level.txt
@@ -0,0 +1,7 @@
1
+ [console_scripts]
2
+ sq = book_mark:main
3
+ wcp = fast_cp:main
4
+ wdu = disk_usage:main
5
+ wll = ls:long_list
6
+ wls = ls:list_directory
7
+ wrm = rm:main
@@ -0,0 +1,5 @@
1
+ python-pyper
2
+ rich
3
+ typer-slim[standard]
4
+ rapidfuzz
5
+ pypinyin
@@ -0,0 +1,5 @@
1
+ book_mark
2
+ disk_usage
3
+ fast_cp
4
+ ls
5
+ rm