From 7891be275dd5cc3a489d01de0a1263b22c847376 Mon Sep 17 00:00:00 2001 From: sitga <2389647927@qq.com> Date: Fri, 20 Mar 2026 14:11:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 187 ++++++- devkit_cli.egg-info/PKG-INFO | 11 + devkit_cli.egg-info/SOURCES.txt | 17 + devkit_cli.egg-info/dependency_links.txt | 1 + devkit_cli.egg-info/entry_points.txt | 2 + devkit_cli.egg-info/requires.txt | 3 + devkit_cli.egg-info/top_level.txt | 1 + devkit_cli/__init__.py | 9 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 449 bytes devkit_cli/__pycache__/main.cpython-314.pyc | Bin 0 -> 1984 bytes devkit_cli/commands/__init__.py | 11 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 376 bytes .../commands/__pycache__/file.cpython-314.pyc | Bin 0 -> 14264 bytes .../commands/__pycache__/info.cpython-314.pyc | Bin 0 -> 13458 bytes .../commands/__pycache__/todo.cpython-314.pyc | Bin 0 -> 20771 bytes devkit_cli/commands/file.py | 341 +++++++++++++ devkit_cli/commands/info.py | 273 +++++++++++ devkit_cli/commands/todo.py | 459 ++++++++++++++++++ devkit_cli/main.py | 51 ++ devkit_cli/utils/__init__.py | 17 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 508 bytes .../utils/__pycache__/display.cpython-314.pyc | Bin 0 -> 4612 bytes .../utils/__pycache__/logger.cpython-314.pyc | Bin 0 -> 4611 bytes devkit_cli/utils/display.py | 123 +++++ devkit_cli/utils/logger.py | 95 ++++ pyproject.toml | 18 + 26 files changed, 1618 insertions(+), 1 deletion(-) 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__/main.cpython-314.pyc create mode 100644 devkit_cli/commands/__init__.py create mode 100644 devkit_cli/commands/__pycache__/__init__.cpython-314.pyc create mode 100644 devkit_cli/commands/__pycache__/file.cpython-314.pyc create mode 100644 devkit_cli/commands/__pycache__/info.cpython-314.pyc create mode 100644 devkit_cli/commands/__pycache__/todo.cpython-314.pyc create mode 100644 devkit_cli/commands/file.py create mode 100644 devkit_cli/commands/info.py create mode 100644 devkit_cli/commands/todo.py create mode 100644 devkit_cli/main.py create mode 100644 devkit_cli/utils/__init__.py create mode 100644 devkit_cli/utils/__pycache__/__init__.cpython-314.pyc create mode 100644 devkit_cli/utils/__pycache__/display.cpython-314.pyc create mode 100644 devkit_cli/utils/__pycache__/logger.cpython-314.pyc create mode 100644 devkit_cli/utils/display.py create mode 100644 devkit_cli/utils/logger.py create mode 100644 pyproject.toml diff --git a/README.md b/README.md index 9770379..417f953 100644 --- a/README.md +++ b/README.md @@ -1 +1,186 @@ -# devkit-cli \ No newline at end of file +# ๐Ÿ› ๏ธ devkit-cli + +A powerful CLI toolkit for developers built with Python + Typer + Rich. + +## โœจ Features + +- ๐Ÿ“Š **System Information** - View CPU, memory, disk, and OS info in beautiful tables +- ๐Ÿ“ **Batch File Rename** - Rename files with custom prefix, suffix, and auto-numbering +- โœ… **Task Management** - Local todo list with priorities and status tracking +- ๐Ÿ“ **Logging** - Automatic logging to `~/.devkit/logs/` +- ๐ŸŽจ **Beautiful Output** - Colorful, formatted console output using Rich + +## ๐Ÿ“ฆ Installation + +```bash +pip install -e . +``` + +## ๐Ÿš€ Quick Start + +### View Help + +```bash +devkit --help # Main help +devkit info --help # Info command help +devkit file --help # File command help +devkit todo --help # Todo command help +``` + +## ๐Ÿ“– Commands Reference + +### 1. System Information (`info`) + +View detailed system information in colorful tables. + +```bash +devkit info all # View all system information +devkit info cpu # CPU information only +devkit info memory # Memory information only +devkit info disk # Disk information only +devkit info os # Operating system information only +``` + +**Example Output:** + +``` + ๐Ÿ–ฅ๏ธ CPU +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“ +โ”ƒ Property โ”ƒ Value โ”ƒ +โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ +โ”‚ Processor โ”‚ Intel64 Family 6 Model 190 Stepping 0, GenuineIntel โ”‚ +โ”‚ Physical Cores โ”‚ 4 โ”‚ +โ”‚ Logical Cores โ”‚ 4 โ”‚ +โ”‚ Max Frequency โ”‚ 1700.00 MHz โ”‚ +โ”‚ Current Frequency โ”‚ 1700.00 MHz โ”‚ +โ”‚ Usage โ”‚ 35.9% โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2. File Operations (`file`) + +Batch rename files with powerful options. + +#### List Files + +```bash +devkit file list # List files in current directory +devkit file list ./photos # List files in specific directory +devkit file list . --ext .txt # Filter by extension +devkit file list . --pattern "\.py$" # Filter by regex pattern +``` + +#### Rename Files + +```bash +# Add prefix with auto-numbering +devkit file rename ./photos --prefix "vacation_" --start 1 + +# Add suffix (before extension) +devkit file rename ./docs --suffix "_final" + +# Filter by regex pattern and rename +devkit file rename . --pattern "\.txt$" --prefix "doc_" + +# Preview changes without executing +devkit file rename . --prefix "test_" --dry-run + +# Skip confirmation prompt +devkit file rename . --prefix "file_" --yes +``` + +**Options:** + +| Option | Short | Description | +|--------|-------|-------------| +| `--prefix` | `-p` | Prefix to add to filenames | +| `--suffix` | `-s` | Suffix to add (before extension) | +| `--start` | `-n` | Starting number for auto-numbering (default: 1) | +| `--width` | `-w` | Width of number padding (default: 3, e.g., 001) | +| `--pattern` | `-f` | Regex pattern to filter files | +| `--dry-run` | `-d` | Preview changes without executing | +| `--yes` | `-y` | Skip confirmation prompt | + +### 3. Task Management (`todo`) + +Manage your tasks locally with priorities. + +#### Add Tasks + +```bash +devkit todo add "Complete the project documentation" +devkit todo add "Fix critical bug" --priority high +devkit todo add "Update dependencies" --priority medium +devkit todo add "Write unit tests" --priority low +``` + +#### List Tasks + +```bash +devkit todo list # List all tasks +devkit todo list --status pending # Only pending tasks +devkit todo list --status completed # Only completed tasks +devkit todo list --priority high # Filter by priority +``` + +#### Complete/Uncomplete Tasks + +```bash +devkit todo complete 1 # Mark task as completed +devkit todo uncomplete 1 # Mark task as pending +``` + +#### Edit Tasks + +```bash +devkit todo edit 1 --task "New task description" +devkit todo edit 1 --priority high +devkit todo edit 1 --task "New text" --priority low +``` + +#### Delete Tasks + +```bash +devkit todo delete 1 # Delete task (with confirmation) +devkit todo delete 1 --yes # Delete without confirmation +``` + +#### Clear Tasks + +```bash +devkit todo clear --status completed # Clear all completed tasks +devkit todo clear --status all # Clear all tasks +devkit todo clear --status all --yes # Clear all without confirmation +``` + +**Example Output:** + +``` + ๐Ÿ“‹ Todo List +โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“ +โ”ƒ ID โ”ƒ Status โ”ƒ Priority โ”ƒ Task โ”ƒ Created โ”ƒ +โ”กโ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ +โ”‚ 1 โ”‚ โœ… โ”‚ HIGH โ”‚ Complete the project โ”‚ 2026-03-20 13:45 โ”‚ +โ”‚ 2 โ”‚ โฌœ โ”‚ HIGH โ”‚ Fix critical bug โ”‚ 2026-03-20 13:45 โ”‚ +โ”‚ 3 โ”‚ โฌœ โ”‚ MEDIUM โ”‚ Update dependencies โ”‚ 2026-03-20 13:45 โ”‚ +โ”‚ 4 โ”‚ โฌœ โ”‚ LOW โ”‚ Write unit tests โ”‚ 2026-03-20 13:45 โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +Progress: 1/4 completed (25%) +``` + +## ๐Ÿ“ Data Storage + +- **Logs**: `~/.devkit/logs/devkit_YYYYMMDD.log` +- **Todos**: `~/.devkit/data/todos.json` + +## ๐Ÿ“‹ Requirements + +- Python >= 3.8 +- typer >= 0.9.0 +- rich >= 13.0.0 +- psutil >= 5.9.0 + +## ๐Ÿ“œ License + +MIT License diff --git a/devkit_cli.egg-info/PKG-INFO b/devkit_cli.egg-info/PKG-INFO new file mode 100644 index 0000000..2a895ca --- /dev/null +++ b/devkit_cli.egg-info/PKG-INFO @@ -0,0 +1,11 @@ +Metadata-Version: 2.4 +Name: devkit-cli +Version: 1.0.0 +Summary: A powerful CLI toolkit for developers +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..68e8b22 --- /dev/null +++ b/devkit_cli.egg-info/SOURCES.txt @@ -0,0 +1,17 @@ +README.md +pyproject.toml +devkit_cli/__init__.py +devkit_cli/main.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 +devkit_cli/commands/__init__.py +devkit_cli/commands/file.py +devkit_cli/commands/info.py +devkit_cli/commands/todo.py +devkit_cli/utils/__init__.py +devkit_cli/utils/display.py +devkit_cli/utils/logger.py \ 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..579ad4e --- /dev/null +++ b/devkit_cli.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +devkit = devkit_cli.main: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..1b339ea --- /dev/null +++ b/devkit_cli/__init__.py @@ -0,0 +1,9 @@ +""" +devkit-cli - A powerful CLI toolkit for developers + +This package provides a set of useful command-line tools for developers, +including system information viewing, batch file renaming, and task management. +""" + +__version__ = "1.0.0" +__author__ = "Developer" diff --git a/devkit_cli/__pycache__/__init__.cpython-314.pyc b/devkit_cli/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7b3723a7fb5a61d281262eced8dabed71a6d67df GIT binary patch literal 449 zcmZ8d%SyvQ6rJ>eHl-C@6qgrV728m^BBHb|1aT2`Aq0jonY2SEGa)l+X;;O+@gH2f zbS(i_{y?Qa;7zdz9+=0Ob2<0UIcc|Cf_VFU6?KP@uafM5e2dK?9xLJ#OMJ&Ve#KV& z>M`lomhW!FXH#MOp%l=ED@gU6XK^lJczXj@Yl$MnIs>fZQl~sKu6sWb22vJI*_cC` z>6wVQ0S1O!&@tqOmkyy$5~iZQ6pC-H%s-=^D^w`+NT@NGg|R#Vp|DrNtk4Q(g3nRv z!4tFL1Y#jMWL&Xi`-sk9nVACiz%df8tmiK4r{2ID6phht+PiMG*rPPkA*Dr=(iwgL z&5^j2vfNH|M(GRLlsOfb!?WPQU`22}dJIO~Os!4>j0V?OCc$4;zgJpnlnNy*rCz!y q_RkX?<&s}y4KytCHyzy8RmX9@D|M%~Zj*z~s`=46dUmrtlzsqHEsnkb literal 0 HcmV?d00001 diff --git a/devkit_cli/__pycache__/main.cpython-314.pyc b/devkit_cli/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..59009ad2e91e41dabbe1012eac92d53e784076a0 GIT binary patch literal 1984 zcmb7EO>7%Q6rTOD*Xy7B7D$_bCqk$glu)WlMJNJFl41)|SxbmOwY%D^$MNFzt~EPO zr$v!xkS1C*Md2b&!D=x`7O-ATbGMxgh5ireyh^Qc&|T789~s zi03t?&Dl7Bo(m;i!%c_uCxzOw?*+a~K#UbFkGMG{R2{GChicVvNvJZv;)n4FQ|o1TWbg3HumAY&0I!hk zw;d+#T)hmvEH!h&Y5G zOA+LfdcqSI@)dZ=$*n_JB>(rl$>bGm`>soDL55$&t$=WZ0|pjcU{8PyaMh=2Lu@RDGvd&9G7!>2hlXZJb{kFrR7&k;DQFF)s+uQD*5rBR3nqRG#A3>Ta2P~`$02S-mp4>TA$s6T;XI?aGg;gh4*91Oj~&u`-m zhwR{>4KZ$D9yr<%!OzJw!IgDB#JI(|$YKw_X)(Kod3Z68lTZ&RXS_8H!>?a`jMuH8 z4mssnTckle_DGDlTM;7)LMN&xC@(cp7L_7a24s=ZObd~rUxow6>1yNZK1m9wn$D6W6WXjSJIn4tq^Jk ztnHF(xp+AE$~?Gq3LX$@cJ$lASd+qcra&VKm8!$&BuD{&C#GXUbAX!i`uQ9`ya+=F z{S`;C^Y_)%{@UK!{Y2(KzpZ!2R`1sDmXC}ZM`^P=HQPO#{Zmu*78e;4CQOs( zlxgOsL(McRzHOR-x((3})6}tRivwU(=9eeMR+E>52;y`^7Jmq6M)gCap~x(eI$>;G zxKp42(HZU~VME2)wCuV_-mvt%5FBtD}sW*ev^sU`{%GsY)?UU8Mg4A>3t^WXe6YGBf literal 0 HcmV?d00001 diff --git a/devkit_cli/commands/__init__.py b/devkit_cli/commands/__init__.py new file mode 100644 index 0000000..80c50e5 --- /dev/null +++ b/devkit_cli/commands/__init__.py @@ -0,0 +1,11 @@ +""" +Commands Module + +This module contains all CLI subcommands. +""" + +from . import info +from . import file +from . import todo + +__all__ = ["info", "file", "todo"] diff --git a/devkit_cli/commands/__pycache__/__init__.cpython-314.pyc b/devkit_cli/commands/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6782c3c51acfcb885461af5febacc6a05bfae1e3 GIT binary patch literal 376 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 literal 0 HcmV?d00001 diff --git a/devkit_cli/commands/__pycache__/info.cpython-314.pyc b/devkit_cli/commands/__pycache__/info.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..400dad01bd2f7933abe772b112083ecf06707f91 GIT binary patch literal 13458 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 literal 0 HcmV?d00001 diff --git a/devkit_cli/commands/__pycache__/todo.cpython-314.pyc b/devkit_cli/commands/__pycache__/todo.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d8077b903867d4282b99df9f1dc38b841c274780 GIT binary patch literal 20771 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 bool: + """ + Validate that the directory exists and is accessible. + + Args: + directory: Path to the directory + + Returns: + True if valid, False otherwise + """ + if not directory.exists(): + print_error(f"Directory does not exist: {directory}") + return False + if not directory.is_dir(): + print_error(f"Path is not a directory: {directory}") + return False + return True + + +def get_files(directory: Path, pattern: Optional[str] = None) -> list[Path]: + """ + Get list of files in directory, optionally filtered by pattern. + + Args: + directory: Path to the directory + pattern: Optional regex pattern to filter files + + Returns: + List of file paths + """ + files = [] + try: + for item in directory.iterdir(): + if item.is_file(): + if pattern: + if re.search(pattern, item.name): + files.append(item) + else: + files.append(item) + files.sort(key=lambda x: x.name.lower()) + except PermissionError as e: + logger.error(f"Permission denied: {e}") + print_error(f"Permission denied accessing directory: {directory}") + except Exception as e: + logger.error(f"Error reading directory: {e}") + print_error(f"Error reading directory: {e}") + return files + + +def generate_new_name( + original_name: str, + prefix: Optional[str] = None, + suffix: Optional[str] = None, + number: Optional[int] = None, + number_width: int = 3, + keep_extension: bool = True, +) -> str: + """ + Generate a new filename based on the provided parameters. + + Args: + original_name: Original filename + prefix: Optional prefix to add + suffix: Optional suffix to add (before extension) + number: Optional number to include + number_width: Width of number padding (default: 3, e.g., 001, 002) + keep_extension: Whether to keep the original extension + + Returns: + New filename + """ + path = Path(original_name) + stem = path.stem + ext = path.suffix if keep_extension else "" + + new_name = stem + + if number is not None: + new_name = f"{str(number).zfill(number_width)}_{new_name}" + + if prefix: + new_name = f"{prefix}{new_name}" + + if suffix: + new_name = f"{new_name}{suffix}" + + if ext: + new_name = f"{new_name}{ext}" + + return new_name + + +def preview_rename( + files: list[Path], + prefix: Optional[str] = None, + suffix: Optional[str] = None, + start_number: int = 1, + number_width: int = 3, +) -> list[tuple[Path, Path]]: + """ + Preview the renaming operation. + + Args: + files: List of files to rename + prefix: Optional prefix + suffix: Optional suffix + start_number: Starting number for auto-numbering + number_width: Width of number padding + + Returns: + List of (old_path, new_path) tuples + """ + rename_pairs = [] + for i, file_path in enumerate(files): + new_name = generate_new_name( + file_path.name, + prefix=prefix, + suffix=suffix, + number=start_number + i, + number_width=number_width, + ) + new_path = file_path.parent / new_name + rename_pairs.append((file_path, new_path)) + return rename_pairs + + +def display_preview(rename_pairs: list[tuple[Path, Path]]) -> None: + """ + Display a preview table of the renaming operation. + + Args: + rename_pairs: List of (old_path, new_path) tuples + """ + table = Table(title="๐Ÿ“‹ Rename Preview", show_header=True, header_style="bold cyan") + table.add_column("Original Name", style="yellow") + table.add_column("โ†’", style="dim", justify="center", width=3) + table.add_column("New Name", style="green") + + for old_path, new_path in rename_pairs: + table.add_row(old_path.name, "โ†’", new_path.name) + + console.print(table) + + +@app.command() +def rename( + directory: str = typer.Argument( + ".", + help="Directory containing files to rename", + ), + prefix: Optional[str] = typer.Option( + None, + "--prefix", "-p", + help="Prefix to add to filenames", + ), + suffix: Optional[str] = typer.Option( + None, + "--suffix", "-s", + help="Suffix to add to filenames (before extension)", + ), + start: int = typer.Option( + 1, + "--start", "-n", + help="Starting number for auto-numbering", + ), + width: int = typer.Option( + 3, + "--width", "-w", + help="Width of number padding (e.g., 3 gives 001, 002)", + ), + pattern: Optional[str] = typer.Option( + None, + "--pattern", "-f", + help="Regex pattern to filter files", + ), + dry_run: bool = typer.Option( + False, + "--dry-run", "-d", + help="Preview changes without executing", + ), + yes: bool = typer.Option( + False, + "--yes", "-y", + help="Skip confirmation prompt", + ), +): + """ + Batch rename files in a directory. + + Examples: + devkit file rename ./photos --prefix "vacation_" --start 1 + devkit file rename ./docs --suffix "_final" --dry-run + devkit file rename . --pattern "\\.txt$" --prefix "doc_" + """ + dir_path = Path(directory).resolve() + + logger.info(f"Starting batch rename in: {dir_path}") + + if not validate_directory(dir_path): + raise typer.Exit(1) + + files = get_files(dir_path, pattern) + + if not files: + print_warning("No files found matching the criteria") + raise typer.Exit(0) + + print_info(f"Found {len(files)} file(s) to process") + + rename_pairs = preview_rename( + files, + prefix=prefix, + suffix=suffix, + start_number=start, + number_width=width, + ) + + console.print() + display_preview(rename_pairs) + console.print() + + if dry_run: + print_info("Dry run mode - no changes made") + raise typer.Exit(0) + + if not yes: + if not Confirm.ask("Proceed with renaming?"): + print_warning("Operation cancelled") + raise typer.Exit(0) + + success_count = 0 + error_count = 0 + + for old_path, new_path in rename_pairs: + try: + if new_path.exists(): + print_warning(f"Skipping {old_path.name} - target already exists: {new_path.name}") + error_count += 1 + continue + + old_path.rename(new_path) + logger.info(f"Renamed: {old_path.name} -> {new_path.name}") + success_count += 1 + except Exception as e: + logger.error(f"Error renaming {old_path.name}: {e}") + print_error(f"Failed to rename {old_path.name}: {e}") + error_count += 1 + + console.print() + print_success(f"Renaming complete: {success_count} succeeded, {error_count} failed") + + +@app.command() +def list( + directory: str = typer.Argument( + ".", + help="Directory to list files from", + ), + pattern: Optional[str] = typer.Option( + None, + "--pattern", "-f", + help="Regex pattern to filter files", + ), + ext: Optional[str] = typer.Option( + None, + "--ext", "-e", + help="Filter by file extension (e.g., .txt)", + ), +): + """ + List files in a directory with optional filtering. + + Examples: + devkit file list ./photos + devkit file list . --ext .txt + devkit file list . --pattern "doc_.*\\.pdf$" + """ + dir_path = Path(directory).resolve() + + logger.info(f"Listing files in: {dir_path}") + + if not validate_directory(dir_path): + raise typer.Exit(1) + + files = get_files(dir_path, pattern) + + if ext: + files = [f for f in files if f.suffix.lower() == ext.lower()] + + if not files: + print_warning("No files found matching the criteria") + raise typer.Exit(0) + + table = Table(title=f"๐Ÿ“ Files in {dir_path}", show_header=True, header_style="bold cyan") + table.add_column("#", style="dim", width=4) + table.add_column("Name", style="green") + table.add_column("Extension", style="yellow") + table.add_column("Size", style="blue", justify="right") + + for i, file_path in enumerate(files, 1): + size = file_path.stat().st_size + size_str = f"{size:,} B" if size < 1024 else f"{size / 1024:.1f} KB" + table.add_row( + str(i), + file_path.name, + file_path.suffix or "(none)", + size_str, + ) + + console.print(table) + print_info(f"Total: {len(files)} file(s)") + + +if __name__ == "__main__": + app() diff --git a/devkit_cli/commands/info.py b/devkit_cli/commands/info.py new file mode 100644 index 0000000..4205af6 --- /dev/null +++ b/devkit_cli/commands/info.py @@ -0,0 +1,273 @@ +""" +Info Command Module + +This module provides system information viewing functionality. +Displays CPU, memory, disk, and OS information in a formatted table. +""" + +import platform +from typing import Optional + +import psutil +import typer +from rich.console import Console +from rich.table import Table +from rich.panel import Panel + +from devkit_cli.utils.logger import get_logger + +app = typer.Typer(help="๐Ÿ“Š View system information") +console = Console() +logger = get_logger() + + +def get_size(bytes_value: int, suffix: str = "B") -> str: + """ + Convert bytes to human-readable format. + + Args: + bytes_value: Size in bytes + suffix: Unit suffix (default: B for bytes) + + Returns: + Human-readable size string + """ + factor = 1024 + for unit in ["", "K", "M", "G", "T", "P"]: + if bytes_value < factor: + return f"{bytes_value:.2f} {unit}{suffix}" + bytes_value /= factor + return f"{bytes_value:.2f} P{suffix}" + + +def get_cpu_info() -> dict[str, str]: + """ + Get CPU information. + + Returns: + Dictionary containing CPU details + """ + try: + cpu_freq = psutil.cpu_freq() + cpu_count_logical = psutil.cpu_count(logical=True) + cpu_count_physical = psutil.cpu_count(logical=False) + cpu_percent = psutil.cpu_percent(interval=0.5) + + return { + "Processor": platform.processor() or "Unknown", + "Physical Cores": str(cpu_count_physical) if cpu_count_physical else "N/A", + "Logical Cores": str(cpu_count_logical) if cpu_count_logical else "N/A", + "Max Frequency": f"{cpu_freq.max:.2f} MHz" if cpu_freq else "N/A", + "Current Frequency": f"{cpu_freq.current:.2f} MHz" if cpu_freq else "N/A", + "Usage": f"{cpu_percent:.1f}%", + } + except Exception as e: + logger.error(f"Error getting CPU info: {e}") + return {"Error": str(e)} + + +def get_memory_info() -> dict[str, str]: + """ + Get memory information. + + Returns: + Dictionary containing memory details + """ + try: + mem = psutil.virtual_memory() + swap = psutil.swap_memory() + + return { + "Total RAM": get_size(mem.total), + "Available RAM": get_size(mem.available), + "Used RAM": get_size(mem.used), + "RAM Usage": f"{mem.percent}%", + "Total Swap": get_size(swap.total), + "Used Swap": get_size(swap.used), + "Swap Usage": f"{swap.percent}%", + } + except Exception as e: + logger.error(f"Error getting memory info: {e}") + return {"Error": str(e)} + + +def get_disk_info() -> list[dict[str, str]]: + """ + Get disk information for all partitions. + + Returns: + List of dictionaries containing disk details + """ + disks = [] + try: + partitions = psutil.disk_partitions() + for partition in partitions: + try: + usage = psutil.disk_usage(partition.mountpoint) + disks.append({ + "Device": partition.device, + "Mountpoint": partition.mountpoint, + "File System": partition.fstype, + "Total Size": get_size(usage.total), + "Used": get_size(usage.used), + "Free": get_size(usage.free), + "Usage": f"{usage.percent}%", + }) + except PermissionError: + continue + except Exception as e: + logger.warning(f"Error accessing partition {partition.mountpoint}: {e}") + continue + except Exception as e: + logger.error(f"Error getting disk info: {e}") + disks.append({"Error": str(e)}) + return disks + + +def get_os_info() -> dict[str, str]: + """ + Get operating system information. + + Returns: + Dictionary containing OS details + """ + try: + return { + "System": platform.system(), + "Node Name": platform.node(), + "Release": platform.release(), + "Version": platform.version(), + "Machine": platform.machine(), + "Processor": platform.processor() or "Unknown", + "Python Version": platform.python_version(), + } + except Exception as e: + logger.error(f"Error getting OS info: {e}") + return {"Error": str(e)} + + +def create_info_table(title: str, data: dict[str, str]) -> Table: + """ + Create a formatted table for displaying information. + + Args: + title: Table title + data: Dictionary of key-value pairs to display + + Returns: + Configured Table object + """ + table = Table(title=title, show_header=True, header_style="bold cyan") + table.add_column("Property", style="green", no_wrap=True) + table.add_column("Value", style="white") + + for key, value in data.items(): + table.add_row(key, value) + + return table + + +@app.command() +def cpu(): + """Display CPU information.""" + logger.info("Fetching CPU information") + cpu_data = get_cpu_info() + table = create_info_table("๐Ÿ–ฅ๏ธ CPU Information", cpu_data) + console.print(table) + + +@app.command() +def memory(): + """Display memory information.""" + logger.info("Fetching memory information") + mem_data = get_memory_info() + table = create_info_table("๐Ÿ’พ Memory Information", mem_data) + console.print(table) + + +@app.command() +def disk(): + """Display disk information.""" + logger.info("Fetching disk information") + disk_data = get_disk_info() + + if not disk_data: + console.print("[yellow]No disk information available[/yellow]") + return + + table = Table(title="๐Ÿ’ฟ Disk Information", show_header=True, header_style="bold cyan") + table.add_column("Device", style="green") + table.add_column("Mountpoint", style="blue") + table.add_column("File System", style="yellow") + table.add_column("Total", style="white") + table.add_column("Used", style="white") + table.add_column("Free", style="white") + table.add_column("Usage", style="magenta") + + for disk in disk_data: + if "Error" not in disk: + table.add_row( + disk.get("Device", "N/A"), + disk.get("Mountpoint", "N/A"), + disk.get("File System", "N/A"), + disk.get("Total Size", "N/A"), + disk.get("Used", "N/A"), + disk.get("Free", "N/A"), + disk.get("Usage", "N/A"), + ) + + console.print(table) + + +@app.command() +def os(): + """Display operating system information.""" + logger.info("Fetching OS information") + os_data = get_os_info() + table = create_info_table("๐Ÿ–ฅ๏ธ Operating System Information", os_data) + console.print(table) + + +@app.command() +def all(): + """Display all system information.""" + logger.info("Fetching all system information") + + console.print() + console.print(Panel.fit("[bold cyan]๐Ÿ“Š System Information Report[/bold cyan]", border_style="blue")) + + console.print() + cpu_data = get_cpu_info() + console.print(create_info_table("๐Ÿ–ฅ๏ธ CPU", cpu_data)) + + console.print() + mem_data = get_memory_info() + console.print(create_info_table("๐Ÿ’พ Memory", mem_data)) + + console.print() + disk_data = get_disk_info() + if disk_data: + disk_table = Table(title="๐Ÿ’ฟ Disk", show_header=True, header_style="bold cyan") + disk_table.add_column("Device", style="green") + disk_table.add_column("Mountpoint", style="blue") + disk_table.add_column("Total", style="white") + disk_table.add_column("Usage", style="magenta") + for disk in disk_data: + if "Error" not in disk: + disk_table.add_row( + disk.get("Device", "N/A"), + disk.get("Mountpoint", "N/A"), + disk.get("Total Size", "N/A"), + disk.get("Usage", "N/A"), + ) + console.print(disk_table) + + console.print() + os_data = get_os_info() + console.print(create_info_table("๐Ÿ–ฅ๏ธ Operating System", os_data)) + + console.print() + + +if __name__ == "__main__": + app() diff --git a/devkit_cli/commands/todo.py b/devkit_cli/commands/todo.py new file mode 100644 index 0000000..697bbec --- /dev/null +++ b/devkit_cli/commands/todo.py @@ -0,0 +1,459 @@ +""" +Todo Command Module + +This module provides local task management functionality. +Supports adding, viewing, marking complete, and deleting tasks. +""" + +import json +from datetime import datetime +from pathlib import Path +from typing import Optional + +import typer +from rich.console import Console +from rich.table import Table +from rich.panel import Panel + +from devkit_cli.utils.logger import get_logger +from devkit_cli.utils.display import print_success, print_error, print_warning, print_info + +app = typer.Typer(help="โœ… Task management") +console = Console() +logger = get_logger() + +DATA_DIR = Path.home() / ".devkit" / "data" +DATA_FILE = DATA_DIR / "todos.json" + + +def ensure_data_dir() -> None: + """Ensure the data directory exists.""" + DATA_DIR.mkdir(parents=True, exist_ok=True) + + +def load_todos() -> list[dict]: + """ + Load todos from the JSON file. + + Returns: + List of todo dictionaries + """ + ensure_data_dir() + try: + if DATA_FILE.exists(): + with open(DATA_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except json.JSONDecodeError as e: + logger.error(f"Error parsing todo file: {e}") + except Exception as e: + logger.error(f"Error loading todos: {e}") + return [] + + +def save_todos(todos: list[dict]) -> None: + """ + Save todos to the JSON file. + + Args: + todos: List of todo dictionaries to save + """ + ensure_data_dir() + try: + with open(DATA_FILE, "w", encoding="utf-8") as f: + json.dump(todos, f, indent=2, ensure_ascii=False) + logger.info(f"Saved {len(todos)} todo(s)") + except Exception as e: + logger.error(f"Error saving todos: {e}") + raise + + +def get_next_id(todos: list[dict]) -> int: + """ + Get the next available todo ID. + + Args: + todos: List of existing todos + + Returns: + Next available ID + """ + if not todos: + return 1 + return max(todo.get("id", 0) for todo in todos) + 1 + + +def display_todos_table(todos: list[dict], title: str = "๐Ÿ“‹ Todo List") -> None: + """ + Display todos in a formatted table. + + Args: + todos: List of todo dictionaries + title: Table title + """ + if not todos: + print_info("No tasks found. Add a new task with 'devkit todo add'") + return + + table = Table(title=title, show_header=True, header_style="bold cyan") + table.add_column("ID", style="dim", width=4, justify="center") + table.add_column("Status", width=8, justify="center") + table.add_column("Priority", width=8, justify="center") + table.add_column("Task", style="white") + table.add_column("Created", style="dim") + + for todo in todos: + status = "โœ…" if todo.get("completed", False) else "โฌœ" + status_style = "green" if todo.get("completed", False) else "yellow" + + priority = todo.get("priority", "medium") + priority_styles = { + "high": "bold red", + "medium": "yellow", + "low": "dim", + } + priority_style = priority_styles.get(priority, "white") + + created = todo.get("created", "") + if created: + try: + dt = datetime.fromisoformat(created) + created = dt.strftime("%Y-%m-%d %H:%M") + except ValueError: + pass + + table.add_row( + str(todo.get("id", "?")), + f"[{status_style}]{status}[/{status_style}]", + f"[{priority_style}]{priority.upper()}[/{priority_style}]", + todo.get("task", ""), + created, + ) + + console.print(table) + + completed = sum(1 for t in todos if t.get("completed", False)) + total = len(todos) + console.print() + console.print(f"[dim]Progress: {completed}/{total} completed ({completed/total*100:.0f}%)[/dim]") + + +@app.command() +def add( + task: str = typer.Argument( + ..., + help="Task description", + ), + priority: str = typer.Option( + "medium", + "--priority", "-p", + help="Task priority: high, medium, low", + ), +): + """ + Add a new task to the todo list. + + Examples: + devkit todo add "Complete the project documentation" + devkit todo add "Fix critical bug" --priority high + """ + if priority.lower() not in ["high", "medium", "low"]: + print_error(f"Invalid priority: {priority}. Must be high, medium, or low.") + raise typer.Exit(1) + + todos = load_todos() + + new_todo = { + "id": get_next_id(todos), + "task": task, + "priority": priority.lower(), + "completed": False, + "created": datetime.now().isoformat(), + "completed_at": None, + } + + todos.append(new_todo) + save_todos(todos) + + logger.info(f"Added task: {task}") + print_success(f"Task added: {task}") + console.print(f"[dim]ID: {new_todo['id']} | Priority: {priority}[/dim]") + + +@app.command() +def list( + status: Optional[str] = typer.Option( + None, + "--status", "-s", + help="Filter by status: all, pending, completed", + ), + priority: Optional[str] = typer.Option( + None, + "--priority", "-p", + help="Filter by priority: high, medium, low", + ), +): + """ + List all tasks with optional filtering. + + Examples: + devkit todo list + devkit todo list --status pending + devkit todo list --priority high + """ + todos = load_todos() + + if status: + status = status.lower() + if status == "pending": + todos = [t for t in todos if not t.get("completed", False)] + elif status == "completed": + todos = [t for t in todos if t.get("completed", False)] + elif status != "all": + print_error(f"Invalid status: {status}. Must be all, pending, or completed.") + raise typer.Exit(1) + + if priority: + priority = priority.lower() + if priority not in ["high", "medium", "low"]: + print_error(f"Invalid priority: {priority}. Must be high, medium, or low.") + raise typer.Exit(1) + todos = [t for t in todos if t.get("priority", "medium") == priority] + + logger.info(f"Listing {len(todos)} todo(s)") + display_todos_table(todos) + + +@app.command() +def complete( + task_id: int = typer.Argument( + ..., + help="Task ID to mark as complete", + ), +): + """ + Mark a task as completed. + + Examples: + devkit todo complete 1 + """ + todos = load_todos() + + for todo in todos: + if todo.get("id") == task_id: + if todo.get("completed", False): + print_warning(f"Task {task_id} is already completed") + raise typer.Exit(0) + + todo["completed"] = True + todo["completed_at"] = datetime.now().isoformat() + save_todos(todos) + + logger.info(f"Completed task {task_id}: {todo.get('task')}") + print_success(f"Task {task_id} marked as completed: {todo.get('task')}") + raise typer.Exit(0) + + print_error(f"Task with ID {task_id} not found") + raise typer.Exit(1) + + +@app.command() +def uncomplete( + task_id: int = typer.Argument( + ..., + help="Task ID to mark as incomplete", + ), +): + """ + Mark a completed task as pending. + + Examples: + devkit todo uncomplete 1 + """ + todos = load_todos() + + for todo in todos: + if todo.get("id") == task_id: + if not todo.get("completed", False): + print_warning(f"Task {task_id} is already pending") + raise typer.Exit(0) + + todo["completed"] = False + todo["completed_at"] = None + save_todos(todos) + + logger.info(f"Uncompleted task {task_id}: {todo.get('task')}") + print_success(f"Task {task_id} marked as pending: {todo.get('task')}") + raise typer.Exit(0) + + print_error(f"Task with ID {task_id} not found") + raise typer.Exit(1) + + +@app.command() +def delete( + task_id: int = typer.Argument( + ..., + help="Task ID to delete", + ), + yes: bool = typer.Option( + False, + "--yes", "-y", + help="Skip confirmation prompt", + ), +): + """ + Delete a task from the todo list. + + Examples: + devkit todo delete 1 + devkit todo delete 1 --yes + """ + todos = load_todos() + + for i, todo in enumerate(todos): + if todo.get("id") == task_id: + task_text = todo.get("task", "") + + if not yes: + console.print(f"[yellow]Delete task:[/yellow] {task_text}") + from rich.prompt import Confirm + if not Confirm.ask("Are you sure?"): + print_warning("Operation cancelled") + raise typer.Exit(0) + + todos.pop(i) + save_todos(todos) + + logger.info(f"Deleted task {task_id}: {task_text}") + print_success(f"Task {task_id} deleted: {task_text}") + raise typer.Exit(0) + + print_error(f"Task with ID {task_id} not found") + raise typer.Exit(1) + + +@app.command() +def clear( + status: Optional[str] = typer.Option( + None, + "--status", "-s", + help="Clear tasks by status: all, completed", + ), + yes: bool = typer.Option( + False, + "--yes", "-y", + help="Skip confirmation prompt", + ), +): + """ + Clear tasks from the todo list. + + Examples: + devkit todo clear --status completed + devkit todo clear --status all --yes + """ + todos = load_todos() + + if not todos: + print_info("No tasks to clear") + raise typer.Exit(0) + + if status is None: + status = "completed" + + status = status.lower() + + if status == "completed": + to_remove = [t for t in todos if t.get("completed", False)] + if not to_remove: + print_info("No completed tasks to clear") + raise typer.Exit(0) + + if not yes: + console.print(f"[yellow]Clear {len(to_remove)} completed task(s)?[/yellow]") + from rich.prompt import Confirm + if not Confirm.ask("Are you sure?"): + print_warning("Operation cancelled") + raise typer.Exit(0) + + todos = [t for t in todos if not t.get("completed", False)] + save_todos(todos) + logger.info(f"Cleared {len(to_remove)} completed task(s)") + print_success(f"Cleared {len(to_remove)} completed task(s)") + + elif status == "all": + if not yes: + console.print(f"[red]Clear ALL {len(todos)} task(s)?[/red]") + from rich.prompt import Confirm + if not Confirm.ask("Are you sure?"): + print_warning("Operation cancelled") + raise typer.Exit(0) + + save_todos([]) + logger.info("Cleared all tasks") + print_success("Cleared all tasks") + + else: + print_error(f"Invalid status: {status}. Must be all or completed.") + raise typer.Exit(1) + + +@app.command() +def edit( + task_id: int = typer.Argument( + ..., + help="Task ID to edit", + ), + task: Optional[str] = typer.Option( + None, + "--task", "-t", + help="New task description", + ), + priority: Optional[str] = typer.Option( + None, + "--priority", "-p", + help="New priority: high, medium, low", + ), +): + """ + Edit an existing task. + + Examples: + devkit todo edit 1 --task "Updated task description" + devkit todo edit 1 --priority high + devkit todo edit 1 --task "New text" --priority low + """ + if task is None and priority is None: + print_error("Please provide at least one option to edit (--task or --priority)") + raise typer.Exit(1) + + if priority and priority.lower() not in ["high", "medium", "low"]: + print_error(f"Invalid priority: {priority}. Must be high, medium, or low.") + raise typer.Exit(1) + + todos = load_todos() + + for todo in todos: + if todo.get("id") == task_id: + old_task = todo.get("task", "") + old_priority = todo.get("priority", "medium") + + if task: + todo["task"] = task + if priority: + todo["priority"] = priority.lower() + + save_todos(todos) + + logger.info(f"Edited task {task_id}") + print_success(f"Task {task_id} updated") + console.print(f"[dim]Task: {old_task} โ†’ {todo.get('task')}[/dim]") + console.print(f"[dim]Priority: {old_priority} โ†’ {todo.get('priority')}[/dim]") + raise typer.Exit(0) + + print_error(f"Task with ID {task_id} not found") + raise typer.Exit(1) + + +if __name__ == "__main__": + app() diff --git a/devkit_cli/main.py b/devkit_cli/main.py new file mode 100644 index 0000000..5dbd57c --- /dev/null +++ b/devkit_cli/main.py @@ -0,0 +1,51 @@ +""" +devkit-cli Main Entry Point + +This module defines the main CLI application and registers all subcommands. +""" + +import typer +from rich.console import Console +from rich.panel import Panel + +from devkit_cli.commands import info, file, todo + +app = typer.Typer( + name="devkit", + help="๐Ÿ› ๏ธ DevKit CLI - A powerful developer toolkit", + add_completion=False, +) +console = Console() + + +@app.callback() +def main(): + """ + ๐Ÿ› ๏ธ DevKit CLI - A powerful developer toolkit + + A collection of useful command-line tools for developers. + Use --help with any command to see more details. + """ + pass + + +app.add_typer(info.app, name="info", help="๐Ÿ“Š View system information") +app.add_typer(file.app, name="file", help="๐Ÿ“ Batch file operations") +app.add_typer(todo.app, name="todo", help="โœ… Task management") + + +@app.command() +def version(): + """Show the current version of devkit-cli.""" + from devkit_cli import __version__ + console.print( + Panel.fit( + f"[bold green]devkit-cli[/bold green] version [bold cyan]{__version__}[/bold cyan]", + title="Version", + border_style="blue", + ) + ) + + +if __name__ == "__main__": + app() diff --git a/devkit_cli/utils/__init__.py b/devkit_cli/utils/__init__.py new file mode 100644 index 0000000..85b4905 --- /dev/null +++ b/devkit_cli/utils/__init__.py @@ -0,0 +1,17 @@ +""" +Utilities Module + +This module contains utility functions for logging and display. +""" + +from .logger import get_logger, setup_logger +from .display import print_success, print_error, print_warning, print_info + +__all__ = [ + "get_logger", + "setup_logger", + "print_success", + "print_error", + "print_warning", + "print_info", +] diff --git a/devkit_cli/utils/__pycache__/__init__.cpython-314.pyc b/devkit_cli/utils/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e4f6e23a095d2e7634ed34ef3f5a50c05374ae34 GIT binary patch literal 508 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 I2R_FC0nsPHGynhq literal 0 HcmV?d00001 diff --git a/devkit_cli/utils/__pycache__/logger.cpython-314.pyc b/devkit_cli/utils/__pycache__/logger.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..52980e2b8c47b776e3b94e954261fa70ac44dcc0 GIT binary patch literal 4611 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 a^3L8DEC<|ppF?3-@-Z5EA%g9=u>B9~sP=RK literal 0 HcmV?d00001 diff --git a/devkit_cli/utils/display.py b/devkit_cli/utils/display.py new file mode 100644 index 0000000..f386fba --- /dev/null +++ b/devkit_cli/utils/display.py @@ -0,0 +1,123 @@ +""" +Display Module + +This module provides beautiful console output utilities using Rich. +""" + +from typing import Any, Optional +from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.text import Text + +console = Console() + + +def print_success(message: str, title: Optional[str] = None) -> None: + """ + Print a success message with green styling. + + Args: + message: The message to display + title: Optional title for the panel + """ + if title: + console.print( + Panel.fit( + f"[bold green]โœ“[/bold green] {message}", + title=title, + border_style="green", + ) + ) + else: + console.print(f"[bold green]โœ“[/bold green] {message}") + + +def print_error(message: str, title: Optional[str] = None) -> None: + """ + Print an error message with red styling. + + Args: + message: The message to display + title: Optional title for the panel + """ + if title: + console.print( + Panel.fit( + f"[bold red]โœ—[/bold red] {message}", + title=title, + border_style="red", + ) + ) + else: + console.print(f"[bold red]โœ—[/bold red] {message}") + + +def print_warning(message: str, title: Optional[str] = None) -> None: + """ + Print a warning message with yellow styling. + + Args: + message: The message to display + title: Optional title for the panel + """ + if title: + console.print( + Panel.fit( + f"[bold yellow]โš [/bold yellow] {message}", + title=title, + border_style="yellow", + ) + ) + else: + console.print(f"[bold yellow]โš [/bold yellow] {message}") + + +def print_info(message: str, title: Optional[str] = None) -> None: + """ + Print an info message with blue styling. + + Args: + message: The message to display + title: Optional title for the panel + """ + if title: + console.print( + Panel.fit( + f"[bold blue]โ„น[/bold blue] {message}", + title=title, + border_style="blue", + ) + ) + else: + console.print(f"[bold blue]โ„น[/bold blue] {message}") + + +def create_table( + title: str, + columns: list[str], + rows: list[list[Any]], + show_header: bool = True, +) -> Table: + """ + Create a formatted table with colored output. + + Args: + title: Table title + columns: Column headers + rows: Table data rows + show_header: Whether to show column headers + + Returns: + Configured Table object + """ + table = Table(title=title, show_header=show_header, header_style="bold cyan") + + for column in columns: + table.add_column(column, style="white") + + for row in rows: + styled_row = [str(cell) for cell in row] + table.add_row(*styled_row) + + return table diff --git a/devkit_cli/utils/logger.py b/devkit_cli/utils/logger.py new file mode 100644 index 0000000..798ed88 --- /dev/null +++ b/devkit_cli/utils/logger.py @@ -0,0 +1,95 @@ +""" +Logger Module + +This module provides logging functionality with colored output support. +""" + +import logging +import sys +from pathlib import Path +from typing import Optional +from datetime import datetime +from rich.console import Console + +console = Console() + +LOG_DIR = Path.home() / ".devkit" / "logs" +LOG_DIR.mkdir(parents=True, exist_ok=True) + + +class ColoredFormatter(logging.Formatter): + """Custom formatter with color support for different log levels.""" + + COLORS = { + "DEBUG": "dim", + "INFO": "blue", + "WARNING": "yellow", + "ERROR": "red", + "CRITICAL": "bold red", + } + + def format(self, record: logging.LogRecord) -> str: + log_message = super().format(record) + color = self.COLORS.get(record.levelname, "white") + return f"[{color}]{log_message}[/{color}]" + + +def setup_logger( + name: str = "devkit", + level: int = logging.INFO, + log_to_file: bool = True, +) -> logging.Logger: + """ + Setup and configure a logger instance. + + Args: + name: Logger name + level: Logging level + log_to_file: Whether to log to file + + Returns: + Configured logger instance + """ + logger = logging.getLogger(name) + logger.setLevel(level) + + if logger.handlers: + return logger + + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(level) + console_formatter = ColoredFormatter( + "%(asctime)s | %(levelname)-8s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + console_handler.setFormatter(console_formatter) + logger.addHandler(console_handler) + + if log_to_file: + log_file = LOG_DIR / f"devkit_{datetime.now().strftime('%Y%m%d')}.log" + file_handler = logging.FileHandler(log_file, encoding="utf-8") + file_handler.setLevel(level) + file_formatter = logging.Formatter( + "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + file_handler.setFormatter(file_formatter) + logger.addHandler(file_handler) + + return logger + + +_logger: Optional[logging.Logger] = None + + +def get_logger() -> logging.Logger: + """ + Get the global logger instance. + + Returns: + Global logger instance + """ + global _logger + if _logger is None: + _logger = setup_logger() + return _logger diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1b23e52 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "devkit-cli" +version = "1.0.0" +description = "A powerful CLI toolkit for developers" +readme = "README.md" +requires-python = ">=3.8" +dependencies = [ + "typer>=0.9.0", + "rich>=13.0.0", + "psutil>=5.9.0", +] + +[project.scripts] +devkit = "devkit_cli.main:app"