diff --git a/README.md b/README.md index 9770379..f623f5b 100644 --- a/README.md +++ b/README.md @@ -1 +1,249 @@ -# devkit-cli \ No newline at end of file +# devkit-cli + +开发者工具包命令行工具 - 一个功能丰富的 CLI 工具,提供系统信息查看、文件批量处理和任务管理功能。 + +## 技术栈 + +- **Python** - 核心编程语言 +- **Typer** - 现代化的 CLI 框架 +- **Rich** - 终端美化与表格展示 +- **psutil** - 系统信息获取 + +## 安装 + +```bash +# 克隆或下载项目后,安装依赖 +pip install -r requirements.txt +``` + +依赖要求: +- typer>=0.9.0 +- rich>=13.0.0 +- psutil>=5.9.0 + +## 快速开始 + +```bash +# 查看帮助 +python devkit.py --help + +# 查看版本 +python devkit.py --version +``` + +## 功能命令 + +### 1. info - 系统信息查看 + +查看本机系统信息,包括操作系统、CPU、内存、磁盘等。 + +```bash +# 查看系统信息(彩色表格) +python devkit.py info + +# JSON 格式输出 +python devkit.py info --json +``` + +**输出内容:** +- 系统信息:操作系统、版本、处理器、主机名、Python版本 +- CPU 信息:物理/逻辑核心数、频率、使用率 +- 内存信息:总内存、可用内存、已用内存、使用率 +- 磁盘信息:各分区容量、已用、可用、使用率 + +### 2. file - 文件批量处理 + +#### file rename - 批量重命名文件 + +```bash +# 基本用法 +python devkit.py file rename <目录路径> --prefix <前缀> --start <起始编号> + +# 示例:添加前缀并自动编号 +python devkit.py file rename ./photos --prefix "vacation_" --start 1 + +# 预览模式(不实际执行) +python devkit.py file rename ./docs --pattern "*.txt" --prefix "doc_" --dry-run + +# 完整参数 +python devkit.py file rename ./files \ + --prefix "file_" # 文件名前缀 \ + --start 1 # 起始编号 \ + --padding 3 # 编号位数(补零) \ + --pattern "*.jpg" # 文件匹配模式(glob) \ + --recursive # 递归处理子目录 \ + --lowercase # 转换为小写 \ + --uppercase # 转换为大写 \ + --dry-run # 预览模式 +``` + +#### file list - 列出目录文件 + +```bash +# 列出当前目录文件 +python devkit.py file list + +# 列出指定目录 +python devkit.py file list ./documents + +# 递归列出 +python devkit.py file list ./projects --recursive + +# 按模式筛选 +python devkit.py file list ./logs --pattern "*.log" +``` + +### 3. todo - 本地任务管理 + +任务数据存储在 `~/.devkit/todos.json` + +#### todo add - 添加任务 + +```bash +# 添加简单任务 +python devkit.py todo add "完成任务报告" + +# 添加带优先级和标签的任务 +python devkit.py todo add "修复Bug" --priority high --tag work + +# 优先级选项:high(高)、medium(中)、low(低) +``` + +#### todo list - 查看任务列表 + +```bash +# 查看未完成任务 +python devkit.py todo list + +# 查看所有任务(包括已完成) +python devkit.py todo list --all + +# 按优先级筛选 +python devkit.py todo list --priority high + +# 按标签筛选 +python devkit.py todo list --tag work +``` + +#### todo done - 标记任务完成 + +```bash +# 标记 ID 为 1 的任务为已完成 +python devkit.py todo done 1 +``` + +#### todo undo - 取消完成状态 + +```bash +# 将 ID 为 1 的任务重置为未完成 +python devkit.py todo undo 1 +``` + +#### todo delete - 删除任务 + +```bash +# 删除 ID 为 1 的任务(会确认) +python devkit.py todo delete 1 + +# 强制删除(不确认) +python devkit.py todo delete 1 --force +``` + +#### todo clear - 清空已完成任务 + +```bash +# 清空所有已完成任务(会确认) +python devkit.py todo clear + +# 强制清空(不确认) +python devkit.py todo clear --force +``` + +## 使用示例 + +### 示例 1:查看系统信息 + +```bash +$ python devkit.py info + + 系统信息 +┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ 项目 ┃ 值 ┃ +┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ +│ 操作系统 │ Windows │ +│ 系统版本 │ 10.0.19045 │ +│ 处理器 │ Intel64 Family 6 Model 190 Stepping 0, GenuineIntel │ +│ Python版本 │ 3.14.3 │ +└─────────────────┴─────────────────────────────────────────────────────┘ +... +``` + +### 示例 2:批量重命名照片 + +```bash +# 1. 先预览 +$ python devkit.py file rename ./vacation --prefix "photo_" --start 1 --dry-run + + 重命名预览 +┏━━━━━━━━┳━━━━━━━━━━━━━━┳━━━┳━━━━━━━━━━━━━━┓ +┃ 序号 ┃ 原文件名 ┃ → ┃ 新文件名 ┃ +┡━━━━━━━━╇━━━━━━━━━━━━━━╇━━━╇━━━━━━━━━━━━━━┩ +│ 1 │ IMG_0001.jpg │ → │ photo_001.jpg│ +│ 2 │ IMG_0002.jpg │ → │ photo_002.jpg│ +└────────┴──────────────┴───┴──────────────┘ + +# 2. 确认后执行 +$ python devkit.py file rename ./vacation --prefix "photo_" --start 1 + +✓ 重命名完成! + 成功: 2 个 +``` + +### 示例 3:任务管理流程 + +```bash +# 添加任务 +$ python devkit.py todo add "完成CLI开发" --priority high --tag work +✓ 任务已添加 + ID: 1 + 标题: 完成CLI开发 + 优先级: high + 标签: work + +# 查看任务 +$ python devkit.py todo list +┏━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ +┃ ID ┃ 状态 ┃ 标题 ┃ 优先级 ┃ 标签 ┃ 创建时间 ┃ +┡━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ +│ 1 │ ○ │ 完成CLI开发 │ high │ work │ 2026-03-20 13:44 │ +└──────┴──────┴──────────────┴──────────┴────────────┴──────────────────┘ + +# 标记完成 +$ python devkit.py todo done 1 +✓ 任务已完成 + +# 查看所有任务 +$ python devkit.py todo list --all +总计: 1 | 待完成: 0 | 已完成: 1 +``` + +## 数据存储 + +- **任务数据**:`~/.devkit/todos.json` +- **日志文件**:`~/.devkit/devkit.log` + +## 特性 + +- ✅ 模块化清晰,单文件实现 +- ✅ 完整的类型提示 +- ✅ 详细的代码注释 +- ✅ 彩色表格输出 +- ✅ 日志记录 +- ✅ 参数校验 +- ✅ 自动生成的帮助文档 +- ✅ 预览模式(dry-run) +- ✅ 交互式确认 + +## 许可证 + +MIT License diff --git a/devkit.py b/devkit.py new file mode 100644 index 0000000..f70208d --- /dev/null +++ b/devkit.py @@ -0,0 +1,808 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +devkit-cli: 开发者工具包命令行工具 + +一个功能丰富的 CLI 工具,提供系统信息查看、文件批量处理和任务管理功能。 +技术栈:Python + Typer + Rich +""" + +import json +import os +import platform +import re +import shutil +import subprocess +import sys +from datetime import datetime +from pathlib import Path +from typing import Annotated, Optional + +import psutil +import typer +from rich.console import Console +from rich.logging import RichHandler +from rich.panel import Panel +from rich.table import Table +from rich.text import Text + +# 初始化 Rich 控制台 +console = Console() + +# 配置日志 +import logging + +logging.basicConfig( + level=logging.INFO, + format="%(message)s", + datefmt="[%X]", + handlers=[RichHandler(console=console, rich_tracebacks=True)], +) +logger = logging.getLogger("devkit") + +# 创建 Typer 应用 +app = typer.Typer( + name="devkit", + help="开发者工具包 CLI - 系统信息、文件管理、任务管理", + add_completion=False, + rich_markup_mode="rich", +) + +# 数据存储目录 +DATA_DIR = Path.home() / ".devkit" +DATA_DIR.mkdir(exist_ok=True) +TODO_FILE = DATA_DIR / "todos.json" +LOG_FILE = DATA_DIR / "devkit.log" + +# 文件处理子命令 +file_app = typer.Typer(help="文件批量处理工具") +app.add_typer(file_app, name="file") + +# 任务管理子命令 +todo_app = typer.Typer(help="本地任务管理") +app.add_typer(todo_app, name="todo") + + +# ============================================================================ +# 工具函数 +# ============================================================================ + +def format_bytes(bytes_value: int) -> str: + """将字节转换为人类可读的格式""" + for unit in ["B", "KB", "MB", "GB", "TB"]: + if bytes_value < 1024.0: + return f"{bytes_value:.2f} {unit}" + bytes_value /= 1024.0 + return f"{bytes_value:.2f} PB" + + +def get_system_info() -> dict: + """获取系统信息""" + info = { + "system": { + "操作系统": platform.system(), + "系统版本": platform.version(), + "发行版": platform.release(), + "机器类型": platform.machine(), + "处理器": platform.processor() or "Unknown", + "主机名": platform.node(), + "Python版本": platform.python_version(), + }, + "cpu": {}, + "memory": {}, + "disk": [], + } + + # CPU 信息 + try: + cpu_count = psutil.cpu_count(logical=False) or 0 + cpu_logical = psutil.cpu_count(logical=True) or 0 + cpu_freq = psutil.cpu_freq() + cpu_percent = psutil.cpu_percent(interval=0.1) + + info["cpu"] = { + "物理核心数": cpu_count, + "逻辑核心数": cpu_logical, + "当前频率": f"{cpu_freq.current:.2f} MHz" if cpu_freq else "N/A", + "使用率": f"{cpu_percent}%", + } + except Exception as e: + logger.warning(f"获取CPU信息失败: {e}") + info["cpu"] = {"错误": str(e)} + + # 内存信息 + try: + mem = psutil.virtual_memory() + info["memory"] = { + "总内存": format_bytes(mem.total), + "可用内存": format_bytes(mem.available), + "已用内存": format_bytes(mem.used), + "内存使用率": f"{mem.percent}%", + } + except Exception as e: + logger.warning(f"获取内存信息失败: {e}") + info["memory"] = {"错误": str(e)} + + # 磁盘信息 + try: + partitions = psutil.disk_partitions() + for partition in partitions: + try: + usage = psutil.disk_usage(partition.mountpoint) + info["disk"].append({ + "设备": partition.device, + "挂载点": partition.mountpoint, + "文件系统": partition.fstype, + "总容量": format_bytes(usage.total), + "已用": format_bytes(usage.used), + "可用": format_bytes(usage.free), + "使用率": f"{usage.percent}%", + }) + except PermissionError: + continue + except Exception as e: + logger.warning(f"获取磁盘信息失败: {e}") + + return info + + +def load_todos() -> list: + """加载任务列表""" + if not TODO_FILE.exists(): + return [] + try: + with open(TODO_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except (json.JSONDecodeError, IOError) as e: + logger.error(f"加载任务列表失败: {e}") + return [] + + +def save_todos(todos: list) -> None: + """保存任务列表""" + try: + with open(TODO_FILE, "w", encoding="utf-8") as f: + json.dump(todos, f, ensure_ascii=False, indent=2) + except IOError as e: + logger.error(f"保存任务列表失败: {e}") + raise typer.Exit(1) + + +def validate_filename(filename: str) -> bool: + """验证文件名是否合法""" + invalid_chars = '<>:"/\\|?*' + return not any(c in filename for c in invalid_chars) + + +# ============================================================================ +# info 子命令 +# ============================================================================ + +@app.command(name="info", help="查看本机系统信息") +def info_command( + json_output: Annotated[ + bool, + typer.Option("--json", "-j", help="以JSON格式输出") + ] = False, +): + """查看系统信息(CPU、内存、磁盘、系统版本)""" + system_info = get_system_info() + + if json_output: + console.print_json(json.dumps(system_info, ensure_ascii=False)) + return + + # 系统信息表格 + sys_table = Table( + title="[bold cyan]系统信息[/bold cyan]", + show_header=True, + header_style="bold magenta", + border_style="blue", + ) + sys_table.add_column("项目", style="green", width=15) + sys_table.add_column("值", style="white") + + for key, value in system_info["system"].items(): + sys_table.add_row(key, str(value)) + + console.print(sys_table) + console.print() + + # CPU 信息表格 + cpu_table = Table( + title="[bold cyan]CPU 信息[/bold cyan]", + show_header=True, + header_style="bold magenta", + border_style="blue", + ) + cpu_table.add_column("项目", style="green", width=15) + cpu_table.add_column("值", style="white") + + for key, value in system_info["cpu"].items(): + cpu_table.add_row(key, str(value)) + + console.print(cpu_table) + console.print() + + # 内存信息表格 + mem_table = Table( + title="[bold cyan]内存信息[/bold cyan]", + show_header=True, + header_style="bold magenta", + border_style="blue", + ) + mem_table.add_column("项目", style="green", width=15) + mem_table.add_column("值", style="white") + + for key, value in system_info["memory"].items(): + mem_table.add_row(key, str(value)) + + console.print(mem_table) + console.print() + + # 磁盘信息表格 + if system_info["disk"]: + disk_table = Table( + title="[bold cyan]磁盘信息[/bold cyan]", + show_header=True, + header_style="bold magenta", + border_style="blue", + ) + disk_table.add_column("设备", style="green") + disk_table.add_column("挂载点", style="cyan") + disk_table.add_column("文件系统", style="yellow") + disk_table.add_column("总容量", style="white") + disk_table.add_column("已用", style="white") + disk_table.add_column("可用", style="white") + disk_table.add_column("使用率", style="red") + + for disk in system_info["disk"]: + disk_table.add_row( + disk["设备"], + disk["挂载点"], + disk["文件系统"], + disk["总容量"], + disk["已用"], + disk["可用"], + disk["使用率"], + ) + + console.print(disk_table) + + logger.info("系统信息查看完成") + + +# ============================================================================ +# file 子命令 +# ============================================================================ + +@file_app.command(name="rename", help="批量重命名文件") +def file_rename( + directory: Annotated[ + Path, + typer.Argument(help="目标目录路径") + ], + prefix: Annotated[ + str, + typer.Option("--prefix", "-p", help="文件名前缀") + ] = "", + start_number: Annotated[ + int, + typer.Option("--start", "-s", help="起始编号") + ] = 1, + padding: Annotated[ + int, + typer.Option("--padding", "-n", help="编号位数补零") + ] = 3, + pattern: Annotated[ + str, + typer.Option("--pattern", "-f", help="文件匹配模式 (glob)") + ] = "*", + recursive: Annotated[ + bool, + typer.Option("--recursive", "-r", help="递归处理子目录") + ] = False, + dry_run: Annotated[ + bool, + typer.Option("--dry-run", "-d", help="预览模式,不实际执行") + ] = False, + lowercase: Annotated[ + bool, + typer.Option("--lowercase", "-l", help="转换为小写") + ] = False, + uppercase: Annotated[ + bool, + typer.Option("--uppercase", "-u", help="转换为大写") + ] = False, +): + """ + 批量重命名文件,支持自定义前缀和自动编号 + + 示例: + devkit file rename ./photos --prefix "vacation_" --start 1 + devkit file rename ./docs --pattern "*.txt" --prefix "doc_" -d + """ + # 验证目录 + if not directory.exists(): + console.print(f"[red]错误: 目录不存在: {directory}[/red]") + raise typer.Exit(1) + + if not directory.is_dir(): + console.print(f"[red]错误: 不是有效的目录: {directory}[/red]") + raise typer.Exit(1) + + # 验证前缀 + if prefix and not validate_filename(prefix): + console.print(f"[red]错误: 前缀包含非法字符[/red]") + raise typer.Exit(1) + + # 获取文件列表 + if recursive: + files = list(directory.rglob(pattern)) + else: + files = list(directory.glob(pattern)) + + # 只保留文件(排除目录) + files = [f for f in files if f.is_file()] + + # 按文件名排序 + files.sort() + + if not files: + console.print(f"[yellow]警告: 未找到匹配的文件: {pattern}[/yellow]") + raise typer.Exit(0) + + # 显示预览 + console.print(f"\n[bold cyan]找到 {len(files)} 个文件[/bold cyan]") + console.print(f"[dim]目录: {directory.absolute()}[/dim]") + console.print(f"[dim]模式: {pattern}[/dim]") + if dry_run: + console.print("[yellow]【预览模式】不会实际修改文件[/yellow]\n") + else: + console.print() + + # 重命名表格 + rename_table = Table( + title="[bold cyan]重命名预览[/bold cyan]", + show_header=True, + header_style="bold magenta", + border_style="blue", + ) + rename_table.add_column("序号", style="dim", width=6) + rename_table.add_column("原文件名", style="red") + rename_table.add_column("→", style="dim", justify="center") + rename_table.add_column("新文件名", style="green") + + # 生成重命名计划 + rename_plan = [] + counter = start_number + + for old_file in files: + # 获取文件扩展名 + suffix = old_file.suffix + stem = old_file.stem + + # 处理大小写转换 + if lowercase: + stem = stem.lower() + suffix = suffix.lower() + elif uppercase: + stem = stem.upper() + suffix = suffix.upper() + + # 生成新文件名 + number_str = str(counter).zfill(padding) + if prefix: + new_name = f"{prefix}{number_str}{suffix}" + else: + new_name = f"{number_str}_{stem}{suffix}" + + new_file = old_file.parent / new_name + + # 检查文件名冲突 + if new_file.exists() and new_file != old_file: + console.print(f"[red]错误: 目标文件已存在: {new_name}[/red]") + raise typer.Exit(1) + + rename_plan.append((old_file, new_file)) + rename_table.add_row( + str(counter - start_number + 1), + old_file.name, + "→", + new_name, + ) + counter += 1 + + console.print(rename_table) + + if dry_run: + console.print("\n[green]预览完成,使用 --dry-run=false 执行实际重命名[/green]") + return + + # 确认执行 + if not typer.confirm("\n确认执行重命名操作?"): + console.print("[yellow]操作已取消[/yellow]") + raise typer.Exit(0) + + # 执行重命名 + success_count = 0 + error_count = 0 + + with console.status("[bold green]正在重命名文件...") as status: + for old_file, new_file in rename_plan: + try: + old_file.rename(new_file) + success_count += 1 + logger.debug(f"重命名: {old_file.name} -> {new_file.name}") + except Exception as e: + error_count += 1 + console.print(f"[red]错误: 无法重命名 {old_file.name}: {e}[/red]") + + # 显示结果 + console.print(f"\n[bold green]✓ 重命名完成![/bold green]") + console.print(f" 成功: {success_count} 个") + if error_count > 0: + console.print(f" [red]失败: {error_count} 个[/red]") + + logger.info(f"批量重命名完成: 成功 {success_count} 个, 失败 {error_count} 个") + + +@file_app.command(name="list", help="列出目录文件") +def file_list( + directory: Annotated[ + Path, + typer.Argument(help="目标目录路径") + ] = Path("."), + pattern: Annotated[ + str, + typer.Option("--pattern", "-p", help="文件匹配模式") + ] = "*", + recursive: Annotated[ + bool, + typer.Option("--recursive", "-r", help="递归列出") + ] = False, + show_size: Annotated[ + bool, + typer.Option("--size", "-s", help="显示文件大小") + ] = True, +): + """列出目录中的文件""" + if not directory.exists(): + console.print(f"[red]错误: 目录不存在: {directory}[/red]") + raise typer.Exit(1) + + if recursive: + files = list(directory.rglob(pattern)) + else: + files = list(directory.glob(pattern)) + + files.sort() + + table = Table( + title=f"[bold cyan]文件列表: {directory}[/bold cyan]", + show_header=True, + header_style="bold magenta", + border_style="blue", + ) + table.add_column("类型", width=4, justify="center") + table.add_column("文件名", style="green") + if show_size: + table.add_column("大小", style="cyan", justify="right") + table.add_column("修改时间", style="dim") + + for f in files: + icon = "📁" if f.is_dir() else "📄" + name = str(f.relative_to(directory)) + size = format_bytes(f.stat().st_size) if f.is_file() and show_size else "-" + mtime = datetime.fromtimestamp(f.stat().st_mtime).strftime("%Y-%m-%d %H:%M") + + if show_size: + table.add_row(icon, name, size, mtime) + else: + table.add_row(icon, name, mtime) + + console.print(table) + console.print(f"\n[dim]共 {len(files)} 个项目[/dim]") + + +# ============================================================================ +# todo 子命令 +# ============================================================================ + +@todo_app.command(name="add", help="添加新任务") +def todo_add( + title: Annotated[ + str, + typer.Argument(help="任务标题") + ], + priority: Annotated[ + str, + typer.Option("--priority", "-p", help="优先级 (high/medium/low)") + ] = "medium", + tag: Annotated[ + Optional[str], + typer.Option("--tag", "-t", help="任务标签") + ] = None, +): + """添加新任务到任务列表""" + # 验证优先级 + valid_priorities = ["high", "medium", "low"] + if priority not in valid_priorities: + console.print(f"[red]错误: 优先级必须是 {', '.join(valid_priorities)} 之一[/red]") + raise typer.Exit(1) + + # 加载现有任务 + todos = load_todos() + + # 创建新任务 + new_task = { + "id": len(todos) + 1, + "title": title, + "priority": priority, + "completed": False, + "created_at": datetime.now().isoformat(), + "completed_at": None, + "tag": tag, + } + + todos.append(new_task) + save_todos(todos) + + # 显示成功信息 + priority_colors = {"high": "red", "medium": "yellow", "low": "green"} + console.print(f"[green]✓ 任务已添加[/green]") + console.print(f" ID: {new_task['id']}") + console.print(f" 标题: {title}") + console.print(f" 优先级: [{priority_colors[priority]}]{priority}[/{priority_colors[priority]}]") + if tag: + console.print(f" 标签: [cyan]{tag}[/cyan]") + + logger.info(f"添加任务: {title}") + + +@todo_app.command(name="list", help="查看任务列表") +def todo_list( + show_all: Annotated[ + bool, + typer.Option("--all", "-a", help="显示所有任务(包括已完成)") + ] = False, + filter_priority: Annotated[ + Optional[str], + typer.Option("--priority", "-p", help="按优先级筛选") + ] = None, + filter_tag: Annotated[ + Optional[str], + typer.Option("--tag", "-t", help="按标签筛选") + ] = None, +): + """查看任务列表""" + todos = load_todos() + + if not todos: + console.print("[yellow]暂无任务,使用 'devkit todo add' 添加新任务[/yellow]") + return + + # 筛选任务 + filtered_todos = todos + + if not show_all: + filtered_todos = [t for t in filtered_todos if not t["completed"]] + + if filter_priority: + filtered_todos = [t for t in filtered_todos if t["priority"] == filter_priority] + + if filter_tag: + filtered_todos = [t for t in filtered_todos if t.get("tag") == filter_tag] + + if not filtered_todos: + console.print("[yellow]没有符合条件的任务[/yellow]") + return + + # 创建表格 + table = Table( + title="[bold cyan]任务列表[/bold cyan]", + show_header=True, + header_style="bold magenta", + border_style="blue", + ) + table.add_column("ID", style="dim", width=4, justify="center") + table.add_column("状态", width=4, justify="center") + table.add_column("标题", style="green", min_width=20) + table.add_column("优先级", width=8, justify="center") + table.add_column("标签", style="cyan", width=10) + table.add_column("创建时间", style="dim", width=16) + + priority_colors = {"high": "red", "medium": "yellow", "low": "green"} + + for task in filtered_todos: + status = "✓" if task["completed"] else "○" + status_style = "green" if task["completed"] else "white" + title_style = "strike dim" if task["completed"] else "" + priority = task["priority"] + priority_display = f"[{priority_colors[priority]}]{priority}[/{priority_colors[priority]}]" + tag = task.get("tag") or "-" + created = task["created_at"][:16].replace("T", " ") + + table.add_row( + str(task["id"]), + f"[{status_style}]{status}[/{status_style}]", + f"[{title_style}]{task['title']}[/{title_style}]" if title_style else task["title"], + priority_display, + tag, + created, + ) + + console.print(table) + + # 统计信息 + total = len(todos) + completed = len([t for t in todos if t["completed"]]) + pending = total - completed + + console.print(f"\n[dim]总计: {total} | 待完成: {pending} | 已完成: {completed}[/dim]") + + +@todo_app.command(name="done", help="标记任务完成") +def todo_done( + task_id: Annotated[ + int, + typer.Argument(help="任务ID") + ], +): + """标记任务为已完成""" + todos = load_todos() + + task = next((t for t in todos if t["id"] == task_id), None) + if not task: + console.print(f"[red]错误: 找不到ID为 {task_id} 的任务[/red]") + raise typer.Exit(1) + + if task["completed"]: + console.print(f"[yellow]任务已经是完成状态[/yellow]") + return + + task["completed"] = True + task["completed_at"] = datetime.now().isoformat() + save_todos(todos) + + console.print(f"[green]✓ 任务已完成[/green]") + console.print(f" ID: {task_id}") + console.print(f" 标题: {task['title']}") + + logger.info(f"完成任务: {task['title']}") + + +@todo_app.command(name="undo", help="取消任务完成状态") +def todo_undo( + task_id: Annotated[ + int, + typer.Argument(help="任务ID") + ], +): + """取消任务的完成状态""" + todos = load_todos() + + task = next((t for t in todos if t["id"] == task_id), None) + if not task: + console.print(f"[red]错误: 找不到ID为 {task_id} 的任务[/red]") + raise typer.Exit(1) + + if not task["completed"]: + console.print(f"[yellow]任务已经是未完成状态[/yellow]") + return + + task["completed"] = False + task["completed_at"] = None + save_todos(todos) + + console.print(f"[green]✓ 任务已重置为未完成[/green]") + console.print(f" ID: {task_id}") + console.print(f" 标题: {task['title']}") + + logger.info(f"重置任务: {task['title']}") + + +@todo_app.command(name="delete", help="删除任务") +def todo_delete( + task_id: Annotated[ + int, + typer.Argument(help="任务ID") + ], + force: Annotated[ + bool, + typer.Option("--force", "-f", help="强制删除,不确认") + ] = False, +): + """删除指定任务""" + todos = load_todos() + + task = next((t for t in todos if t["id"] == task_id), None) + if not task: + console.print(f"[red]错误: 找不到ID为 {task_id} 的任务[/red]") + raise typer.Exit(1) + + if not force: + confirm = typer.confirm(f"确定要删除任务 '{task['title']}' 吗?") + if not confirm: + console.print("[yellow]操作已取消[/yellow]") + raise typer.Exit(0) + + todos.remove(task) + + # 重新编号 + for i, t in enumerate(todos, 1): + t["id"] = i + + save_todos(todos) + + console.print(f"[green]✓ 任务已删除[/green]") + console.print(f" ID: {task_id}") + console.print(f" 标题: {task['title']}") + + logger.info(f"删除任务: {task['title']}") + + +@todo_app.command(name="clear", help="清空已完成任务") +def todo_clear( + force: Annotated[ + bool, + typer.Option("--force", "-f", help="强制清空,不确认") + ] = False, +): + """清空所有已完成的任务""" + todos = load_todos() + + completed_tasks = [t for t in todos if t["completed"]] + if not completed_tasks: + console.print("[yellow]没有已完成的任务[/yellow]") + return + + if not force: + confirm = typer.confirm(f"确定要清空 {len(completed_tasks)} 个已完成的任务吗?") + if not confirm: + console.print("[yellow]操作已取消[/yellow]") + raise typer.Exit(0) + + todos = [t for t in todos if not t["completed"]] + + # 重新编号 + for i, t in enumerate(todos, 1): + t["id"] = i + + save_todos(todos) + + console.print(f"[green]✓ 已清空 {len(completed_tasks)} 个完成任务[/green]") + + logger.info(f"清空已完成任务: {len(completed_tasks)} 个") + + +# ============================================================================ +# 主程序入口 +# ============================================================================ + +def version_callback(value: bool) -> None: + """版本信息回调函数""" + if value: + console.print("[bold cyan]devkit-cli[/bold cyan] v1.0.0") + console.print("[dim]作者: DevKit Team[/dim]") + console.print("[dim]技术栈: Python + Typer + Rich[/dim]") + raise typer.Exit() + + +@app.callback() +def main( + version: Annotated[ + bool, + typer.Option( + "--version", "-v", + help="显示版本信息", + is_eager=True, + callback=version_callback + ) + ] = False, +): + """开发者工具包 CLI - 系统信息、文件管理、任务管理""" + + +if __name__ == "__main__": + app() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..88182e2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +typer>=0.9.0 +rich>=13.0.0 +psutil>=5.9.0 diff --git a/test_files/doc_001.txt b/test_files/doc_001.txt new file mode 100644 index 0000000..8962b20 Binary files /dev/null and b/test_files/doc_001.txt differ diff --git a/test_files/doc_002.txt b/test_files/doc_002.txt new file mode 100644 index 0000000..76f46c3 Binary files /dev/null and b/test_files/doc_002.txt differ diff --git a/test_files/doc_003.txt b/test_files/doc_003.txt new file mode 100644 index 0000000..eb0ef6e Binary files /dev/null and b/test_files/doc_003.txt differ