From 925894a1657fcfd4974bf0a16fe7bfe11a9594c3 Mon Sep 17 00:00:00 2001 From: sitga <2389647927@qq.com> Date: Fri, 20 Mar 2026 14:04:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 205 ++++++++- devkit.py | 10 + devkit_cli.egg-info/PKG-INFO | 27 ++ devkit_cli.egg-info/SOURCES.txt | 13 + devkit_cli.egg-info/dependency_links.txt | 1 + devkit_cli.egg-info/entry_points.txt | 3 + devkit_cli.egg-info/requires.txt | 3 + devkit_cli.egg-info/top_level.txt | 1 + devkit_cli/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 162 bytes devkit_cli/__pycache__/cli.cpython-314.pyc | Bin 0 -> 3265 bytes .../__pycache__/file_rename.cpython-314.pyc | Bin 0 -> 13234 bytes devkit_cli/__pycache__/info.cpython-314.pyc | Bin 0 -> 9435 bytes devkit_cli/__pycache__/todo.cpython-314.pyc | Bin 0 -> 24537 bytes devkit_cli/cli.py | 66 +++ devkit_cli/file_rename.py | 222 ++++++++++ devkit_cli/info.py | 191 +++++++++ devkit_cli/todo.py | 397 ++++++++++++++++++ pyproject.toml | 42 ++ 19 files changed, 1180 insertions(+), 1 deletion(-) create mode 100644 devkit.py create mode 100644 devkit_cli.egg-info/PKG-INFO create mode 100644 devkit_cli.egg-info/SOURCES.txt create mode 100644 devkit_cli.egg-info/dependency_links.txt create mode 100644 devkit_cli.egg-info/entry_points.txt create mode 100644 devkit_cli.egg-info/requires.txt create mode 100644 devkit_cli.egg-info/top_level.txt create mode 100644 devkit_cli/__init__.py create mode 100644 devkit_cli/__pycache__/__init__.cpython-314.pyc create mode 100644 devkit_cli/__pycache__/cli.cpython-314.pyc create mode 100644 devkit_cli/__pycache__/file_rename.cpython-314.pyc create mode 100644 devkit_cli/__pycache__/info.cpython-314.pyc create mode 100644 devkit_cli/__pycache__/todo.cpython-314.pyc create mode 100644 devkit_cli/cli.py create mode 100644 devkit_cli/file_rename.py create mode 100644 devkit_cli/info.py create mode 100644 devkit_cli/todo.py create mode 100644 pyproject.toml diff --git a/README.md b/README.md index 9770379..1ca5f46 100644 --- a/README.md +++ b/README.md @@ -1 +1,204 @@ -# devkit-cli \ No newline at end of file +# devkit-cli + +开发者工具集命令行工具 - 系统信息、文件重命名、任务管理 + +--- + +## 🚀 安装和运行 + +### 环境要求 +- Python 3.8+ +- Windows / macOS / Linux + +### 安装依赖 + +```bash +# 克隆或下载项目后,进入项目目录 +cd devkit-cli + +# 创建虚拟环境(推荐) +python -m venv .venv + +# 激活虚拟环境 +# Windows (PowerShell): +.venv\Scripts\Activate.ps1 + +# Windows (CMD): +# .venv\Scripts\activate.bat + +# macOS/Linux: +# source .venv/bin/activate + +# 安装依赖 +pip install typer rich psutil + +# 以开发模式安装CLI工具 +pip install -e . +``` + +### 运行方式 + +**方式一:使用命令行工具(推荐)** +```bash +# 激活虚拟环境后可直接使用 +devkit-cli --help +devkit --help # 短名命令也可用 +``` + +**方式二:直接运行Python脚本** +```bash +python devkit.py --help +``` + +--- + +## 📖 功能使用说明 + +### 1. 📊 系统信息 - info 子命令 + +查看本机系统信息(CPU、内存、磁盘、系统版本),以彩色表格展示。 + +**使用方法:** +```bash +devkit-cli info show +``` + +**包含内容:** +- 基本信息(系统版本、架构、主机名) +- CPU信息(物理/逻辑核心数、频率、各核心使用率) +- 内存信息(物理内存、交换内存的总量和使用率) +- 磁盘信息(各分区的文件系统、容量和使用情况) +- 网络接口(IP地址、MAC地址) + +--- + +### 2. 📁 文件重命名 - file 子命令 + +批量文件重命名,支持自定义前缀、自动编号、扩展名过滤。 + +**常用命令:** + +```bash +# 列出目录中的文件 +devkit-cli file list # 当前目录 +devkit-cli file list ./photos # 指定目录 +devkit-cli file list --ext .jpg # 只显示jpg文件 +devkit-cli file list -e .jpg -e .png # 支持多个扩展名 + +# 预览重命名(不实际执行) +devkit-cli file rename --prefix vacation_ --dry-run + +# 实际执行重命名 +devkit-cli file rename --prefix img_ --digits 4 +devkit-cli file rename --prefix doc_ --start 100 + +# 指定目录和扩展名 +devkit-cli file rename ./photos --prefix summer_ --ext .jpg + +# 跳过确认提示 +devkit-cli file rename --prefix test_ --yes +``` + +**参数说明:** +| 参数 | 短名 | 说明 | 默认值 | +|------|------|------|--------| +| `--prefix` | `-p` | 文件名前缀(必填) | - | +| `--start` | `-s` | 起始编号 | 1 | +| `--digits` | `-d` | 编号位数(如 3=001) | 3 | +| `--ext` | `-e` | 指定文件扩展名(可多次使用) | 所有文件 | +| `--dry-run` | `-n` | 预览模式,不实际执行 | False | +| `--yes` | `-y` | 跳过确认提示 | False | + +--- + +### 3. ✅ 任务管理 - todo 子命令 + +本地任务管理,支持添加、查看、标记完成、删除。 + +**任务操作:** + +```bash +# 添加任务 +devkit-cli todo add "完成项目文档" +devkit-cli todo add "代码审查" --desc "审查PR #123" --priority high + +# 查看任务列表 +devkit-cli todo list # 查看未完成任务 +devkit-cli todo list --all # 查看所有任务(包括已完成) +devkit-cli todo list --priority high # 按优先级过滤 + +# 查看单个任务详情 +devkit-cli todo show 1 + +# 标记任务状态 +devkit-cli todo done 1 # 标记完成 +devkit-cli todo undone 1 # 标记未完成 + +# 删除任务 +devkit-cli todo delete 1 # 删除(会确认) +devkit-cli todo delete 1 --yes # 删除(跳过确认) + +# 清除所有已完成任务 +devkit-cli todo clear +``` + +**优先级选项:** +- `high` - 高优先级(🔴) +- `medium` - 中优先级(🟡,默认) +- `low` - 低优先级(🟢) + +**数据存储:** +- 任务数据保存在用户目录下的 `.devkit_todo.json` +- 可直接编辑或备份该文件 + +--- + +## 💡 通用功能 + +### 版本信息 +```bash +devkit-cli --version +devkit-cli -v +``` + +### 帮助信息 +```bash +devkit-cli --help +devkit-cli info --help +devkit-cli file --help +devkit-cli todo --help +``` + +--- + +## 🌟 特性亮点 + +- **彩色美化输出:** 使用 Rich 库提供精美的表格和彩色输出 +- **参数校验:** 自动校验输入参数,提供友好错误提示 +- **进度显示:** 文件重命名时显示实时进度条 +- **交互式确认:** 危险操作前提供确认提示 +- **零配置:** 开箱即用,无需额外配置文件 +- **跨平台:** 支持 Windows、macOS、Linux + +--- + +## 📦 项目结构 + +``` +devkit-cli/ +├── devkit_cli/ +│ ├── __init__.py # 包初始化 +│ ├── cli.py # 主入口和命令调度 +│ ├── info.py # 系统信息模块 +│ ├── file_rename.py # 文件重命名模块 +│ └── todo.py # 任务管理模块 +├── devkit.py # 直接运行入口 +├── pyproject.toml # 项目配置 +└── README.md # 使用说明 +``` + +--- + +## 📝 许可证 + +MIT diff --git a/devkit.py b/devkit.py new file mode 100644 index 0000000..870bcd6 --- /dev/null +++ b/devkit.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +""" +devkit-cli 入口文件 +可以直接运行此文件:python devkit.py +""" + +from devkit_cli.cli import app + +if __name__ == "__main__": + app() diff --git a/devkit_cli.egg-info/PKG-INFO b/devkit_cli.egg-info/PKG-INFO new file mode 100644 index 0000000..143eef2 --- /dev/null +++ b/devkit_cli.egg-info/PKG-INFO @@ -0,0 +1,27 @@ +Metadata-Version: 2.4 +Name: devkit-cli +Version: 1.0.0 +Summary: 开发者工具集命令行工具 - 系统信息、文件重命名、任务管理 +Author-email: Developer +Project-URL: Homepage, https://github.com/example/devkit-cli +Project-URL: Bug Tracker, https://github.com/example/devkit-cli/issues +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Operating System :: OS Independent +Classifier: Topic :: Utilities +Classifier: Topic :: System :: Shells +Classifier: Intended Audience :: Developers +Classifier: Environment :: Console +Requires-Python: >=3.8 +Description-Content-Type: text/markdown +Requires-Dist: typer>=0.9.0 +Requires-Dist: rich>=13.0.0 +Requires-Dist: psutil>=5.9.0 + +# devkit-cli diff --git a/devkit_cli.egg-info/SOURCES.txt b/devkit_cli.egg-info/SOURCES.txt new file mode 100644 index 0000000..0dbe489 --- /dev/null +++ b/devkit_cli.egg-info/SOURCES.txt @@ -0,0 +1,13 @@ +README.md +pyproject.toml +devkit_cli/__init__.py +devkit_cli/cli.py +devkit_cli/file_rename.py +devkit_cli/info.py +devkit_cli/todo.py +devkit_cli.egg-info/PKG-INFO +devkit_cli.egg-info/SOURCES.txt +devkit_cli.egg-info/dependency_links.txt +devkit_cli.egg-info/entry_points.txt +devkit_cli.egg-info/requires.txt +devkit_cli.egg-info/top_level.txt \ No newline at end of file diff --git a/devkit_cli.egg-info/dependency_links.txt b/devkit_cli.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/devkit_cli.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/devkit_cli.egg-info/entry_points.txt b/devkit_cli.egg-info/entry_points.txt new file mode 100644 index 0000000..73b9ce9 --- /dev/null +++ b/devkit_cli.egg-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +devkit = devkit_cli.cli:app +devkit-cli = devkit_cli.cli:app diff --git a/devkit_cli.egg-info/requires.txt b/devkit_cli.egg-info/requires.txt new file mode 100644 index 0000000..88182e2 --- /dev/null +++ b/devkit_cli.egg-info/requires.txt @@ -0,0 +1,3 @@ +typer>=0.9.0 +rich>=13.0.0 +psutil>=5.9.0 diff --git a/devkit_cli.egg-info/top_level.txt b/devkit_cli.egg-info/top_level.txt new file mode 100644 index 0000000..59e8581 --- /dev/null +++ b/devkit_cli.egg-info/top_level.txt @@ -0,0 +1 @@ +devkit_cli diff --git a/devkit_cli/__init__.py b/devkit_cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/devkit_cli/__pycache__/__init__.cpython-314.pyc b/devkit_cli/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..06369876c1b6dad1166a930f717722f8e570e966 GIT binary patch literal 162 zcmdPqmSty_=z{nVW;}=)AD@|*SrQ+wS5SG2!zMRB br8Fniu80+A9>~UG5aSawBO_xGGmr%Uh8HLf literal 0 HcmV?d00001 diff --git a/devkit_cli/__pycache__/cli.cpython-314.pyc b/devkit_cli/__pycache__/cli.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a9e98cdc5165e15f734612941a20cee00174a7f6 GIT binary patch literal 3265 zcma)8U2GKB6~41GyE{AkgE82Ef8sGAvp`z~8jZ$HIy6lee#N(aS z-5Ce`v~CMF24tc{Eh4Url|)La)KsceNDLv5edvSRTCF>RKBW7DpR$%i^VpsVN%&{2M3bX0O0x?K(q9mPo`P14LrF8H0h* z)9vYE4Ymz=Y&X~CHN0)4pKT`x-Tj8IpXrH|l~u=fB5{&e@=1P)Kg2nFgCAg|KzE=^ zFa*%ZNJ4k8D`bRhI@lfViWm{l&iAxc^^~c8K?+G>DRPK)I8T)eNt>j)4ah~L%~E}> zoV3NLleTUnQo}Z))ftNJDjet(4LxbAnE5b2^ZVSwLVjwlI6qgsc2PJLWi0!e5XaKrWrqOJqEe<8jRxqP9`{vYBLlN~*FUU|lKZ`?6W|OmL{HtSQarg_f5rjUQ zxgbJtpNP+x_r=cro%<7r9~VOe?*?Le?d#BG$tz^QbNH8l2H>@jA2CCW#Pq?6(07md7^}Z^sTvB$H)XwyID`$b3(UzVwk)!PJ-WHEA&>tVHU{gdLm9~Uo8!XY`pA3JdRjIJfcvs6)1{ndz^ z-dDpGMsY?*#->b1+-BHy&?C487}gB`tqfkrhost+sj>#8xNn+9H-k$5UO7Kp{% z-;H@lp`0@)nnbri2cg{xg9Cdzv^HYxZZ9qX z*dUr;lC8`MX4UQ=1R~R|ms(3KvU1^uw~|)}N?x3l1mY7`gGkyjP1O2>zl4ul+X+ZS zJofA{W4p4i2XDa>P4d)W`Pq`SsDHL3w_%;$4NLkqEa|UGRw@{8-Ur8pja;$~ zmA2fy`^C53hfAFM&Q$r$o_IlHz&l@WL5 z=dTndudU2|RD63LieO>xot?rd^IPDAD)72VW> zHOx&uq9{JIkeT~y_F*8_?R!cwGdpTFVN|M{zPh+U3aNJzPEyX8XWV2%@n+Lz=%Acp zKBM@k${>1oySa}WiK{6Z1t6DcWoKAcw1|vre`_zbtRBoz+=*RnQPHXaKoE`_x^7`qnG z7Gz?Yr|r1na~?}}AkZ#^sw{s%Y(t$Y!G9axIHsn2mwp3wUeTO`be<;ay08-_5~+~e?Gh%Xjl$(EN|Jq+|c|*!0Y#9*~fK+X?{rBACjh$ zk1*Uscsx8gbp4gZJ>A*x5_x%wCyL3R^cdW`mD8sQ#r8@pva%##?Uo zW?TNo?18nmU9;K^ZJ7^E4qrdGxaY{Hy^B4`FIZ2T?>}~uU1N!7Z^>il+YZ8oCdZc8 R9slI~7mu&9gln?B{~wCYjXD4T literal 0 HcmV?d00001 diff --git a/devkit_cli/__pycache__/file_rename.cpython-314.pyc b/devkit_cli/__pycache__/file_rename.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e90cd0a3cd80241e8bf24a0163d05bcc535469b2 GIT binary patch literal 13234 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 EzdNbWs{jB1 literal 0 HcmV?d00001 diff --git a/devkit_cli/__pycache__/info.cpython-314.pyc b/devkit_cli/__pycache__/info.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3ac24e8dd40251cb828d205da80b5fa0ee95403e GIT binary patch literal 9435 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 literal 0 HcmV?d00001 diff --git a/devkit_cli/__pycache__/todo.cpython-314.pyc b/devkit_cli/__pycache__/todo.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7374a3ed971aa2b52629bb9362e95cff9fa8451d GIT binary patch literal 24537 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 None: + """显示版本信息""" + if value: + console.print(f"[bold green]devkit-cli[/bold green] version: [cyan]{__version__}[/cyan]") + raise typer.Exit() + +@app.callback(invoke_without_command=True) +def main( + ctx: typer.Context, + version: Optional[bool] = typer.Option( + None, "--version", "-v", callback=version_callback, is_eager=True, help="显示版本信息" + ), +) -> None: + """ + [bold green]devkit-cli[/bold green] - 开发者工具集命令行工具 + + 使用示例: + $ devkit-cli info + $ devkit-cli file rename --prefix test_ + $ devkit-cli todo add "完成项目文档" + """ + if ctx.invoked_subcommand is None: + console.print("[bold yellow]⚠️ 请指定一个子命令[/bold yellow]") + console.print("使用 --help 查看帮助信息") + console.print("\n可用子命令:") + console.print(" [cyan]info[/cyan] 📊 查看本机系统信息") + console.print(" [cyan]file[/cyan] 📁 批量文件重命名") + console.print(" [cyan]todo[/cyan] ✅ 本地任务管理") + +if __name__ == "__main__": + app() diff --git a/devkit_cli/file_rename.py b/devkit_cli/file_rename.py new file mode 100644 index 0000000..f589c0e --- /dev/null +++ b/devkit_cli/file_rename.py @@ -0,0 +1,222 @@ +""" +文件操作模块 - 批量文件重命名,支持自定义前缀、自动编号 +""" + +import os +import typer +from typing import Optional, List +from rich.console import Console +from rich.table import Table +from rich import box +from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn +from rich.prompt import Confirm, Prompt + +# 初始化控制台 +console = Console() +file_app = typer.Typer(help="📁 批量文件重命名") + +def is_valid_directory(path: str) -> bool: + """检查路径是否为有效目录""" + return os.path.isdir(path) + +def get_file_list(directory: str, extensions: Optional[List[str]] = None) -> List[str]: + """ + 获取目录中的文件列表 + + Args: + directory: 目标目录 + extensions: 文件扩展名过滤列表(如 ['.txt', '.jpg']) + + Returns: + 文件名列表(仅文件,排除子目录) + """ + files = [] + for f in os.listdir(directory): + file_path = os.path.join(directory, f) + if os.path.isfile(file_path): + if extensions: + file_ext = os.path.splitext(f)[1].lower() + if file_ext in extensions: + files.append(f) + else: + files.append(f) + return files + +@file_app.command("rename") +def batch_rename( + directory: str = typer.Argument(".", help="目标目录路径,默认为当前目录"), + prefix: str = typer.Option(..., "--prefix", "-p", help="文件名前缀"), + start_number: int = typer.Option(1, "--start", "-s", min=1, help="起始编号,默认为 1"), + digits: int = typer.Option(3, "--digits", "-d", min=1, max=10, help="编号位数,默认为 3"), + extension: Optional[List[str]] = typer.Option( + None, "--ext", "-e", help="指定文件扩展名(可多次使用,如 -e .txt -e .jpg)" + ), + recursive: bool = typer.Option(False, "--recursive", "-r", help="递归处理子目录(暂不支持)"), + dry_run: bool = typer.Option(False, "--dry-run", "-n", help="预览重命名结果,不实际执行"), + yes: bool = typer.Option(False, "--yes", "-y", help="跳过确认提示"), +) -> None: + """ + 📝 批量文件重命名,支持自定义前缀、自动编号 + + 示例: + $ devkit-cli file rename --prefix vacation_ --digits 4 --ext .jpg + $ devkit-cli file rename ./photos --prefix img_ --start 100 + $ devkit-cli file rename --prefix test_ --dry-run + """ + # 验证目录 + if not is_valid_directory(directory): + console.print(f"[bold red]❌ 错误:目录 '{directory}' 不存在或无效[/bold red]") + raise typer.Exit(code=1) + + # 规范化扩展名(转换为小写) + if extension: + extension = [ext.lower() if ext.startswith(".") else f".{ext.lower()}" for ext in extension] + + # 获取文件列表 + files = get_file_list(directory, extension) + + if not files: + if extension: + console.print(f"[bold yellow]⚠️ 在目录 '{directory}' 中没有找到指定扩展名的文件[/bold yellow]") + else: + console.print(f"[bold yellow]⚠️ 在目录 '{directory}' 中没有找到任何文件[/bold yellow]") + raise typer.Exit(code=0) + + # 按名称排序 + files.sort() + + # 显示预览表格 + console.print(f"\n[bold magenta]📍 目录:[/bold magenta] {os.path.abspath(directory)}") + console.print(f"[bold magenta]📦 找到文件数:[/bold magenta] {len(files)}\n") + + preview_table = Table( + title="[bold blue]🔍 重命名预览[/bold blue]", + box=box.ROUNDED, + show_header=True, + header_style="bold cyan", + title_justify="left", + ) + preview_table.add_column("#", style="dim", width=4) + preview_table.add_column("原文件名", style="yellow") + preview_table.add_column("→", style="magenta", width=3) + preview_table.add_column("新文件名", style="green") + + rename_map = [] # 存储 (原文件名, 新文件名) 对 + for idx, filename in enumerate(files, start=start_number): + file_name, file_ext = os.path.splitext(filename) + new_filename = f"{prefix}{str(idx).zfill(digits)}{file_ext}" + rename_map.append((filename, new_filename)) + preview_table.add_row(str(idx - start_number + 1), filename, "→", new_filename) + + console.print(preview_table) + + # 如果是预览模式,在此结束 + if dry_run: + console.print("\n[bold cyan]ℹ️ 预览模式,未执行实际重命名操作[/bold cyan]\n") + return + + # 确认操作 + if not yes: + confirm = Confirm.ask( + f"\n[bold yellow]⚠️ 即将重命名 {len(files)} 个文件,是否继续?[/bold yellow]", + default=False, + ) + if not confirm: + console.print("[bold cyan]ℹ️ 操作已取消[/bold cyan]") + raise typer.Exit(code=0) + + # 执行重命名 + success_count = 0 + error_count = 0 + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + BarColumn(), + TaskProgressColumn(), + console=console, + ) as progress: + task = progress.add_task("[cyan]正在重命名文件...", total=len(rename_map)) + + for old_name, new_name in rename_map: + old_path = os.path.join(directory, old_name) + new_path = os.path.join(directory, new_name) + + try: + # 检查新文件名是否已存在 + if os.path.exists(new_path): + console.print(f"[bold red]❌ 冲突:[/bold red] '{new_name}' 已存在,跳过 '{old_name}'") + error_count += 1 + else: + os.rename(old_path, new_path) + success_count += 1 + except Exception as e: + console.print(f"[bold red]❌ 重命名失败 '{old_name}': {str(e)}[/bold red]") + error_count += 1 + + progress.advance(task) + + # 显示结果摘要 + console.print("\n[bold magenta]════════════════════════════════════════[/bold magenta]") + console.print(f"[bold green]✅ 成功:[/bold green] {success_count} 个文件") + console.print(f"[bold red]❌ 失败:[/bold red] {error_count} 个文件") + console.print("[bold magenta]════════════════════════════════════════[/bold magenta]\n") + +@file_app.command("list") +def list_files( + directory: str = typer.Argument(".", help="目标目录路径,默认为当前目录"), + extension: Optional[List[str]] = typer.Option( + None, "--ext", "-e", help="指定文件扩展名过滤" + ), +) -> None: + """ + 📋 列出目录中的文件,用于预览筛选结果 + + 示例: + $ devkit-cli file list + $ devkit-cli file list ./photos --ext .jpg + """ + if not is_valid_directory(directory): + console.print(f"[bold red]❌ 错误:目录 '{directory}' 不存在或无效[/bold red]") + raise typer.Exit(code=1) + + if extension: + extension = [ext.lower() if ext.startswith(".") else f".{ext.lower()}" for ext in extension] + + files = get_file_list(directory, extension) + files.sort() + + if not files: + console.print(f"[bold yellow]⚠️ 目录 '{directory}' 为空或没有匹配的文件[/bold yellow]") + return + + console.print(f"\n[bold magenta]📍 目录:[/bold magenta] {os.path.abspath(directory)}") + console.print(f"[bold magenta]📦 文件数:[/bold magenta] {len(files)}\n") + + file_table = Table(box=box.ROUNDED, show_header=True, header_style="bold cyan") + file_table.add_column("#", style="dim", width=4) + file_table.add_column("文件名", style="green") + file_table.add_column("大小", style="yellow", justify="right") + + for idx, filename in enumerate(files, 1): + file_path = os.path.join(directory, filename) + size = os.path.getsize(file_path) + # 格式化大小 + if size < 1024: + size_str = f"{size} B" + elif size < 1024 * 1024: + size_str = f"{size / 1024:.1f} KB" + elif size < 1024 * 1024 * 1024: + size_str = f"{size / (1024 * 1024):.1f} MB" + else: + size_str = f"{size / (1024 * 1024 * 1024):.1f} GB" + + file_table.add_row(str(idx), filename, size_str) + + console.print(file_table) + console.print() + +@file_app.callback() +def file_callback() -> None: + """📁 批量文件重命名,支持自定义前缀、自动编号""" + pass diff --git a/devkit_cli/info.py b/devkit_cli/info.py new file mode 100644 index 0000000..931f9ee --- /dev/null +++ b/devkit_cli/info.py @@ -0,0 +1,191 @@ +""" +系统信息模块 - 查看本机系统信息(CPU、内存、磁盘、系统版本) +""" + +import typer +import platform +import psutil +from rich.console import Console +from rich.table import Table +from rich import box + +# 初始化控制台 +console = Console() +info_app = typer.Typer(help="📊 查看本机系统信息") + +def get_size(bytes: int, suffix: str = "B") -> str: + """ + 转换字节数为易读格式 + + Args: + bytes: 字节数 + suffix: 单位后缀 + + Returns: + 格式化后的大小字符串 + """ + factor = 1024 + for unit in ["", "K", "M", "G", "T", "P"]: + if bytes < factor: + return f"{bytes:.2f} {unit}{suffix}" + bytes /= factor + return f"{bytes:.2f} Y{suffix}" + +@info_app.command("show") +def show_info() -> None: + """ + 📊 显示本机系统信息(CPU、内存、磁盘、系统版本) + """ + # 创建主标题 + console.print("\n[bold magenta]╔════════════════════════════════════════════════════════════╗[/bold magenta]") + console.print("[bold magenta]║[/bold magenta] [bold cyan]🖥️ 系统信息概览[/bold cyan] [bold magenta]║[/bold magenta]") + console.print("[bold magenta]╚════════════════════════════════════════════════════════════╝[/bold magenta]\n") + + # 1. 系统信息表格 + sys_table = Table( + title="[bold blue]📋 基本信息[/bold blue]", + box=box.ROUNDED, + show_header=True, + header_style="bold cyan", + title_justify="left", + ) + sys_table.add_column("项目", style="bold green", width=12) + sys_table.add_column("信息", style="yellow") + + uname = platform.uname() + sys_table.add_row("系统", uname.system) + sys_table.add_row("版本", platform.version()) + sys_table.add_row("发行版", platform.platform()) + sys_table.add_row("架构", uname.machine) + sys_table.add_row("主机名", uname.node) + console.print(sys_table) + console.print() + + # 2. CPU信息表格 + cpu_table = Table( + title="[bold blue]⚡ CPU信息[/bold blue]", + box=box.ROUNDED, + show_header=True, + header_style="bold cyan", + title_justify="left", + ) + cpu_table.add_column("项目", style="bold green", width=15) + cpu_table.add_column("信息", style="yellow") + + cpu_freq = psutil.cpu_freq() + cpu_table.add_row("物理核心数", str(psutil.cpu_count(logical=False))) + cpu_table.add_row("逻辑核心数", str(psutil.cpu_count(logical=True))) + cpu_table.add_row("当前频率", f"{cpu_freq.current:.2f} MHz") + cpu_table.add_row("CPU使用率", f"{psutil.cpu_percent(interval=0.5)}%") + + # 每个核心的使用率 + per_cpu = psutil.cpu_percent(percpu=True, interval=0.5) + cpu_usage_str = ", ".join([f"Core{i}: {u}%" for i, u in enumerate(per_cpu)]) + cpu_table.add_row("各核心使用率", cpu_usage_str) + console.print(cpu_table) + console.print() + + # 3. 内存信息表格 + mem_table = Table( + title="[bold blue]🧠 内存信息[/bold blue]", + box=box.ROUNDED, + show_header=True, + header_style="bold cyan", + title_justify="left", + ) + mem_table.add_column("项目", style="bold green", width=12) + mem_table.add_column("总量", style="cyan") + mem_table.add_column("已用", style="yellow") + mem_table.add_column("可用", style="green") + mem_table.add_column("使用率", style="magenta") + + svmem = psutil.virtual_memory() + mem_table.add_row( + "物理内存", + get_size(svmem.total), + get_size(svmem.used), + get_size(svmem.available), + f"{svmem.percent}%", + ) + + # 交换内存 + swap = psutil.swap_memory() + mem_table.add_row( + "交换内存", + get_size(swap.total), + get_size(swap.used), + get_size(swap.free), + f"{swap.percent}%", + ) + console.print(mem_table) + console.print() + + # 4. 磁盘信息表格 + disk_table = Table( + title="[bold blue]💾 磁盘信息[/bold blue]", + box=box.ROUNDED, + show_header=True, + header_style="bold cyan", + title_justify="left", + ) + disk_table.add_column("设备", style="bold green", width=10) + disk_table.add_column("挂载点", style="cyan") + disk_table.add_column("文件系统", style="yellow") + disk_table.add_column("总量", style="blue") + disk_table.add_column("已用", style="magenta") + disk_table.add_column("可用", style="green") + disk_table.add_column("使用率", style="red") + + partitions = psutil.disk_partitions() + for partition in partitions: + try: + usage = psutil.disk_usage(partition.mountpoint) + disk_table.add_row( + partition.device, + partition.mountpoint, + partition.fstype, + get_size(usage.total), + get_size(usage.used), + get_size(usage.free), + f"{usage.percent}%", + ) + except PermissionError: + continue + console.print(disk_table) + console.print() + + # 5. 网络信息 + net_table = Table( + title="[bold blue]🌐 网络接口[/bold blue]", + box=box.ROUNDED, + show_header=True, + header_style="bold cyan", + title_justify="left", + ) + net_table.add_column("接口", style="bold green", width=10) + net_table.add_column("IP地址", style="cyan") + net_table.add_column("MAC地址", style="yellow") + + net_io = psutil.net_if_addrs() + for iface, addrs in net_io.items(): + if iface == "Loopback": + continue + ip = "" + mac = "" + for addr in addrs: + if addr.family == 2: # AF_INET + ip = addr.address + elif addr.family == -1: # AF_PACKET / MAC + mac = addr.address + if ip or mac: + net_table.add_row(iface, ip, mac) + console.print(net_table) + console.print() + + # 底部提示 + console.print("[dim]💡 提示:数据为实时采集,CPU使用率可能有波动[/dim]\n") + +@info_app.callback() +def info_callback() -> None: + """📊 查看本机系统信息(CPU、内存、磁盘、系统版本)""" + pass diff --git a/devkit_cli/todo.py b/devkit_cli/todo.py new file mode 100644 index 0000000..ceffdd0 --- /dev/null +++ b/devkit_cli/todo.py @@ -0,0 +1,397 @@ +""" +任务管理模块 - 本地任务管理,支持添加、查看、标记完成、删除 +""" + +import os +import json +import typer +from typing import Optional, List +from datetime import datetime +from rich.console import Console +from rich.table import Table +from rich import box +from rich.prompt import Prompt, Confirm, IntPrompt + +# 初始化控制台 +console = Console() +todo_app = typer.Typer(help="✅ 本地任务管理") + +# 数据存储文件路径 +TODO_FILE = os.path.join(os.path.expanduser("~"), ".devkit_todo.json") + +class TodoItem: + """任务项数据类""" + def __init__( + self, + id: int, + title: str, + description: str = "", + completed: bool = False, + created_at: str = None, + completed_at: str = None, + priority: str = "medium", + ): + self.id = id + self.title = title + self.description = description + self.completed = completed + self.created_at = created_at or datetime.now().isoformat() + self.completed_at = completed_at + self.priority = priority # high, medium, low + + def to_dict(self) -> dict: + """转换为字典用于JSON存储""" + return { + "id": self.id, + "title": self.title, + "description": self.description, + "completed": self.completed, + "created_at": self.created_at, + "completed_at": self.completed_at, + "priority": self.priority, + } + + @classmethod + def from_dict(cls, data: dict) -> "TodoItem": + """从字典创建任务项""" + return cls(**data) + +class TodoManager: + """任务管理器""" + def __init__(self): + self.items: List[TodoItem] = [] + self.next_id: int = 1 + self.load() + + def load(self) -> None: + """从文件加载任务数据""" + if os.path.exists(TODO_FILE): + try: + with open(TODO_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + self.items = [TodoItem.from_dict(item) for item in data.get("items", [])] + self.next_id = data.get("next_id", 1) + except json.JSONDecodeError: + console.print(f"[bold yellow]⚠️ 任务文件损坏,将创建新文件[/bold yellow]") + self.items = [] + self.next_id = 1 + except Exception as e: + console.print(f"[bold red]❌ 加载任务失败: {str(e)}[/bold red]") + else: + # 文件不存在,初始化空列表 + self.items = [] + self.next_id = 1 + + def save(self) -> None: + """保存任务数据到文件""" + try: + data = { + "next_id": self.next_id, + "items": [item.to_dict() for item in self.items], + } + with open(TODO_FILE, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + except Exception as e: + console.print(f"[bold red]❌ 保存任务失败: {str(e)}[/bold red]") + + def add(self, title: str, description: str = "", priority: str = "medium") -> TodoItem: + """添加新任务""" + item = TodoItem( + id=self.next_id, + title=title, + description=description, + priority=priority, + ) + self.items.append(item) + self.next_id += 1 + self.save() + return item + + def get(self, item_id: int) -> Optional[TodoItem]: + """根据ID获取任务""" + for item in self.items: + if item.id == item_id: + return item + return None + + def mark_complete(self, item_id: int) -> bool: + """标记任务完成""" + item = self.get(item_id) + if item: + item.completed = True + item.completed_at = datetime.now().isoformat() + self.save() + return True + return False + + def mark_incomplete(self, item_id: int) -> bool: + """标记任务未完成""" + item = self.get(item_id) + if item: + item.completed = False + item.completed_at = None + self.save() + return True + return False + + def delete(self, item_id: int) -> bool: + """删除任务""" + item = self.get(item_id) + if item: + self.items.remove(item) + self.save() + return True + return False + + def clear_completed(self) -> int: + """清除所有已完成的任务""" + count = len([item for item in self.items if item.completed]) + self.items = [item for item in self.items if not item.completed] + self.save() + return count + + def get_all(self, include_completed: bool = True) -> List[TodoItem]: + """获取所有任务""" + if include_completed: + return self.items + return [item for item in self.items if not item.completed] + +# 初始化任务管理器 +todo_manager = TodoManager() + +def get_priority_style(priority: str) -> str: + """获取优先级对应的样式""" + styles = { + "high": "[bold red]🔴 高[/bold red]", + "medium": "[bold yellow]🟡 中[/bold yellow]", + "low": "[bold green]🟢 低[/bold green]", + } + return styles.get(priority.lower(), f"⚪ {priority}") + +def format_datetime(iso_str: str) -> str: + """格式化ISO时间字符串""" + if not iso_str: + return "-" + try: + dt = datetime.fromisoformat(iso_str) + return dt.strftime("%Y-%m-%d %H:%M") + except: + return iso_str + +@todo_app.command("add") +def add_task( + title: str = typer.Argument(..., help="任务标题"), + description: str = typer.Option("", "--desc", "-d", help="任务描述"), + priority: str = typer.Option( + "medium", "--priority", "-p", help="优先级 (high, medium, low)" + ), +) -> None: + """ + ➕ 添加新任务 + + 示例: + $ devkit-cli todo add "完成项目文档" + $ devkit-cli todo add "代码审查" --desc "审查张三的PR" --priority high + """ + # 验证优先级 + priority = priority.lower() + if priority not in ["high", "medium", "low"]: + console.print(f"[bold red]❌ 无效的优先级 '{priority}',请使用: high, medium, low[/bold red]") + raise typer.Exit(code=1) + + item = todo_manager.add(title, description, priority) + + console.print(f"\n[bold green]✅ 任务已添加[/bold green]") + console.print(f"[cyan]ID:[/cyan] {item.id}") + console.print(f"[cyan]标题:[/cyan] {item.title}") + console.print(f"[cyan]描述:[/cyan] {item.description or '-'}") + console.print(f"[cyan]优先级:[/cyan] {get_priority_style(item.priority)}") + console.print() + +@todo_app.command("list") +def list_tasks( + all: bool = typer.Option(False, "--all", "-a", help="显示所有任务(包括已完成)"), + priority: Optional[str] = typer.Option( + None, "--priority", "-p", help="按优先级过滤 (high, medium, low)" + ), +) -> None: + """ + 📋 列出所有任务 + + 示例: + $ devkit-cli todo list + $ devkit-cli todo list --all + $ devkit-cli todo list --priority high + """ + items = todo_manager.get_all(include_completed=all) + + # 按优先级过滤 + if priority: + priority = priority.lower() + items = [item for item in items if item.priority == priority] + + if not items: + console.print("\n[bold yellow]📭 没有任务[/bold yellow]\n") + return + + # 统计信息 + total = len(items) + completed = len([item for item in items if item.completed]) + pending = total - completed + + console.print(f"\n[bold magenta]📊 统计:[/bold magenta] 共 {total} 个任务 " + f"([bold green]{completed} 已完成[/bold green], " + f"[bold yellow]{pending} 进行中[/bold yellow])\n") + + # 创建表格 + table = Table(box=box.ROUNDED, show_header=True, header_style="bold cyan") + table.add_column("ID", style="dim", width=4) + table.add_column("状态", style="bold", width=6) + table.add_column("优先级", width=8) + table.add_column("标题", style="green") + table.add_column("描述", style="yellow") + table.add_column("创建时间", style="cyan", width=16) + + for item in items: + status = "✅" if item.completed else "🔄" + if item.completed: + display_title = f"[strike]{item.title}[/strike]" + else: + display_title = item.title + + table.add_row( + str(item.id), + status, + get_priority_style(item.priority), + display_title, + item.description or "-", + format_datetime(item.created_at), + ) + + console.print(table) + console.print() + +@todo_app.command("show") +def show_task( + task_id: int = typer.Argument(..., help="任务ID"), +) -> None: + """ + 🔍 查看任务详情 + + 示例: + $ devkit-cli todo show 1 + """ + item = todo_manager.get(task_id) + if not item: + console.print(f"[bold red]❌ 未找到ID为 {task_id} 的任务[/bold red]") + raise typer.Exit(code=1) + + console.print(f"\n[bold magenta]════════════════════════════════════════[/bold magenta]") + console.print(f"[bold cyan]📋 任务详情[/bold cyan]") + console.print(f"[bold magenta]════════════════════════════════════════[/bold magenta]") + console.print(f"[cyan]ID:[/cyan] {item.id}") + console.print(f"[cyan]状态:[/cyan] {'✅ 已完成' if item.completed else '🔄 进行中'}") + console.print(f"[cyan]标题:[/cyan] {item.title}") + console.print(f"[cyan]描述:[/cyan] {item.description or '-'}") + console.print(f"[cyan]优先级:[/cyan] {get_priority_style(item.priority)}") + console.print(f"[cyan]创建时间:[/cyan] {format_datetime(item.created_at)}") + if item.completed and item.completed_at: + console.print(f"[cyan]完成时间:[/cyan] {format_datetime(item.completed_at)}") + console.print("[bold magenta]════════════════════════════════════════[/bold magenta]\n") + +@todo_app.command("done") +def mark_done( + task_id: int = typer.Argument(..., help="任务ID"), +) -> None: + """ + ✅ 标记任务为完成 + + 示例: + $ devkit-cli todo done 1 + """ + if todo_manager.mark_complete(task_id): + console.print(f"[bold green]✅ 任务 {task_id} 已标记为完成[/bold green]\n") + else: + console.print(f"[bold red]❌ 未找到ID为 {task_id} 的任务[/bold red]") + raise typer.Exit(code=1) + +@todo_app.command("undone") +def mark_undone( + task_id: int = typer.Argument(..., help="任务ID"), +) -> None: + """ + 🔄 标记任务为未完成 + + 示例: + $ devkit-cli todo undone 1 + """ + if todo_manager.mark_incomplete(task_id): + console.print(f"[bold yellow]🔄 任务 {task_id} 已标记为未完成[/bold yellow]\n") + else: + console.print(f"[bold red]❌ 未找到ID为 {task_id} 的任务[/bold red]") + raise typer.Exit(code=1) + +@todo_app.command("delete") +def delete_task( + task_id: int = typer.Argument(..., help="任务ID"), + yes: bool = typer.Option(False, "--yes", "-y", help="跳过确认提示"), +) -> None: + """ + 🗑️ 删除任务 + + 示例: + $ devkit-cli todo delete 1 + $ devkit-cli todo delete 1 --yes + """ + item = todo_manager.get(task_id) + if not item: + console.print(f"[bold red]❌ 未找到ID为 {task_id} 的任务[/bold red]") + raise typer.Exit(code=1) + + if not yes: + confirm = Confirm.ask( + f"[bold yellow]⚠️ 确定要删除任务 {task_id}: '{item.title}' 吗?[/bold yellow]", + default=False, + ) + if not confirm: + console.print("[bold cyan]ℹ️ 操作已取消[/bold cyan]\n") + return + + if todo_manager.delete(task_id): + console.print(f"[bold green]🗑️ 任务 {task_id} 已删除[/bold green]\n") + else: + console.print(f"[bold red]❌ 删除任务 {task_id} 失败[/bold red]") + raise typer.Exit(code=1) + +@todo_app.command("clear") +def clear_completed( + yes: bool = typer.Option(False, "--yes", "-y", help="跳过确认提示"), +) -> None: + """ + 🧹 清除所有已完成的任务 + + 示例: + $ devkit-cli todo clear + $ devkit-cli todo clear --yes + """ + completed_items = [item for item in todo_manager.items if item.completed] + if not completed_items: + console.print("\n[bold yellow]📭 没有已完成的任务需要清除[/bold yellow]\n") + return + + if not yes: + confirm = Confirm.ask( + f"[bold yellow]⚠️ 确定要清除 {len(completed_items)} 个已完成的任务吗?[/bold yellow]", + default=False, + ) + if not confirm: + console.print("[bold cyan]ℹ️ 操作已取消[/bold cyan]\n") + return + + count = todo_manager.clear_completed() + console.print(f"[bold green]🧹 已清除 {count} 个已完成的任务[/bold green]\n") + +@todo_app.callback() +def todo_callback() -> None: + """✅ 本地任务管理,支持添加、查看、标记完成、删除""" + pass diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ca0baa9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,42 @@ +[project] +name = "devkit-cli" +version = "1.0.0" +description = "开发者工具集命令行工具 - 系统信息、文件重命名、任务管理" +authors = [ + { name="Developer", email="dev@example.com" } +] +readme = "README.md" +license = { file="LICENSE" } +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Operating System :: OS Independent", + "Topic :: Utilities", + "Topic :: System :: Shells", + "Intended Audience :: Developers", + "Environment :: Console", +] + +dependencies = [ + "typer>=0.9.0", + "rich>=13.0.0", + "psutil>=5.9.0", +] + +[project.scripts] +devkit-cli = "devkit_cli.cli:app" +devkit = "devkit_cli.cli:app" + +[project.urls] +Homepage = "https://github.com/example/devkit-cli" +"Bug Tracker" = "https://github.com/example/devkit-cli/issues" + +[tool.setuptools] +packages = ["devkit_cli"]