lsum 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.
lsum-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: lsum
3
+ Version: 0.1.0
4
+ Summary: Directory summary tool
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: click>=8.3.2
8
+ Requires-Dist: flit-core>=3.12.0
9
+ Requires-Dist: pathspec>=0.12.1
10
+ Requires-Dist: python-magic>=0.4.27
11
+ Requires-Dist: rich>=15.0.0
12
+
13
+ # 📂 lsum
14
+
15
+ [![PyPI version](https://img.shields.io/pypi/v/lsum.svg?color=blue)](https://pypi.org/project/lsum/)
16
+ [![Python Version](https://img.shields.io/badge/python-3.12+-blue.svg)](https://python.org)
17
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
18
+ [![Code Style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
19
+
20
+ **lsum** (List Summary) is a high-performance, visually rich CLI directory analysis tool that transforms standard file listings into actionable intelligence.
21
+
22
+ ---
23
+
24
+ ### ⚡ TL;DR
25
+
26
+ `lsum` is `ls` on steroids. It doesn't just list files; it **categorizes**, **counts**, and **visualizes** your directory's distribution by MIME types, extensions, and metadata—all while respecting your `.gitignore` rules.
27
+
28
+ ---
29
+
30
+ ### 🤔 Why lsum?
31
+
32
+ Standard tools like `ls` or `tree` are great for finding files, but they fail to answer higher-level questions about your workspace. `lsum` was built to fill that gap:
33
+
34
+ * **Audit Your Assets:** Instantly see how many gigabytes of images vs. text files you have.
35
+ * **Visualize Structure:** Group files into elegant, color-coded panels based on their actual content (MIME) rather than just extensions.
36
+ * **Clean Analysis:** Use the `--gitignore` flag to strip out `node_modules`, build artifacts, and logs, focusing only on the code that matters.
37
+ * **Recursive Intelligence:** Understand the composition of entire project trees in a single, formatted view.
38
+
39
+ ---
40
+
41
+ ### 🚀 Installation
42
+
43
+ #### 📦 From PyPI (Recommended)
44
+
45
+ Install using [uv](https://github.com/astral-sh/uv) for the best experience:
46
+
47
+ ```bash
48
+ uv tool install lsum
49
+ ```
50
+
51
+ Or with standard pip:
52
+
53
+ ```bash
54
+ pip install lsum
55
+ ```
56
+
57
+ #### 🛠️ Building From Source
58
+
59
+ Perfect for developers who want the latest features:
60
+
61
+ ```bash
62
+ # Clone the repository
63
+ git clone https://github.com/Debajyati/lsum.git
64
+ cd lsum
65
+
66
+ # Install dependencies and build
67
+ uv pip install -e .
68
+ ```
69
+
70
+ ---
71
+
72
+ ### 🛠️ Usage Examples
73
+
74
+ #### 1. Basic Listing
75
+
76
+ A clean, tabular view of your current directory:
77
+
78
+ ```bash
79
+ lsum
80
+ ```
81
+
82
+ #### 2. Group by MIME Type (with Icons)
83
+
84
+ See your files grouped by their actual content type (e.g., Image, Video, Text):
85
+
86
+ ```bash
87
+ lsum --group
88
+ ```
89
+
90
+ #### 3. Respect Gitignore
91
+
92
+ Exclude build artifacts and ignored files for a "clean" summary:
93
+
94
+ ```bash
95
+ lsum . --gitignore --count
96
+ ```
97
+
98
+ #### 4. Recursive Extension Summary
99
+
100
+ Analyze every file in your project tree, grouped by extension:
101
+
102
+ ```bash
103
+ lsum . --recursive --group-extension
104
+ ```
105
+
106
+ #### 5. Advanced Sorting & Filtering
107
+
108
+ Find all `.txt` files and sort them by size:
109
+
110
+ ```bash
111
+ lsum --filter-extension .txt --sort size
112
+ ```
113
+
114
+ ---
115
+
116
+ ### ⌨️ CLI Options
117
+
118
+ | Option | Shorthand | Description |
119
+ |:------------------- |:--------- |:---------------------------------------------------- |
120
+ | `--count` | `-c` | Count files/directories in groups or total. |
121
+ | `--group` | `-g` | Group files by MIME type. |
122
+ | `--group-extension` | `-ge` | Group files by file extension. |
123
+ | `--gitignore` | `-gi` | Respect `.gitignore` rules (excludes ignored files). |
124
+ | `--recursive` | `-r` | Perform operations on all subdirectories. |
125
+ | `--sort` | `-s` | Sort by `name`, `size`, or `date`. |
126
+ | `--filter` | `-f` | Filter by a specific MIME type (e.g., `image/jpeg`). |
127
+
128
+ ---
129
+
130
+ ### 📄 License
131
+
132
+ Distributed under the MIT License. See `LICENSE` for more information.
133
+
134
+ ---
135
+
136
+ *Built with ❤️ using Python and Rich.*
137
+
lsum-0.1.0/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # 📂 lsum
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/lsum.svg?color=blue)](https://pypi.org/project/lsum/)
4
+ [![Python Version](https://img.shields.io/badge/python-3.12+-blue.svg)](https://python.org)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Code Style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
7
+
8
+ **lsum** (List Summary) is a high-performance, visually rich CLI directory analysis tool that transforms standard file listings into actionable intelligence.
9
+
10
+ ---
11
+
12
+ ### ⚡ TL;DR
13
+
14
+ `lsum` is `ls` on steroids. It doesn't just list files; it **categorizes**, **counts**, and **visualizes** your directory's distribution by MIME types, extensions, and metadata—all while respecting your `.gitignore` rules.
15
+
16
+ ---
17
+
18
+ ### 🤔 Why lsum?
19
+
20
+ Standard tools like `ls` or `tree` are great for finding files, but they fail to answer higher-level questions about your workspace. `lsum` was built to fill that gap:
21
+
22
+ * **Audit Your Assets:** Instantly see how many gigabytes of images vs. text files you have.
23
+ * **Visualize Structure:** Group files into elegant, color-coded panels based on their actual content (MIME) rather than just extensions.
24
+ * **Clean Analysis:** Use the `--gitignore` flag to strip out `node_modules`, build artifacts, and logs, focusing only on the code that matters.
25
+ * **Recursive Intelligence:** Understand the composition of entire project trees in a single, formatted view.
26
+
27
+ ---
28
+
29
+ ### 🚀 Installation
30
+
31
+ #### 📦 From PyPI (Recommended)
32
+
33
+ Install using [uv](https://github.com/astral-sh/uv) for the best experience:
34
+
35
+ ```bash
36
+ uv tool install lsum
37
+ ```
38
+
39
+ Or with standard pip:
40
+
41
+ ```bash
42
+ pip install lsum
43
+ ```
44
+
45
+ #### 🛠️ Building From Source
46
+
47
+ Perfect for developers who want the latest features:
48
+
49
+ ```bash
50
+ # Clone the repository
51
+ git clone https://github.com/Debajyati/lsum.git
52
+ cd lsum
53
+
54
+ # Install dependencies and build
55
+ uv pip install -e .
56
+ ```
57
+
58
+ ---
59
+
60
+ ### 🛠️ Usage Examples
61
+
62
+ #### 1. Basic Listing
63
+
64
+ A clean, tabular view of your current directory:
65
+
66
+ ```bash
67
+ lsum
68
+ ```
69
+
70
+ #### 2. Group by MIME Type (with Icons)
71
+
72
+ See your files grouped by their actual content type (e.g., Image, Video, Text):
73
+
74
+ ```bash
75
+ lsum --group
76
+ ```
77
+
78
+ #### 3. Respect Gitignore
79
+
80
+ Exclude build artifacts and ignored files for a "clean" summary:
81
+
82
+ ```bash
83
+ lsum . --gitignore --count
84
+ ```
85
+
86
+ #### 4. Recursive Extension Summary
87
+
88
+ Analyze every file in your project tree, grouped by extension:
89
+
90
+ ```bash
91
+ lsum . --recursive --group-extension
92
+ ```
93
+
94
+ #### 5. Advanced Sorting & Filtering
95
+
96
+ Find all `.txt` files and sort them by size:
97
+
98
+ ```bash
99
+ lsum --filter-extension .txt --sort size
100
+ ```
101
+
102
+ ---
103
+
104
+ ### ⌨️ CLI Options
105
+
106
+ | Option | Shorthand | Description |
107
+ |:------------------- |:--------- |:---------------------------------------------------- |
108
+ | `--count` | `-c` | Count files/directories in groups or total. |
109
+ | `--group` | `-g` | Group files by MIME type. |
110
+ | `--group-extension` | `-ge` | Group files by file extension. |
111
+ | `--gitignore` | `-gi` | Respect `.gitignore` rules (excludes ignored files). |
112
+ | `--recursive` | `-r` | Perform operations on all subdirectories. |
113
+ | `--sort` | `-s` | Sort by `name`, `size`, or `date`. |
114
+ | `--filter` | `-f` | Filter by a specific MIME type (e.g., `image/jpeg`). |
115
+
116
+ ---
117
+
118
+ ### 📄 License
119
+
120
+ Distributed under the MIT License. See `LICENSE` for more information.
121
+
122
+ ---
123
+
124
+ *Built with ❤️ using Python and Rich.*
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "lsum"
3
+ version = "0.1.0"
4
+ description = "Directory summary tool"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "click>=8.3.2",
9
+ "flit-core>=3.12.0",
10
+ "pathspec>=0.12.1",
11
+ "python-magic>=0.4.27",
12
+ "rich>=15.0.0",
13
+ ]
14
+
15
+ [project.scripts]
16
+ lsum = "lsum.main:cli"
17
+
18
+ [build-system]
19
+ requires = ["flit_core<4"]
20
+ build-backend = "flit_core.buildapi"
21
+
File without changes
File without changes
@@ -0,0 +1,39 @@
1
+ colormap = {
2
+ "pdf": "red",
3
+ "ppt": "bright_red",
4
+ "pptx": "bright_red",
5
+ "vnd.openxmlformats-officedocument.presentationml.presentation": "bright_red",
6
+ "image": "green",
7
+ "doc": "blue",
8
+ "vnd.openxmlformats-officedocument.wordprocessingml.document": "blue",
9
+ "x-script.python": "bright_cyan",
10
+ "json": "yellow",
11
+ "javascript": "yellow",
12
+ "audio": "cyan",
13
+ "video": "magenta",
14
+ "plain": "white",
15
+ "csv": "bright_green",
16
+ "vnd.openxmlformats-officedocument.spreadsheetml.sheet": "green",
17
+ "zip": "bright_blue",
18
+ "exe": "bright_red",
19
+ }
20
+
21
+ MIME_TYPE_ICONS = {
22
+ "pdf": "📄",
23
+ "ppt": "📊",
24
+ "pptx": "📊",
25
+ "vnd.openxmlformats-officedocument.presentationml.presentation": "📊",
26
+ "image": "🖼️",
27
+ "doc": "📃",
28
+ "vnd.openxmlformats-officedocument.wordprocessingml.document": "📃",
29
+ "json": "🔧",
30
+ "audio": "🎵",
31
+ "video": "🎬",
32
+ "text": "📄",
33
+ "x-script.python": "🐍",
34
+ "javascript": "📜",
35
+ "csv": "📈",
36
+ "vnd.openxmlformats-officedocument.spreadsheetml.sheet": "📈",
37
+ "zip": "🗜️",
38
+ "exe": "⚙️",
39
+ }
@@ -0,0 +1,620 @@
1
+ import os
2
+ from collections import Counter
3
+ from .utils import assoclist, get_gitignore_matcher
4
+ from rich import print
5
+ from rich.table import Table
6
+ from rich.panel import Panel
7
+ from rich.columns import Columns
8
+ from .mime import get_mime_type
9
+ from .constants import MIME_TYPE_ICONS, colormap
10
+
11
+ def count_files(path=".", gitignore=False):
12
+ try:
13
+ matcher = get_gitignore_matcher(path) if gitignore else None
14
+ dirs = 0
15
+ non_dirs = 0
16
+ with os.scandir(path) as files:
17
+ for file in files:
18
+ if matcher and matcher.match_file(file.name + ("/" if file.is_dir() else "")):
19
+ continue
20
+ if file.is_dir():
21
+ dirs += 1
22
+ else:
23
+ non_dirs += 1
24
+ print(f"Directories: {dirs}")
25
+ print(f"Files: {non_dirs}")
26
+ except FileNotFoundError:
27
+ print(
28
+ f"[bold yellow]Error:[/bold yellow] The directory '{path}' does not exist."
29
+ )
30
+ except PermissionError:
31
+ print(
32
+ f"[bold yellow]Error:[/bold yellow] You do not have permission to access '{path}'."
33
+ )
34
+
35
+ def list_files(path=".", gitignore=False):
36
+ try:
37
+ matcher = get_gitignore_matcher(path) if gitignore else None
38
+ dirs = []
39
+ non_dirs = []
40
+ with os.scandir(path) as files:
41
+ for file in files:
42
+ if matcher and matcher.match_file(file.name + ("/" if file.is_dir() else "")):
43
+ continue
44
+ if file.is_dir():
45
+ dirs.append(file.name)
46
+ else:
47
+ non_dirs.append(file.name)
48
+
49
+ dirlen = len(dirs)
50
+ non_dirlen = len(non_dirs)
51
+
52
+ if dirlen < non_dirlen:
53
+ dirs = dirs + [""] * (non_dirlen - dirlen)
54
+ elif non_dirlen < dirlen:
55
+ non_dirs = non_dirs + [""] * (dirlen - non_dirlen)
56
+
57
+ files_assoclist = assoclist(dirs, non_dirs)
58
+ table = Table(
59
+ show_header=True,
60
+ header_style="bold magenta",
61
+ title=f"{path if path != '.' else 'CWD'} Listing",
62
+ title_style="bold underline magenta",
63
+ )
64
+ table.add_column("Index", style="dim", width=6, justify="right")
65
+ table.add_column("Directories", style="bold blue underline", justify="left")
66
+ table.add_column("Files", style="bold green underline", justify="left")
67
+
68
+ index = 1
69
+ for first, second in files_assoclist.__iter__():
70
+ table.add_row(
71
+ str(index), f"{first}/" if len(first) else "", f"{second}"
72
+ )
73
+ index += 1
74
+ print(table)
75
+
76
+ except FileNotFoundError:
77
+ print(
78
+ f"[bold yellow]Error:[/bold yellow] The directory '{path}' does not exist."
79
+ )
80
+ except PermissionError:
81
+ print(
82
+ f"[bold yellow]Error:[/bold yellow] You do not have permission to access '{path}'."
83
+ )
84
+
85
+ def count_files_by_mime_type(path=".", gitignore=False):
86
+ try:
87
+ matcher = get_gitignore_matcher(path) if gitignore else None
88
+ mime_counts = Counter()
89
+ with os.scandir(path) as files:
90
+ for file in files:
91
+ if matcher and matcher.match_file(file.name + ("/" if file.is_dir() else "")):
92
+ continue
93
+ if file.is_file():
94
+ mime_type = get_mime_type(file.path) or "Unknown MIME Type"
95
+ mime_counts[mime_type] += 1
96
+
97
+ table = Table(
98
+ show_header=True,
99
+ header_style="bold magenta",
100
+ title=f"{path if path != '.' else 'CWD'} File Counts by MIME Type",
101
+ title_style="bold underline magenta",
102
+ )
103
+ table.add_column("MIME Type", style="bold red", justify="left")
104
+ table.add_column("Count", style="bold yellow", justify="right")
105
+
106
+ for mime, count in mime_counts.items():
107
+ table.add_row(mime, str(count))
108
+
109
+ print(table)
110
+
111
+ except FileNotFoundError:
112
+ print(
113
+ f"[bold yellow]Error:[/bold yellow] The directory '{path}' does not exist."
114
+ )
115
+ except PermissionError:
116
+ print(
117
+ f"[bold yellow]Error:[/bold yellow] You do not have permission to access '{path}'."
118
+ )
119
+
120
+ # multibox layout with one box for each MIME type, and each box contains a list of files with that MIME type, and the box title is the MIME type and the count of files with that MIME type
121
+ # use colormap and MIME_TYPE_ICONS to color the box title and add an icon to the box title based on the MIME type, if the MIME type is not in the colormap or MIME_TYPE_ICONS, use a default color and icon
122
+ def group_files_by_mime_type(path=".", gitignore=False):
123
+ try:
124
+ matcher = get_gitignore_matcher(path) if gitignore else None
125
+ mime_groups = {}
126
+ with os.scandir(path) as entries:
127
+ for entry in entries:
128
+ if matcher and matcher.match_file(entry.name + ("/" if entry.is_dir() else "")):
129
+ continue
130
+ if entry.is_file():
131
+ mime_type = get_mime_type(entry.path) or "unknown/type"
132
+ mime_groups.setdefault(mime_type, []).append(entry.name)
133
+
134
+ panels = []
135
+ for mime, files in mime_groups.items():
136
+ # Get the prefix (e.g., 'image' from 'image/jpeg')
137
+ prefix, suffix = mime.split("/")[0], mime.split("/")[1]
138
+ color = colormap.get(prefix, colormap.get(suffix, "white"))
139
+ icon = MIME_TYPE_ICONS.get(suffix, MIME_TYPE_ICONS.get(prefix, "📁"))
140
+
141
+ # 1. Join all files into one string first so they don't overwrite each other
142
+ file_list_str = "\n".join([f"[{color}]{f}[/{color}]" for f in files])
143
+
144
+ # 2. Create a Panel (the "Box") for this MIME type
145
+ box_title = f"{icon} {mime} ({len(files)})"
146
+ panels.append(Panel(file_list_str, title=box_title, border_style=color, expand=False))
147
+
148
+ # 3. Use Columns to display boxes side-by-side (or wrapped)
149
+ print(Columns(panels))
150
+
151
+ except FileNotFoundError:
152
+ print(f"[bold red]Error:[/bold red] Directory '{path}' not found.")
153
+ except PermissionError:
154
+ print(f"[bold red]Error:[/bold red] Permission denied for '{path}'.")
155
+
156
+ def count_files_by_extension(path=".", gitignore=False):
157
+ try:
158
+ matcher = get_gitignore_matcher(path) if gitignore else None
159
+ extension_counts = Counter()
160
+ with os.scandir(path) as files:
161
+ for file in files:
162
+ if matcher and matcher.match_file(file.name + ("/" if file.is_dir() else "")):
163
+ continue
164
+ if file.is_file():
165
+ ext = os.path.splitext(file.name)[1].lower() or "No Extension"
166
+ extension_counts[ext] += 1
167
+
168
+ table = Table(
169
+ show_header=True,
170
+ header_style="bold magenta",
171
+ title=f"{path if path != '.' else 'CWD'} File Counts by Extension",
172
+ title_style="bold underline magenta",
173
+ )
174
+ table.add_column("Extension", style="bold red", justify="left")
175
+ table.add_column("Count", style="bold yellow", justify="right")
176
+
177
+ for ext, count in extension_counts.items():
178
+ table.add_row(ext, str(count))
179
+
180
+ print(table)
181
+
182
+ except FileNotFoundError:
183
+ print(
184
+ f"[bold yellow]Error:[/bold yellow] The directory '{path}' does not exist."
185
+ )
186
+ except PermissionError:
187
+ print(
188
+ f"[bold yellow]Error:[/bold yellow] You do not have permission to access '{path}'."
189
+ )
190
+
191
+ def group_files_by_extension(path=".", gitignore=False):
192
+ try:
193
+ matcher = get_gitignore_matcher(path) if gitignore else None
194
+ extension_groups = {}
195
+ with os.scandir(path) as files:
196
+ for file in files:
197
+ if matcher and matcher.match_file(file.name + ("/" if file.is_dir() else "")):
198
+ continue
199
+ if file.is_file():
200
+ ext = os.path.splitext(file.name)[1].lower() or "No Extension"
201
+ extension_groups.setdefault(ext, []).append(file.path)
202
+
203
+ table = Table(
204
+ show_header=True,
205
+ header_style="bold magenta",
206
+ title=f"{path if path != '.' else 'CWD'} Files Grouped by Extension",
207
+ title_style="bold underline magenta",
208
+ )
209
+ table.add_column("Index", style="dim", width=6, justify="center")
210
+ table.add_column("Extension", style="bold red", justify="left")
211
+ table.add_column("Files", style="bold yellow", justify="left")
212
+
213
+ # when a new extension is encountered, print the first row with the extension with overline style, and subsequent rows with an empty extension column and no overline style
214
+ for ext, files in extension_groups.items():
215
+ first = True
216
+ index = 1
217
+ for file in files:
218
+ if first:
219
+ table.add_row(f"{index}",ext, file)
220
+ first = False
221
+ index += 1
222
+ else:
223
+ table.add_row(f"{index}","", file)
224
+ index += 1
225
+ table.add_row("") # add an empty row after each extension group for better readability
226
+
227
+ print(table)
228
+
229
+ except FileNotFoundError:
230
+ print(
231
+ f"[bold yellow]Error:[/bold yellow] The directory '{path}' does not exist."
232
+ )
233
+ except PermissionError:
234
+ print(
235
+ f"[bold yellow]Error:[/bold yellow] You do not have permission to access '{path}'."
236
+ )
237
+
238
+ def recursive_list_files(path=".", gitignore=False):
239
+ try:
240
+ matcher = get_gitignore_matcher(path) if gitignore else None
241
+ table = Table(
242
+ show_header=True,
243
+ header_style="bold magenta",
244
+ title=f"{path if path != '.' else 'CWD'} Recursive Listing",
245
+ title_style="bold underline magenta",
246
+ )
247
+ table.add_column("Index", style="dim", width=6, justify="right")
248
+ table.add_column("Path", style="bold cyan", justify="left")
249
+
250
+ index = 1
251
+ for root, dirs, files in os.walk(path):
252
+ if matcher:
253
+ rel_root = os.path.relpath(root, path)
254
+ if rel_root == ".":
255
+ rel_root = ""
256
+ dirs[:] = [d for d in dirs if not matcher.match_file(os.path.join(rel_root, d) + "/")]
257
+ files = [f for f in files if not matcher.match_file(os.path.join(rel_root, f))]
258
+
259
+ for name in dirs + files:
260
+ full_path = os.path.join(root, name)
261
+ table.add_row(str(index), full_path)
262
+ index += 1
263
+
264
+ print(table)
265
+
266
+ except FileNotFoundError:
267
+ print(
268
+ f"[bold yellow]Error:[/bold yellow] The directory '{path}' does not exist."
269
+ )
270
+ except PermissionError:
271
+ print(
272
+ f"[bold yellow]Error:[/bold yellow] You do not have permission to access '{path}'."
273
+ )
274
+
275
+ def recursive_count_files(path=".", gitignore=False):
276
+ try:
277
+ matcher = get_gitignore_matcher(path) if gitignore else None
278
+ file_count = 0
279
+ dir_count = 0
280
+ for root, dirs, files in os.walk(path):
281
+ if matcher:
282
+ rel_root = os.path.relpath(root, path)
283
+ if rel_root == ".":
284
+ rel_root = ""
285
+ dirs[:] = [d for d in dirs if not matcher.match_file(os.path.join(rel_root, d) + "/")]
286
+ files = [f for f in files if not matcher.match_file(os.path.join(rel_root, f))]
287
+
288
+ file_count += len(files)
289
+ dir_count += len(dirs)
290
+
291
+ print(f"Total Directories: {dir_count}")
292
+ print(f"Total Files: {file_count}")
293
+
294
+ except FileNotFoundError:
295
+ print(
296
+ f"[bold yellow]Error:[/bold yellow] The directory '{path}' does not exist."
297
+ )
298
+ except PermissionError:
299
+ print(
300
+ f"[bold yellow]Error:[/bold yellow] You do not have permission to access '{path}'."
301
+ )
302
+
303
+ def recursive_count_files_by_mime_type(path=".", gitignore=False):
304
+ try:
305
+ matcher = get_gitignore_matcher(path) if gitignore else None
306
+ mime_counts = Counter()
307
+ for root, dirs, files in os.walk(path):
308
+ if matcher:
309
+ rel_root = os.path.relpath(root, path)
310
+ if rel_root == ".":
311
+ rel_root = ""
312
+ dirs[:] = [d for d in dirs if not matcher.match_file(os.path.join(rel_root, d) + "/")]
313
+ files = [f for f in files if not matcher.match_file(os.path.join(rel_root, f))]
314
+
315
+ for file in files:
316
+ full_path = os.path.join(root, file)
317
+ mime_type = get_mime_type(full_path) or "Unknown MIME Type"
318
+ mime_counts[mime_type] += 1
319
+
320
+ table = Table(
321
+ show_header=True,
322
+ header_style="bold magenta",
323
+ title=f"{path if path != '.' else 'CWD'} Recursive File Counts by MIME Type",
324
+ title_style="bold underline magenta",
325
+ )
326
+ table.add_column("MIME Type", style="bold red", justify="left")
327
+ table.add_column("Count", style="bold yellow", justify="right")
328
+
329
+ for mime, count in mime_counts.items():
330
+ table.add_row(mime, str(count))
331
+
332
+ print(table)
333
+
334
+ except FileNotFoundError:
335
+ print(
336
+ f"[bold yellow]Error:[/bold yellow] The directory '{path}' does not exist."
337
+ )
338
+ except PermissionError:
339
+ print(
340
+ f"[bold yellow]Error:[/bold yellow] You do not have permission to access '{path}'."
341
+ )
342
+
343
+ def recursive_group_files_by_mime_type(path=".", gitignore=False):
344
+ try:
345
+ matcher = get_gitignore_matcher(path) if gitignore else None
346
+ mime_groups = {}
347
+ for root, dirs, files in os.walk(path):
348
+ if matcher:
349
+ rel_root = os.path.relpath(root, path)
350
+ if rel_root == ".":
351
+ rel_root = ""
352
+ dirs[:] = [d for d in dirs if not matcher.match_file(os.path.join(rel_root, d) + "/")]
353
+ files = [f for f in files if not matcher.match_file(os.path.join(rel_root, f))]
354
+
355
+ for file in files:
356
+ full_path = os.path.join(root, file)
357
+ mime_type = get_mime_type(full_path) or "unknown/type"
358
+ mime_groups.setdefault(mime_type, []).append(full_path)
359
+
360
+ panels = []
361
+ for mime, files in mime_groups.items():
362
+ prefix, suffix = mime.split("/")[0], mime.split("/")[1]
363
+ color = colormap.get(prefix, colormap.get(suffix, "white"))
364
+ icon = MIME_TYPE_ICONS.get(suffix, MIME_TYPE_ICONS.get(prefix, "📁"))
365
+ file_list_str = "\n".join([f"[{color}]{f}[/{color}]" for f in files])
366
+ box_title = f"{icon} {mime} ({len(files)})"
367
+ panels.append(Panel(file_list_str, title=box_title, border_style=color, expand=False))
368
+
369
+ print(Columns(panels))
370
+
371
+ except FileNotFoundError:
372
+ print(f"[bold red]Error:[/bold red] Directory '{path}' not found.")
373
+ except PermissionError:
374
+ print(f"[bold red]Error:[/bold red] Permission denied for '{path}'.")
375
+
376
+ def recursive_count_files_by_extension(path=".", gitignore=False):
377
+ try:
378
+ matcher = get_gitignore_matcher(path) if gitignore else None
379
+ extension_counts = Counter()
380
+ for root, dirs, files in os.walk(path):
381
+ if matcher:
382
+ rel_root = os.path.relpath(root, path)
383
+ if rel_root == ".":
384
+ rel_root = ""
385
+ dirs[:] = [d for d in dirs if not matcher.match_file(os.path.join(rel_root, d) + "/")]
386
+ files = [f for f in files if not matcher.match_file(os.path.join(rel_root, f))]
387
+
388
+ for file in files:
389
+ ext = os.path.splitext(file)[1].lower() or "No Extension"
390
+ extension_counts[ext] += 1
391
+
392
+ table = Table(
393
+ show_header=True,
394
+ header_style="bold magenta",
395
+ title=f"{path if path != '.' else 'CWD'} Recursive File Counts by Extension",
396
+ title_style="bold underline magenta",
397
+ )
398
+ table.add_column("Extension", style="bold red", justify="left")
399
+ table.add_column("Count", style="bold yellow", justify="right")
400
+
401
+ for ext, count in extension_counts.items():
402
+ table.add_row(ext, str(count))
403
+
404
+ print(table)
405
+
406
+ except FileNotFoundError:
407
+ print(
408
+ f"[bold yellow]Error:[/bold yellow] The directory '{path}' does not exist."
409
+ )
410
+ except PermissionError:
411
+ print(
412
+ f"[bold yellow]Error:[/bold yellow] You do not have permission to access '{path}'."
413
+ )
414
+
415
+ def recursive_group_files_by_extension(path=".", gitignore=False):
416
+ try:
417
+ matcher = get_gitignore_matcher(path) if gitignore else None
418
+ extension_groups = {}
419
+ for root, dirs, files in os.walk(path):
420
+ if matcher:
421
+ rel_root = os.path.relpath(root, path)
422
+ if rel_root == ".":
423
+ rel_root = ""
424
+ dirs[:] = [d for d in dirs if not matcher.match_file(os.path.join(rel_root, d) + "/")]
425
+ files = [f for f in files if not matcher.match_file(os.path.join(rel_root, f))]
426
+
427
+ for file in files:
428
+ ext = os.path.splitext(file)[1].lower() or "No Extension"
429
+ full_path = os.path.join(root, file)
430
+ extension_groups.setdefault(ext, []).append(full_path)
431
+
432
+ table = Table(
433
+ show_header=True,
434
+ header_style="bold magenta",
435
+ title=f"{path if path != '.' else 'CWD'} Recursive Files Grouped by Extension",
436
+ title_style="bold underline magenta",
437
+ )
438
+ table.add_column("Index", style="dim", width=6, justify="center")
439
+ table.add_column("Extension", style="bold red", justify="left")
440
+ table.add_column("Files", style="bold yellow", justify="left")
441
+
442
+ for ext, files in extension_groups.items():
443
+ first = True
444
+ index = 1
445
+ for file in files:
446
+ if first:
447
+ table.add_row(f"{index}",ext, file)
448
+ first = False
449
+ index += 1
450
+ else:
451
+ table.add_row(f"{index}","", file)
452
+ index += 1
453
+ table.add_row("") # add an empty row after each extension group for better readability
454
+
455
+ print(table)
456
+
457
+ except FileNotFoundError:
458
+ print(
459
+ f"[bold yellow]Error:[/bold yellow] The directory '{path}' does not exist."
460
+ )
461
+ except PermissionError:
462
+ print(
463
+ f"[bold yellow]Error:[/bold yellow] You do not have permission to access '{path}'."
464
+ )
465
+
466
+ def filter_files_by_extension(path=".", extension=".txt", gitignore=False):
467
+ try:
468
+ matcher = get_gitignore_matcher(path) if gitignore else None
469
+ table = Table(
470
+ show_header=True,
471
+ header_style="bold magenta",
472
+ title=f"{path if path != '.' else 'CWD'} Files with Extension '{extension}'",
473
+ title_style="bold underline magenta",
474
+ )
475
+ table.add_column("Index", style="dim", width=6, justify="right")
476
+ table.add_column("File Name", style="bold green", justify="left")
477
+
478
+ index = 1
479
+ with os.scandir(path) as files:
480
+ for file in files:
481
+ if matcher and matcher.match_file(file.name + ("/" if file.is_dir() else "")):
482
+ continue
483
+ if file.is_file() and file.name.lower().endswith(extension.lower()):
484
+ table.add_row(str(index), file.name)
485
+ index += 1
486
+
487
+ print(table)
488
+
489
+ except FileNotFoundError:
490
+ print(
491
+ f"[bold yellow]Error:[/bold yellow] The directory '{path}' does not exist."
492
+ )
493
+ except PermissionError:
494
+ print(
495
+ f"[bold yellow]Error:[/bold yellow] You do not have permission to access '{path}'."
496
+ )
497
+
498
+ def filter_files_by_mime_type(path=".", mime_type="text/plain", gitignore=False):
499
+ try:
500
+ matcher = get_gitignore_matcher(path) if gitignore else None
501
+ table = Table(
502
+ show_header=True,
503
+ header_style="bold magenta",
504
+ title=f"{path if path != '.' else 'CWD'} Files with MIME Type '{mime_type}'",
505
+ title_style="bold underline magenta",
506
+ )
507
+ table.add_column("Index", style="dim", width=6, justify="right")
508
+ table.add_column("File Name", style="bold green", justify="left")
509
+
510
+ index = 1
511
+ with os.scandir(path) as files:
512
+ for file in files:
513
+ if matcher and matcher.match_file(file.name + ("/" if file.is_dir() else "")):
514
+ continue
515
+ if file.is_file():
516
+ file_mime_type = get_mime_type(file.path)
517
+ if file_mime_type == mime_type:
518
+ table.add_row(str(index), file.name)
519
+ index += 1
520
+
521
+ print(table)
522
+
523
+ except FileNotFoundError:
524
+ print(
525
+ f"[bold yellow]Error:[/bold yellow] The directory '{path}' does not exist."
526
+ )
527
+ except PermissionError:
528
+ print(
529
+ f"[bold yellow]Error:[/bold yellow] You do not have permission to access '{path}'."
530
+ )
531
+
532
+ def count_filter_files_by_mime_type(path=".", mime_type="text/plain", gitignore=False):
533
+ try:
534
+ matcher = get_gitignore_matcher(path) if gitignore else None
535
+ count = 0
536
+ with os.scandir(path) as files:
537
+ for file in files:
538
+ if matcher and matcher.match_file(file.name + ("/" if file.is_dir() else "")):
539
+ continue
540
+ if file.is_file():
541
+ file_mime_type = get_mime_type(file.path)
542
+ if file_mime_type == mime_type:
543
+ count += 1
544
+
545
+ print(f"Number of files with MIME type '{mime_type}': {count}")
546
+
547
+ except FileNotFoundError:
548
+ print(
549
+ f"[bold yellow]Error:[/bold yellow] The directory '{path}' does not exist."
550
+ )
551
+ except PermissionError:
552
+ print(
553
+ f"[bold yellow]Error:[/bold yellow] You do not have permission to access '{path}'."
554
+ )
555
+
556
+ def count_filter_files_by_extension(path=".", extension=".txt", gitignore=False):
557
+ try:
558
+ matcher = get_gitignore_matcher(path) if gitignore else None
559
+ count = 0
560
+ with os.scandir(path) as files:
561
+ for file in files:
562
+ if matcher and matcher.match_file(file.name + ("/" if file.is_dir() else "")):
563
+ continue
564
+ if file.is_file() and file.name.lower().endswith(extension.lower()):
565
+ count += 1
566
+
567
+ print(f"Number of files with extension '{extension}': {count}")
568
+
569
+ except FileNotFoundError:
570
+ print(
571
+ f"[bold yellow]Error:[/bold yellow] The directory '{path}' does not exist."
572
+ )
573
+ except PermissionError:
574
+ print(
575
+ f"[bold yellow]Error:[/bold yellow] You do not have permission to access '{path}'."
576
+ )
577
+
578
+ def sort_files(path=".", sort_by="name", gitignore=False):
579
+ try:
580
+ matcher = get_gitignore_matcher(path) if gitignore else None
581
+ files_list = []
582
+ with os.scandir(path) as files:
583
+ for file in files:
584
+ if matcher and matcher.match_file(file.name + ("/" if file.is_dir() else "")):
585
+ continue
586
+ if file.is_file():
587
+ if sort_by == "name":
588
+ files_list.append((file.name, file.path))
589
+ elif sort_by == "size":
590
+ files_list.append((file.stat().st_size, file.path))
591
+ elif sort_by == "date":
592
+ files_list.append((file.stat().st_mtime, file.path))
593
+
594
+ files_list.sort(key=lambda x: x[0])
595
+
596
+ table = Table(
597
+ show_header=True,
598
+ header_style="bold magenta",
599
+ title=f"{path if path != '.' else 'CWD'} Files Sorted by {sort_by.capitalize()}",
600
+ title_style="bold underline magenta",
601
+ )
602
+ table.add_column("Index", style="dim", width=6, justify="right")
603
+ table.add_column("File Name", style="bold green", justify="left")
604
+
605
+ index = 1
606
+ for _, file_path in files_list:
607
+ table.add_row(str(index), os.path.basename(file_path))
608
+ index += 1
609
+
610
+ print(table)
611
+
612
+ except FileNotFoundError:
613
+ print(
614
+ f"[bold yellow]Error:[/bold yellow] The directory '{path}' does not exist."
615
+ )
616
+ except PermissionError:
617
+ print(
618
+ f"[bold yellow]Error:[/bold yellow] You do not have permission to access '{path}'."
619
+ )
620
+
@@ -0,0 +1,27 @@
1
+ import os
2
+ import mimetypes
3
+
4
+ def get_mime_type(file_path:str):
5
+ """
6
+ Detects the MIME type of a file.
7
+ - First tries python-magic (content-based detection)
8
+ - Falls back to mimetypes (extension-based detection)
9
+ """
10
+ if not os.path.isfile(file_path):
11
+ raise FileNotFoundError(f"File not found: {file_path}")
12
+
13
+ # Try python-magic if available
14
+ try:
15
+ import magic
16
+ mime = magic.Magic(mime=True)
17
+ mime_type = mime.from_file(file_path)
18
+ if mime_type:
19
+ return mime_type
20
+ except ImportError:
21
+ print("python-magic not installed. Falling back to mimetypes.")
22
+ except Exception as e:
23
+ print(f"python-magic failed: {e}. Falling back to mimetypes.")
24
+
25
+ # Fallback: mimetypes (based on file extension)
26
+ mime_type, _ = mimetypes.guess_type(file_path)
27
+ return mime_type or "Unknown"
@@ -0,0 +1,19 @@
1
+ from typing import Any, Tuple, Optional
2
+ import os
3
+ import pathspec
4
+
5
+ def assoclist(array1:list[Any], array2:list[Any]):
6
+ result:list[Tuple[Any,Any]] = []
7
+
8
+ length = min(len(array1),len(array2))
9
+ for i in range(length):
10
+ result.append((array1[i],array2[i]))
11
+ return result
12
+
13
+ def get_gitignore_matcher(path: str) -> Optional[pathspec.PathSpec]:
14
+ gitignore_path = os.path.join(path, ".gitignore")
15
+ if os.path.exists(gitignore_path):
16
+ with open(gitignore_path, "r") as f:
17
+ spec = pathspec.PathSpec.from_lines("gitwildmatch", f)
18
+ return spec
19
+ return None
@@ -0,0 +1,146 @@
1
+ #! /usr/bin/env python3
2
+ from .lib.ls import (
3
+ count_files_by_extension,
4
+ count_files_by_mime_type,
5
+ group_files_by_mime_type,
6
+ list_files,
7
+ count_files,
8
+ group_files_by_extension,
9
+ recursive_count_files,
10
+ recursive_count_files_by_extension,
11
+ recursive_count_files_by_mime_type,
12
+ recursive_group_files_by_extension,
13
+ recursive_group_files_by_mime_type,
14
+ recursive_list_files,
15
+ filter_files_by_extension,
16
+ filter_files_by_mime_type,
17
+ count_filter_files_by_extension,
18
+ count_filter_files_by_mime_type,
19
+ sort_files,
20
+ )
21
+ import click
22
+
23
+
24
+ @click.command()
25
+ @click.argument("path", default=".", required=False)
26
+ @click.option(
27
+ "--count",
28
+ "-c",
29
+ is_flag=True,
30
+ help="Count the number of files and directories in the specified path. Using it in conjunction with --group or --group-extension will count files within each group instead of the total count. Using it with --filter or --filter-extension will count only the files that match the specified filter criteria.",
31
+ )
32
+ @click.option("--group", "-g", is_flag=True, help="Group files by their MIME type.")
33
+ @click.option(
34
+ "--group-extension",
35
+ "-ge",
36
+ is_flag=True,
37
+ help="Group files by their file extension.",
38
+ )
39
+ @click.option(
40
+ "--group-by",
41
+ "-gb",
42
+ type=click.Choice(["mime", "extension"], case_sensitive=False),
43
+ help="Group files by MIME type or file extension.",
44
+ )
45
+ @click.option(
46
+ "--filter",
47
+ "-f",
48
+ type=str,
49
+ help="Filter files by a specific MIME type (e.g., 'image/jpeg').",
50
+ )
51
+ @click.option(
52
+ "--filter-extension",
53
+ "-fe",
54
+ type=str,
55
+ help="Filter files by a specific file extension (e.g., '.txt').",
56
+ )
57
+ @click.option(
58
+ "--sort",
59
+ "-s",
60
+ type=click.Choice(["name", "size", "date"], case_sensitive=False),
61
+ help="Sort files by name, size, or date.",
62
+ )
63
+ @click.option(
64
+ "--recursive",
65
+ "-r",
66
+ is_flag=True,
67
+ help="Recursively perform the specified operations on all subdirectories within the given path. Must be used in conjunction with other options to specify the desired operations (e.g., counting, grouping). Note: currently, filtering or sorting files is not supported in recursive mode.",
68
+ )
69
+ @click.option(
70
+ "--gitignore",
71
+ "-gi",
72
+ is_flag=True,
73
+ help="Respect the .gitignore file if present in the specified directory and exclude those files and directories mentioned in the file from the search space.",
74
+ )
75
+ def cli(
76
+ path,
77
+ count,
78
+ group,
79
+ group_extension,
80
+ group_by,
81
+ filter,
82
+ filter_extension,
83
+ sort,
84
+ recursive,
85
+ gitignore,
86
+ ):
87
+ if recursive:
88
+ if count and group_by:
89
+ if group_by == "mime":
90
+ recursive_count_files_by_mime_type(path, gitignore=gitignore)
91
+ elif group_by == "extension":
92
+ recursive_count_files_by_extension(path, gitignore=gitignore)
93
+ elif count and not group_by and not group and not group_extension:
94
+ recursive_count_files(path, gitignore=gitignore)
95
+ elif count and group:
96
+ recursive_count_files_by_mime_type(path, gitignore=gitignore)
97
+ elif count and group_extension:
98
+ recursive_count_files_by_extension(path, gitignore=gitignore)
99
+ elif group_extension:
100
+ recursive_group_files_by_extension(path, gitignore=gitignore)
101
+ elif group:
102
+ recursive_group_files_by_mime_type(path, gitignore=gitignore)
103
+ elif group_by:
104
+ if group_by == "mime":
105
+ recursive_group_files_by_mime_type(path, gitignore=gitignore)
106
+ elif group_by == "extension":
107
+ recursive_group_files_by_extension(path, gitignore=gitignore)
108
+ else:
109
+ recursive_list_files(path, gitignore=gitignore)
110
+ else:
111
+ if count and group_by:
112
+ if group_by == "mime":
113
+ count_files_by_mime_type(path, gitignore=gitignore)
114
+ elif group_by == "extension":
115
+ count_files_by_extension(path, gitignore=gitignore)
116
+ elif count and not group_by and not group and not group_extension:
117
+ count_files(path, gitignore=gitignore)
118
+ elif count and group:
119
+ count_files_by_mime_type(path, gitignore=gitignore)
120
+ elif count and group_extension:
121
+ count_files_by_extension(path, gitignore=gitignore)
122
+ elif group_extension:
123
+ group_files_by_extension(path, gitignore=gitignore)
124
+ elif group:
125
+ group_files_by_mime_type(path, gitignore=gitignore)
126
+ elif group_by:
127
+ if group_by == "mime":
128
+ group_files_by_mime_type(path, gitignore=gitignore)
129
+ elif group_by == "extension":
130
+ group_files_by_extension(path, gitignore=gitignore)
131
+ elif count and filter:
132
+ count_filter_files_by_mime_type(path, filter, gitignore=gitignore)
133
+ elif count and filter_extension:
134
+ count_filter_files_by_extension(path, filter_extension, gitignore=gitignore)
135
+ elif filter_extension:
136
+ filter_files_by_extension(path, filter_extension, gitignore=gitignore)
137
+ elif filter:
138
+ filter_files_by_mime_type(path, filter, gitignore=gitignore)
139
+ elif sort:
140
+ sort_files(path, sort, gitignore=gitignore)
141
+ else:
142
+ list_files(path, gitignore=gitignore)
143
+
144
+
145
+ if __name__ == "__main__":
146
+ cli()