devbits 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
devbits-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bruce Chuang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
devbits-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,175 @@
1
+ Metadata-Version: 2.4
2
+ Name: devbits
3
+ Version: 0.1.0
4
+ Summary: A lightweight CLI toolkit for daily development utilities.
5
+ Author: Bruce Chuang
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/<your-github-username>/devbits
8
+ Project-URL: Repository, https://github.com/<your-github-username>/devbits
9
+ Project-URL: Issues, https://github.com/<your-github-username>/devbits/issues
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: opencv-python>=4.8.0
14
+ Requires-Dist: pillow>=10.0.0
15
+ Dynamic: license-file
16
+
17
+ # devbits
18
+
19
+ ## CLI style
20
+
21
+ `devbits` provides both a main command and standalone commands installed into your Python environment's PATH.
22
+
23
+ Main command style:
24
+
25
+ ```bash
26
+ devbits clearcache .
27
+ devbits images2video frames/ -o output.mp4 --fps 30
28
+ ```
29
+
30
+ Standalone command style:
31
+
32
+ ```bash
33
+ clearcache .
34
+ images2video frames/ -o output.mp4 --fps 30
35
+ video2gif input.mp4 -o output.gif
36
+ clipvideo input.mp4 --start 3 --end 10 -o clip.mp4
37
+ ```
38
+
39
+ After editable install, reopen the terminal or run `hash -r` if your shell does not immediately find the new commands.
40
+
41
+
42
+ `devbits` is a lightweight CLI toolkit for daily development utilities, including cache cleanup, media conversion, image helpers, dataset-like file operations, and project maintenance tools.
43
+
44
+ The project supports two CLI styles:
45
+
46
+ ```bash
47
+ devbits <command> [options]
48
+ <command> [options]
49
+ ```
50
+
51
+ ## Features
52
+
53
+ ### Project cleanup
54
+
55
+ ```bash
56
+ devbits clearcache .
57
+ devbits clearcache . --all
58
+ devbits clearcache . --dry-run
59
+ ```
60
+
61
+ Removes:
62
+
63
+ - `__pycache__/`
64
+ - `*.pyc`
65
+ - `*.pyo`
66
+ - optionally `.pytest_cache/`, `.mypy_cache/`, `.ruff_cache/`
67
+
68
+ ### Image and video conversion
69
+
70
+ ```bash
71
+ devbits images2video ./frames -o output.mp4 --fps 30
72
+ devbits video2images input.mp4 -o ./frames --every 1
73
+ devbits images2gif ./frames -o output.gif --fps 10
74
+ devbits video2gif input.mp4 -o output.gif --fps 10 --start 2 --end 8
75
+ devbits clipvideo input.mp4 -o clip.mp4 --start 3 --end 10
76
+ devbits clipvideo input.mp4 -o clip.mp4 --start-frame 100 --end-frame 500
77
+ devbits resizevideo input.mp4 -o resized.mp4 --size 1280,720
78
+ ```
79
+
80
+ ### Image utilities
81
+
82
+ ```bash
83
+ devbits image2ico logo.png -o logo.ico
84
+ devbits resizeimage input.jpg -o output.jpg --size 640,480
85
+ devbits batchimages ./images -o ./resized --size 640,480 --format jpg
86
+ devbits checkimages ./images --recursive
87
+ devbits contactsheet ./images -o sheet.jpg --cols 5 --labels
88
+ ```
89
+
90
+ ### Project utilities
91
+
92
+ ```bash
93
+ devbits tree . --depth 3
94
+ devbits size . --top 20
95
+ devbits renamefiles ./images --prefix frame --digits 6
96
+ devbits renamefiles ./images --prefix frame --digits 6 --dry-run
97
+ devbits samplefiles ./images -o ./sample --num 100
98
+ ```
99
+
100
+ ## Installation for local development
101
+
102
+ ```bash
103
+ git clone https://github.com/yourname/devbits.git
104
+ cd devbits
105
+ python -m venv .venv
106
+ source .venv/bin/activate
107
+ pip install -e ".[dev]"
108
+ ```
109
+
110
+ Check the CLI:
111
+
112
+ ```bash
113
+ devbits --help
114
+ devbits images2video --help
115
+ clearcache --help
116
+ images2video --help
117
+ ```
118
+
119
+ ## Build package
120
+
121
+ ```bash
122
+ python -m pip install --upgrade build twine
123
+ python -m build
124
+ python -m twine check dist/*
125
+ ```
126
+
127
+ ## Upload to TestPyPI
128
+
129
+ ```bash
130
+ python -m twine upload --repository testpypi dist/*
131
+ ```
132
+
133
+ ## Upload to PyPI
134
+
135
+ ```bash
136
+ python -m twine upload dist/*
137
+ ```
138
+
139
+ ## Commands
140
+
141
+ | Command | Description |
142
+ |---|---|
143
+ | `clearcache` | Clear Python cache files. |
144
+ | `images2video` | Convert image sequence to MP4 video. |
145
+ | `video2images` | Extract frames from a video. |
146
+ | `images2gif` | Convert image sequence to GIF. |
147
+ | `video2gif` | Convert video to GIF. |
148
+ | `clipvideo` | Clip video by seconds or frame indices. |
149
+ | `resizevideo` | Resize a video. |
150
+ | `image2ico` | Convert image to `.ico`. |
151
+ | `resizeimage` | Resize an image. |
152
+ | `batchimages` | Batch resize or convert images. |
153
+ | `checkimages` | Check broken image files. |
154
+ | `contactsheet` | Create a contact sheet. |
155
+ | `tree` | Print a compact project tree. |
156
+ | `size` | Show top-level folder/file sizes. |
157
+ | `renamefiles` | Batch rename files. |
158
+ | `samplefiles` | Copy or move first N files. |
159
+
160
+ ## Roadmap
161
+
162
+ - Interactive `clipvideo --gui`
163
+ - `mergevideos`
164
+ - `comparevideos`
165
+ - `concatframes`
166
+ - `annotatevideo`
167
+ - `watermark`
168
+ - `splitdataset`
169
+ - `countdataset`
170
+ - `dedupimages`
171
+ - Optional ROS helpers under `devbits[ros]`
172
+
173
+ ## License
174
+
175
+ MIT
@@ -0,0 +1,159 @@
1
+ # devbits
2
+
3
+ ## CLI style
4
+
5
+ `devbits` provides both a main command and standalone commands installed into your Python environment's PATH.
6
+
7
+ Main command style:
8
+
9
+ ```bash
10
+ devbits clearcache .
11
+ devbits images2video frames/ -o output.mp4 --fps 30
12
+ ```
13
+
14
+ Standalone command style:
15
+
16
+ ```bash
17
+ clearcache .
18
+ images2video frames/ -o output.mp4 --fps 30
19
+ video2gif input.mp4 -o output.gif
20
+ clipvideo input.mp4 --start 3 --end 10 -o clip.mp4
21
+ ```
22
+
23
+ After editable install, reopen the terminal or run `hash -r` if your shell does not immediately find the new commands.
24
+
25
+
26
+ `devbits` is a lightweight CLI toolkit for daily development utilities, including cache cleanup, media conversion, image helpers, dataset-like file operations, and project maintenance tools.
27
+
28
+ The project supports two CLI styles:
29
+
30
+ ```bash
31
+ devbits <command> [options]
32
+ <command> [options]
33
+ ```
34
+
35
+ ## Features
36
+
37
+ ### Project cleanup
38
+
39
+ ```bash
40
+ devbits clearcache .
41
+ devbits clearcache . --all
42
+ devbits clearcache . --dry-run
43
+ ```
44
+
45
+ Removes:
46
+
47
+ - `__pycache__/`
48
+ - `*.pyc`
49
+ - `*.pyo`
50
+ - optionally `.pytest_cache/`, `.mypy_cache/`, `.ruff_cache/`
51
+
52
+ ### Image and video conversion
53
+
54
+ ```bash
55
+ devbits images2video ./frames -o output.mp4 --fps 30
56
+ devbits video2images input.mp4 -o ./frames --every 1
57
+ devbits images2gif ./frames -o output.gif --fps 10
58
+ devbits video2gif input.mp4 -o output.gif --fps 10 --start 2 --end 8
59
+ devbits clipvideo input.mp4 -o clip.mp4 --start 3 --end 10
60
+ devbits clipvideo input.mp4 -o clip.mp4 --start-frame 100 --end-frame 500
61
+ devbits resizevideo input.mp4 -o resized.mp4 --size 1280,720
62
+ ```
63
+
64
+ ### Image utilities
65
+
66
+ ```bash
67
+ devbits image2ico logo.png -o logo.ico
68
+ devbits resizeimage input.jpg -o output.jpg --size 640,480
69
+ devbits batchimages ./images -o ./resized --size 640,480 --format jpg
70
+ devbits checkimages ./images --recursive
71
+ devbits contactsheet ./images -o sheet.jpg --cols 5 --labels
72
+ ```
73
+
74
+ ### Project utilities
75
+
76
+ ```bash
77
+ devbits tree . --depth 3
78
+ devbits size . --top 20
79
+ devbits renamefiles ./images --prefix frame --digits 6
80
+ devbits renamefiles ./images --prefix frame --digits 6 --dry-run
81
+ devbits samplefiles ./images -o ./sample --num 100
82
+ ```
83
+
84
+ ## Installation for local development
85
+
86
+ ```bash
87
+ git clone https://github.com/yourname/devbits.git
88
+ cd devbits
89
+ python -m venv .venv
90
+ source .venv/bin/activate
91
+ pip install -e ".[dev]"
92
+ ```
93
+
94
+ Check the CLI:
95
+
96
+ ```bash
97
+ devbits --help
98
+ devbits images2video --help
99
+ clearcache --help
100
+ images2video --help
101
+ ```
102
+
103
+ ## Build package
104
+
105
+ ```bash
106
+ python -m pip install --upgrade build twine
107
+ python -m build
108
+ python -m twine check dist/*
109
+ ```
110
+
111
+ ## Upload to TestPyPI
112
+
113
+ ```bash
114
+ python -m twine upload --repository testpypi dist/*
115
+ ```
116
+
117
+ ## Upload to PyPI
118
+
119
+ ```bash
120
+ python -m twine upload dist/*
121
+ ```
122
+
123
+ ## Commands
124
+
125
+ | Command | Description |
126
+ |---|---|
127
+ | `clearcache` | Clear Python cache files. |
128
+ | `images2video` | Convert image sequence to MP4 video. |
129
+ | `video2images` | Extract frames from a video. |
130
+ | `images2gif` | Convert image sequence to GIF. |
131
+ | `video2gif` | Convert video to GIF. |
132
+ | `clipvideo` | Clip video by seconds or frame indices. |
133
+ | `resizevideo` | Resize a video. |
134
+ | `image2ico` | Convert image to `.ico`. |
135
+ | `resizeimage` | Resize an image. |
136
+ | `batchimages` | Batch resize or convert images. |
137
+ | `checkimages` | Check broken image files. |
138
+ | `contactsheet` | Create a contact sheet. |
139
+ | `tree` | Print a compact project tree. |
140
+ | `size` | Show top-level folder/file sizes. |
141
+ | `renamefiles` | Batch rename files. |
142
+ | `samplefiles` | Copy or move first N files. |
143
+
144
+ ## Roadmap
145
+
146
+ - Interactive `clipvideo --gui`
147
+ - `mergevideos`
148
+ - `comparevideos`
149
+ - `concatframes`
150
+ - `annotatevideo`
151
+ - `watermark`
152
+ - `splitdataset`
153
+ - `countdataset`
154
+ - `dedupimages`
155
+ - Optional ROS helpers under `devbits[ros]`
156
+
157
+ ## License
158
+
159
+ MIT
@@ -0,0 +1,3 @@
1
+ """devbits: A lightweight CLI toolkit for daily development utilities."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ CACHE_DIR_NAMES = {"__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache"}
7
+ PYC_PATTERNS = ["*.pyc", "*.pyo"]
8
+
9
+
10
+ def clear_cache(root: Path, include_extra: bool = False, dry_run: bool = False) -> list[Path]:
11
+ targets: list[Path] = []
12
+ dir_names = set(CACHE_DIR_NAMES if include_extra else {"__pycache__"})
13
+
14
+ for path in root.rglob("*"):
15
+ if path.is_dir() and path.name in dir_names:
16
+ targets.append(path)
17
+
18
+ for pattern in PYC_PATTERNS:
19
+ targets.extend(root.rglob(pattern))
20
+
21
+ unique_targets = sorted(set(targets), key=lambda p: len(p.parts), reverse=True)
22
+ if dry_run:
23
+ return unique_targets
24
+
25
+ for target in unique_targets:
26
+ if not target.exists():
27
+ continue
28
+ if target.is_dir():
29
+ shutil.rmtree(target)
30
+ else:
31
+ target.unlink()
32
+ return unique_targets
@@ -0,0 +1,228 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from .cache import clear_cache
8
+ from .image import batch_images, check_images, contact_sheet, image_to_ico, resize_image
9
+ from .media import clip_video, images_to_gif, images_to_video, resize_video, video_to_gif, video_to_images
10
+ from .project import print_tree, rename_files, sample_files, top_sizes
11
+ from .utils import ensure_exists
12
+
13
+
14
+ def build_parser() -> argparse.ArgumentParser:
15
+ parser = argparse.ArgumentParser(prog="devbits", description="Daily development utility CLI toolkit.")
16
+ parser.add_argument("--version", action="version", version="devbits 0.1.0")
17
+ sub = parser.add_subparsers(dest="command", required=True)
18
+
19
+ p = sub.add_parser("clearcache", help="Clear Python cache files under a folder.")
20
+ p.add_argument("folder", type=Path)
21
+ p.add_argument("--all", action="store_true", help="Also remove pytest, mypy, and ruff caches.")
22
+ p.add_argument("--dry-run", action="store_true")
23
+ p.set_defaults(func=cmd_clearcache)
24
+
25
+ p = sub.add_parser("images2video", help="Convert image sequence to MP4 video.")
26
+ p.add_argument("folder", type=Path)
27
+ p.add_argument("-o", "--output", type=Path, default=Path("output.mp4"))
28
+ p.add_argument("--fps", type=float, default=30.0)
29
+ p.add_argument("--pattern", default="*")
30
+ p.set_defaults(func=cmd_images2video)
31
+
32
+ p = sub.add_parser("video2images", help="Extract frames from a video.")
33
+ p.add_argument("video", type=Path)
34
+ p.add_argument("-o", "--output", type=Path, default=Path("frames"))
35
+ p.add_argument("--every", type=int, default=1)
36
+ p.add_argument("--prefix", default="frame")
37
+ p.add_argument("--digits", type=int, default=6)
38
+ p.add_argument("--format", default="jpg")
39
+ p.set_defaults(func=cmd_video2images)
40
+
41
+ p = sub.add_parser("images2gif", help="Convert image sequence to GIF.")
42
+ p.add_argument("folder", type=Path)
43
+ p.add_argument("-o", "--output", type=Path, default=Path("output.gif"))
44
+ p.add_argument("--fps", type=float, default=10.0)
45
+ p.add_argument("--pattern", default="*")
46
+ p.set_defaults(func=cmd_images2gif)
47
+
48
+ p = sub.add_parser("video2gif", help="Convert video to GIF.")
49
+ p.add_argument("video", type=Path)
50
+ p.add_argument("-o", "--output", type=Path, default=Path("output.gif"))
51
+ p.add_argument("--fps", type=float, default=10.0)
52
+ p.add_argument("--start", type=float)
53
+ p.add_argument("--end", type=float)
54
+ p.add_argument("--size", help="Output size, e.g. 640,360")
55
+ p.set_defaults(func=cmd_video2gif)
56
+
57
+ p = sub.add_parser("clipvideo", help="Clip video by seconds or frame indices.")
58
+ p.add_argument("video", type=Path)
59
+ p.add_argument("-o", "--output", type=Path, default=Path("clip.mp4"))
60
+ p.add_argument("--start", type=float)
61
+ p.add_argument("--end", type=float)
62
+ p.add_argument("--start-frame", type=int)
63
+ p.add_argument("--end-frame", type=int)
64
+ p.add_argument("--gui", action="store_true", help="Reserved for future GUI clip selection.")
65
+ p.set_defaults(func=cmd_clipvideo)
66
+
67
+ p = sub.add_parser("resizevideo", help="Resize a video.")
68
+ p.add_argument("video", type=Path)
69
+ p.add_argument("-o", "--output", type=Path, default=Path("resized.mp4"))
70
+ p.add_argument("--size", required=True, help="Output size, e.g. 1280,720")
71
+ p.set_defaults(func=cmd_resizevideo)
72
+
73
+ p = sub.add_parser("image2ico", help="Convert an image to ICO.")
74
+ p.add_argument("image", type=Path)
75
+ p.add_argument("-o", "--output", type=Path, default=Path("image.ico"))
76
+ p.add_argument("--sizes", default="16,32,48,64,128,256")
77
+ p.set_defaults(func=cmd_image2ico)
78
+
79
+ p = sub.add_parser("resizeimage", help="Resize an image.")
80
+ p.add_argument("image", type=Path)
81
+ p.add_argument("-o", "--output", type=Path, default=Path("resized.jpg"))
82
+ p.add_argument("--size", required=True)
83
+ p.add_argument("--no-keep-ratio", action="store_true")
84
+ p.set_defaults(func=cmd_resizeimage)
85
+
86
+ p = sub.add_parser("batchimages", help="Batch resize or convert images.")
87
+ p.add_argument("folder", type=Path)
88
+ p.add_argument("-o", "--output", type=Path, required=True)
89
+ p.add_argument("--size")
90
+ p.add_argument("--format")
91
+ p.set_defaults(func=cmd_batchimages)
92
+
93
+ p = sub.add_parser("checkimages", help="Check broken image files.")
94
+ p.add_argument("folder", type=Path)
95
+ p.add_argument("--recursive", action="store_true")
96
+ p.add_argument("--remove-broken", action="store_true")
97
+ p.set_defaults(func=cmd_checkimages)
98
+
99
+ p = sub.add_parser("contactsheet", help="Create a contact sheet from images.")
100
+ p.add_argument("folder", type=Path)
101
+ p.add_argument("-o", "--output", type=Path, default=Path("sheet.jpg"))
102
+ p.add_argument("--cols", type=int, default=5)
103
+ p.add_argument("--thumb-size", default="256,256")
104
+ p.add_argument("--labels", action="store_true")
105
+ p.set_defaults(func=cmd_contactsheet)
106
+
107
+ p = sub.add_parser("tree", help="Print project tree.")
108
+ p.add_argument("folder", type=Path, nargs="?", default=Path("."))
109
+ p.add_argument("--depth", type=int, default=3)
110
+ p.set_defaults(func=cmd_tree)
111
+
112
+ p = sub.add_parser("size", help="Show top-level folder sizes.")
113
+ p.add_argument("folder", type=Path, nargs="?", default=Path("."))
114
+ p.add_argument("--top", type=int, default=20)
115
+ p.set_defaults(func=cmd_size)
116
+
117
+ p = sub.add_parser("renamefiles", help="Batch rename files in a folder.")
118
+ p.add_argument("folder", type=Path)
119
+ p.add_argument("--prefix", default="file")
120
+ p.add_argument("--digits", type=int, default=6)
121
+ p.add_argument("--start", type=int, default=1)
122
+ p.add_argument("--dry-run", action="store_true")
123
+ p.set_defaults(func=cmd_renamefiles)
124
+
125
+ p = sub.add_parser("samplefiles", help="Copy first N files into another folder.")
126
+ p.add_argument("folder", type=Path)
127
+ p.add_argument("-o", "--output", type=Path, required=True)
128
+ p.add_argument("--num", type=int, required=True)
129
+ p.add_argument("--move", action="store_true")
130
+ p.set_defaults(func=cmd_samplefiles)
131
+ return parser
132
+
133
+
134
+ def cmd_clearcache(args: argparse.Namespace) -> None:
135
+ targets = clear_cache(ensure_exists(args.folder), include_extra=args.all, dry_run=args.dry_run)
136
+ for target in targets:
137
+ print(target)
138
+ print(f"{'Found' if args.dry_run else 'Removed'} {len(targets)} cache item(s).")
139
+
140
+
141
+ def cmd_images2video(args: argparse.Namespace) -> None:
142
+ print(images_to_video(ensure_exists(args.folder), args.output, args.fps, args.pattern))
143
+
144
+
145
+ def cmd_video2images(args: argparse.Namespace) -> None:
146
+ outputs = video_to_images(ensure_exists(args.video), args.output, args.every, args.prefix, args.digits, args.format)
147
+ print(f"Saved {len(outputs)} frame(s) to {args.output}")
148
+
149
+
150
+ def cmd_images2gif(args: argparse.Namespace) -> None:
151
+ print(images_to_gif(ensure_exists(args.folder), args.output, args.fps, args.pattern))
152
+
153
+
154
+ def cmd_video2gif(args: argparse.Namespace) -> None:
155
+ print(video_to_gif(ensure_exists(args.video), args.output, args.fps, args.start, args.end, args.size))
156
+
157
+
158
+ def cmd_clipvideo(args: argparse.Namespace) -> None:
159
+ if args.gui:
160
+ print("Warning: --gui is reserved for a future interactive clip selector; using CLI options now.")
161
+ print(clip_video(ensure_exists(args.video), args.output, args.start, args.end, args.start_frame, args.end_frame))
162
+
163
+
164
+ def cmd_resizevideo(args: argparse.Namespace) -> None:
165
+ print(resize_video(ensure_exists(args.video), args.output, args.size))
166
+
167
+
168
+ def cmd_image2ico(args: argparse.Namespace) -> None:
169
+ print(image_to_ico(ensure_exists(args.image), args.output, args.sizes))
170
+
171
+
172
+ def cmd_resizeimage(args: argparse.Namespace) -> None:
173
+ print(resize_image(ensure_exists(args.image), args.output, args.size, not args.no_keep_ratio))
174
+
175
+
176
+ def cmd_batchimages(args: argparse.Namespace) -> None:
177
+ outputs = batch_images(ensure_exists(args.folder), args.output, args.size, args.format)
178
+ print(f"Saved {len(outputs)} image(s) to {args.output}")
179
+
180
+
181
+ def cmd_checkimages(args: argparse.Namespace) -> None:
182
+ broken = check_images(ensure_exists(args.folder), args.recursive, args.remove_broken)
183
+ if broken:
184
+ print("Broken images:")
185
+ for path in broken:
186
+ print(path)
187
+ print(f"Found {len(broken)} broken image(s).")
188
+
189
+
190
+ def cmd_contactsheet(args: argparse.Namespace) -> None:
191
+ print(contact_sheet(ensure_exists(args.folder), args.output, args.cols, args.thumb_size, labels=args.labels))
192
+
193
+
194
+ def cmd_tree(args: argparse.Namespace) -> None:
195
+ for line in print_tree(ensure_exists(args.folder), args.depth):
196
+ print(line)
197
+
198
+
199
+ def cmd_size(args: argparse.Namespace) -> None:
200
+ for path, _, size_text in top_sizes(ensure_exists(args.folder), args.top):
201
+ print(f"{size_text:>10} {path}")
202
+
203
+
204
+ def cmd_renamefiles(args: argparse.Namespace) -> None:
205
+ mappings = rename_files(ensure_exists(args.folder), args.prefix, args.digits, args.start, args.dry_run)
206
+ for old, new in mappings:
207
+ print(f"{old.name} -> {new.name}")
208
+ print(f"{'Planned' if args.dry_run else 'Renamed'} {len(mappings)} file(s).")
209
+
210
+
211
+ def cmd_samplefiles(args: argparse.Namespace) -> None:
212
+ outputs = sample_files(ensure_exists(args.folder), args.output, args.num, copy=not args.move)
213
+ print(f"Saved {len(outputs)} file(s) to {args.output}")
214
+
215
+
216
+ def main(argv: list[str] | None = None) -> int:
217
+ parser = build_parser()
218
+ args = parser.parse_args(argv)
219
+ try:
220
+ args.func(args)
221
+ except Exception as exc:
222
+ print(f"Error: {exc}", file=sys.stderr)
223
+ return 1
224
+ return 0
225
+
226
+
227
+ if __name__ == "__main__":
228
+ raise SystemExit(main())