treex-cli 0.2.0__cp313-abi3-win_amd64.whl

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.
treex/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ # Copyright (c) 2026 Corey Goldberg
2
+ # SPDX-License-Identifier: MIT
treex/_native.pyd ADDED
Binary file
treex/treex.py ADDED
@@ -0,0 +1,590 @@
1
+ # Copyright (c) 2026 Corey Goldberg
2
+ # SPDX-License-Identifier: MIT
3
+
4
+
5
+ """List directory contents as a tree with file and directory metadata.
6
+
7
+ The directory tree is rendered using Unicode box-drawing characters.
8
+
9
+ When run inside a Git repository, files and directories ignored by Git
10
+ are automatically excluded using applicable `.gitignore` rules. Ignore
11
+ matching is handled natively by Gitoxide (`gix`) and does not require
12
+ Git to be installed. If the directory is not part of a Git repository,
13
+ the filesystem is scanned normally without Git filtering.
14
+ """
15
+
16
+ import argparse
17
+ import fnmatch
18
+ import re
19
+ import sys
20
+ from datetime import datetime
21
+ from importlib.metadata import PackageNotFoundError, version
22
+ from pathlib import Path
23
+
24
+ from ._native import IgnoreMatcher
25
+
26
+ try:
27
+ __version__ = version("treex-cli")
28
+ except PackageNotFoundError:
29
+ __version__ = "unknown"
30
+
31
+
32
+ class GitIgnore:
33
+ """Apply Git ignore rules using Gitoxide when inside a repository."""
34
+
35
+ def __init__(self, directory):
36
+ self.directory = Path(directory).resolve()
37
+ self.enabled = False
38
+ self.repo_root = None
39
+ self.matcher = None
40
+ # Discover the repository and initialize ignore matching
41
+ try:
42
+ self.matcher = IgnoreMatcher(str(self.directory))
43
+ except RuntimeError:
44
+ return
45
+ if self.matcher.repo_root is None:
46
+ self.matcher = None
47
+ return
48
+ # Enable Git support if this is a repository
49
+ self.repo_root = Path(self.matcher.repo_root).resolve()
50
+ self.enabled = True
51
+
52
+ def ignored(self, path):
53
+ """Check if Git considers a path ignored."""
54
+ return bool(self.ignored_many([path]))
55
+
56
+ def ignored_many(self, paths):
57
+ """Check which paths Git considers ignored."""
58
+ if not self.enabled or self.repo_root is None or self.matcher is None:
59
+ return set()
60
+ relative_paths = []
61
+ matcher_paths = []
62
+ for path in paths:
63
+ try:
64
+ relative = path.resolve().relative_to(self.repo_root)
65
+ except (ValueError, OSError):
66
+ continue
67
+ relative_paths.append(relative.as_posix())
68
+ matcher_paths.append((relative.as_posix(), path.is_dir()))
69
+ if not matcher_paths:
70
+ return set()
71
+ results = self.matcher.ignored_many(matcher_paths)
72
+ return {
73
+ relative
74
+ for relative, ignored in zip(relative_paths, results, strict=True)
75
+ if ignored
76
+ }
77
+
78
+
79
+ def _format_size(size):
80
+ """Format bytes as a human-readable size."""
81
+ units = ["B", "KB", "MB", "GB", "TB"]
82
+ for unit in units:
83
+ if size < 1024:
84
+ if unit == "B":
85
+ return f"{size} B"
86
+ return f"{size:.1f} {unit}"
87
+ size /= 1024
88
+ # Anything above 1 TB is shown in TB
89
+ return f"{size:.1f} TB"
90
+
91
+
92
+ def _format_modified(timestamp):
93
+ """Format a modification timestamp."""
94
+ return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M")
95
+
96
+
97
+ def _is_binary_file(path, chunk_size=8192):
98
+ """Check if the file appears to be binary."""
99
+ try:
100
+ with path.open("rb") as f:
101
+ chunk = f.read(chunk_size)
102
+ if not chunk:
103
+ return False
104
+ # Check for NUL bytes
105
+ if b"\x00" in chunk:
106
+ return True
107
+ # Treat files that aren't valid UTF-8 as binary
108
+ try:
109
+ chunk.decode("utf-8")
110
+ except UnicodeDecodeError:
111
+ return True
112
+ else:
113
+ return False
114
+ except OSError:
115
+ return True
116
+
117
+
118
+ def _count_lines(path):
119
+ """Count lines without loading the entire file into memory."""
120
+ try:
121
+ with path.open("rb") as f:
122
+ return sum(1 for _ in f)
123
+ except OSError:
124
+ return None
125
+
126
+
127
+ def _file_metadata(path):
128
+ """Get file metadata."""
129
+ try:
130
+ stat = path.stat()
131
+ size = stat.st_size
132
+ modified = _format_modified(stat.st_mtime)
133
+ except OSError:
134
+ return "?", "[unreadable]", 0, None
135
+ size_text = _format_size(size)
136
+ if _is_binary_file(path):
137
+ return size_text, "[binary]", size, modified
138
+ lines = _count_lines(path)
139
+ if lines is None:
140
+ return size_text, "[unreadable]", size, modified
141
+ return size_text, f"{lines:,} lines", size, modified
142
+
143
+
144
+ def _get_tree_entries(path, gitignore):
145
+ """Get entries to include in the tree."""
146
+ try:
147
+ entries = list(path.iterdir())
148
+ except PermissionError:
149
+ return None
150
+ entries = [entry for entry in entries if not entry.is_symlink()]
151
+ if gitignore is not None:
152
+ entries = [entry for entry in entries if entry.name != ".git"]
153
+ if gitignore.enabled and gitignore.repo_root is not None:
154
+ ignored = gitignore.ignored_many(entries)
155
+ filtered_entries = []
156
+ for entry in entries:
157
+ try:
158
+ relative = entry.resolve().relative_to(gitignore.repo_root)
159
+ except (ValueError, OSError):
160
+ filtered_entries.append(entry)
161
+ continue
162
+ if relative.as_posix() not in ignored:
163
+ filtered_entries.append(entry)
164
+ entries = filtered_entries
165
+ entries.sort(key=lambda p: (p.is_file(), p.name.casefold()))
166
+ return entries
167
+
168
+
169
+ def _matches_pattern(path, pattern):
170
+ """Match a relative path using a case-sensitive glob."""
171
+ return pattern is not None and fnmatch.fnmatchcase(path, pattern)
172
+
173
+
174
+ def _scan_tree(
175
+ path,
176
+ gitignore,
177
+ show_metadata,
178
+ show_modified,
179
+ include_pattern=None,
180
+ exclude_pattern=None,
181
+ include_regex=None,
182
+ exclude_regex=None,
183
+ max_depth=None,
184
+ depth=0,
185
+ root=None,
186
+ ):
187
+ """Scan and filter the tree, returning nodes for rendering.
188
+
189
+ The starting directory has depth 0. Its immediate entries have
190
+ depth 1. Directories at the depth limit are shown without
191
+ descending into them. Include/exclude patterns and regular
192
+ expressions match paths relative to the starting directory.
193
+ Directories are retained only when they contain matching files.
194
+ Directory sizes include only files within the permitted depth
195
+ that pass the include/exclude filters.
196
+ """
197
+ path = path.resolve()
198
+ if root is None:
199
+ root = path
200
+ if max_depth is not None and depth >= max_depth:
201
+ return []
202
+ entries = _get_tree_entries(path, gitignore)
203
+ if entries is None:
204
+ return [
205
+ {
206
+ "name": "[permission denied]",
207
+ "kind": "error",
208
+ "children": [],
209
+ "size": 0,
210
+ "info": None,
211
+ "modified": None,
212
+ "contains_match": False,
213
+ }
214
+ ]
215
+ nodes = []
216
+ has_include = include_pattern is not None or include_regex is not None
217
+ for entry in entries:
218
+ relative_path = entry.relative_to(root).as_posix()
219
+ # Exclusions take precedence over inclusions.
220
+ if _matches_pattern(relative_path, exclude_pattern):
221
+ continue
222
+ if exclude_regex is not None and exclude_regex.search(relative_path):
223
+ continue
224
+ if entry.is_dir():
225
+ entry_depth = depth + 1
226
+ if max_depth is not None and entry_depth >= max_depth:
227
+ children = []
228
+ else:
229
+ children = _scan_tree(
230
+ entry,
231
+ gitignore,
232
+ show_metadata,
233
+ show_modified,
234
+ include_pattern,
235
+ exclude_pattern,
236
+ include_regex,
237
+ exclude_regex,
238
+ max_depth,
239
+ entry_depth,
240
+ root,
241
+ )
242
+ contains_match = any(child["contains_match"] for child in children)
243
+ if has_include and not contains_match:
244
+ continue
245
+ size = (
246
+ sum(child["size"] or 0 for child in children) if show_metadata else None
247
+ )
248
+ nodes.append(
249
+ {
250
+ "name": entry.name,
251
+ "kind": "directory",
252
+ "children": children,
253
+ "size": size,
254
+ "info": None,
255
+ "modified": None,
256
+ "contains_match": contains_match,
257
+ }
258
+ )
259
+ continue
260
+ if not entry.is_file():
261
+ continue
262
+ if include_pattern is not None and not _matches_pattern(
263
+ relative_path,
264
+ include_pattern,
265
+ ):
266
+ continue
267
+ if include_regex is not None and not include_regex.search(relative_path):
268
+ continue
269
+ if show_metadata:
270
+ size_text, info, size, modified = _file_metadata(entry)
271
+ else:
272
+ size_text, info, size, modified = None, None, None, None
273
+ nodes.append(
274
+ {
275
+ "name": entry.name,
276
+ "kind": "file",
277
+ "children": [],
278
+ "size": size,
279
+ "size_text": size_text,
280
+ "info": info,
281
+ "modified": modified if show_modified else None,
282
+ "contains_match": True,
283
+ }
284
+ )
285
+ return nodes
286
+
287
+
288
+ def _collect_stats(nodes, stats):
289
+ """Add node counts and sizes to the summary statistics."""
290
+ for node in nodes:
291
+ if node["kind"] == "directory":
292
+ stats["directories"] += 1
293
+ _collect_stats(node["children"], stats)
294
+ elif node["kind"] == "file":
295
+ stats["files"] += 1
296
+ if stats["total_size"] is not None:
297
+ stats["total_size"] += node["size"]
298
+
299
+
300
+ def _render_tree(nodes, prefix, rows, show_metadata, show_files):
301
+ """Render nodes as tree rows."""
302
+ visible = [node for node in nodes if show_files or node["kind"] != "file"]
303
+ for index, node in enumerate(visible):
304
+ is_last = index == len(visible) - 1
305
+ connector = "└── " if is_last else "├── "
306
+ tree_name = prefix + connector + node["name"]
307
+ if node["kind"] == "error":
308
+ rows.append((tree_name, None))
309
+ continue
310
+ if node["kind"] == "directory":
311
+ metadata = (
312
+ (_format_size(node["size"]), None, None) if show_metadata else None
313
+ )
314
+ rows.append((tree_name, metadata))
315
+ extension = " " if is_last else "│ "
316
+ _render_tree(
317
+ node["children"],
318
+ prefix + extension,
319
+ rows,
320
+ show_metadata,
321
+ show_files,
322
+ )
323
+ elif show_files:
324
+ metadata = None
325
+ if show_metadata:
326
+ metadata = (
327
+ node["size_text"],
328
+ node["info"],
329
+ node["modified"],
330
+ )
331
+ rows.append((tree_name, metadata))
332
+
333
+
334
+ def _collect_tree(
335
+ path,
336
+ prefix,
337
+ stats,
338
+ gitignore,
339
+ rows,
340
+ show_metadata,
341
+ show_modified,
342
+ show_files,
343
+ include_pattern=None,
344
+ exclude_pattern=None,
345
+ include_regex=None,
346
+ exclude_regex=None,
347
+ max_depth=None,
348
+ ):
349
+ """Collect filtered tree rows and update summary statistics."""
350
+ if not show_metadata:
351
+ stats["total_size"] = None
352
+ nodes = _scan_tree(
353
+ Path(path),
354
+ gitignore,
355
+ show_metadata,
356
+ show_modified,
357
+ include_pattern,
358
+ exclude_pattern,
359
+ include_regex,
360
+ exclude_regex,
361
+ max_depth,
362
+ )
363
+ _collect_stats(nodes, stats)
364
+ _render_tree(nodes, prefix, rows, show_metadata, show_files)
365
+ return sum(node["size"] or 0 for node in nodes)
366
+
367
+
368
+ def print_tree(
369
+ directory,
370
+ prefix="",
371
+ stats=None,
372
+ gitignore=None,
373
+ show_metadata=True,
374
+ show_modified=False,
375
+ show_tree=True,
376
+ show_files=True,
377
+ include_pattern=None,
378
+ exclude_pattern=None,
379
+ include_regex=None,
380
+ exclude_regex=None,
381
+ max_depth=None,
382
+ ):
383
+ """Print the directory tree with aligned metadata."""
384
+ if stats is None:
385
+ stats = {"directories": 0, "files": 0, "total_size": 0}
386
+ rows = []
387
+ _collect_tree(
388
+ Path(directory),
389
+ prefix,
390
+ stats,
391
+ gitignore,
392
+ rows,
393
+ show_metadata,
394
+ show_modified,
395
+ show_files,
396
+ include_pattern,
397
+ exclude_pattern,
398
+ include_regex,
399
+ exclude_regex,
400
+ max_depth,
401
+ )
402
+ if show_tree:
403
+ # Align metadata with the longest tree/name
404
+ max_width = max((len(tree_name) for tree_name, _ in rows), default=0)
405
+ for tree_name, metadata in rows:
406
+ if metadata is None:
407
+ print(tree_name)
408
+ continue
409
+ size_text, info, modified = metadata
410
+ output = f"{tree_name:<{max_width}}{size_text:>10}"
411
+ if info is not None:
412
+ output += f" {info:<12}"
413
+ if modified is not None:
414
+ output += f" {modified}"
415
+ print(output)
416
+ return stats
417
+
418
+
419
+ def print_summary(stats):
420
+ """Print a summary of directory/file counts and total size."""
421
+ directories = stats["directories"]
422
+ files = stats["files"]
423
+ total_size = stats["total_size"]
424
+ directory_label = "directory" if directories == 1 else "directories"
425
+ file_label = "file" if files == 1 else "files"
426
+ summary = f"{directories:,} {directory_label} • {files:,} {file_label}"
427
+ if total_size is not None:
428
+ summary += f" • {_format_size(total_size)}"
429
+ print(summary)
430
+
431
+
432
+ def _positive_int(value):
433
+ """Parse a positive integer."""
434
+ try:
435
+ number = int(value)
436
+ except ValueError:
437
+ msg = "must be a positive integer"
438
+ raise argparse.ArgumentTypeError(msg) from None
439
+ if number < 1:
440
+ msg = "must be at least 1"
441
+ raise argparse.ArgumentTypeError(msg)
442
+ return number
443
+
444
+
445
+ def parse_args(argv=None):
446
+ """Parse command-line arguments."""
447
+
448
+ def formatter(prog):
449
+ return argparse.HelpFormatter(
450
+ prog,
451
+ width=80,
452
+ max_help_position=32,
453
+ )
454
+
455
+ parser = argparse.ArgumentParser(
456
+ description="List directory contents as a tree "
457
+ "with file and directory metadata.",
458
+ formatter_class=formatter,
459
+ )
460
+ parser.add_argument(
461
+ "directory",
462
+ nargs="?",
463
+ default=Path(),
464
+ type=Path,
465
+ help="directory to scan (default: current directory)",
466
+ )
467
+ parser.add_argument(
468
+ "-V",
469
+ "--version",
470
+ action="version",
471
+ version=(f"%(prog)s {__version__} "),
472
+ help="show version information and exit",
473
+ )
474
+ parser.add_argument(
475
+ "-a",
476
+ "--all",
477
+ action="store_true",
478
+ help="show all files, including those ignored by Git",
479
+ )
480
+ parser.add_argument(
481
+ "-m",
482
+ "--modified",
483
+ action="store_true",
484
+ help="show file modification times",
485
+ )
486
+ parser.add_argument(
487
+ "-q",
488
+ "--quiet",
489
+ action="store_true",
490
+ help="don't show file metadata",
491
+ )
492
+ parser.add_argument(
493
+ "-d",
494
+ "--dirs",
495
+ action="store_true",
496
+ help="show directories only",
497
+ )
498
+ parser.add_argument(
499
+ "-s",
500
+ "--summary",
501
+ action="store_true",
502
+ help="show summary only",
503
+ )
504
+ parser.add_argument(
505
+ "-i",
506
+ "--include",
507
+ metavar="PATTERN",
508
+ help="include paths matching a glob pattern",
509
+ )
510
+ parser.add_argument(
511
+ "-e",
512
+ "--exclude",
513
+ metavar="PATTERN",
514
+ help="exclude paths matching a glob pattern",
515
+ )
516
+ parser.add_argument(
517
+ "-I",
518
+ "--include-regex",
519
+ metavar="PATTERN",
520
+ help="include paths matching a regular expression",
521
+ )
522
+ parser.add_argument(
523
+ "-E",
524
+ "--exclude-regex",
525
+ metavar="PATTERN",
526
+ help="exclude paths matching a regular expression",
527
+ )
528
+ parser.add_argument(
529
+ "-D",
530
+ "--depth",
531
+ type=_positive_int,
532
+ metavar="DEPTH",
533
+ help="limit levels displayed below starting directory",
534
+ )
535
+ return parser.parse_args(argv)
536
+
537
+
538
+ def main():
539
+ try:
540
+ args = parse_args()
541
+ path = args.directory
542
+ if not path.is_dir():
543
+ print(f"Not a directory: {path}", file=sys.stderr)
544
+ return 1
545
+ try:
546
+ include_regex = (
547
+ re.compile(args.include_regex)
548
+ if args.include_regex is not None
549
+ else None
550
+ )
551
+ exclude_regex = (
552
+ re.compile(args.exclude_regex)
553
+ if args.exclude_regex is not None
554
+ else None
555
+ )
556
+ except re.error as exc:
557
+ print(f"Invalid regular expression: {exc}", file=sys.stderr)
558
+ return 2
559
+ # Only initialize Git support if it will be used
560
+ gitignore = None if args.all else GitIgnore(path)
561
+ show_tree = not args.summary
562
+ show_metadata = not args.quiet
563
+ show_files = not args.dirs
564
+ if show_tree:
565
+ print(path)
566
+ stats = print_tree(
567
+ path,
568
+ gitignore=gitignore,
569
+ show_metadata=show_metadata,
570
+ show_modified=args.modified,
571
+ show_tree=show_tree,
572
+ show_files=show_files,
573
+ include_pattern=args.include,
574
+ exclude_pattern=args.exclude,
575
+ include_regex=include_regex,
576
+ exclude_regex=exclude_regex,
577
+ max_depth=args.depth,
578
+ )
579
+ if show_tree:
580
+ print()
581
+ print_summary(stats)
582
+ except KeyboardInterrupt:
583
+ print("\nInterrupted", file=sys.stderr)
584
+ return 130
585
+ else:
586
+ return 0
587
+
588
+
589
+ if __name__ == "__main__":
590
+ raise SystemExit(main())
@@ -0,0 +1,212 @@
1
+ Metadata-Version: 2.4
2
+ Name: treex-cli
3
+ Version: 0.2.0
4
+ Summary: Command-line directory tree utility with metadata and Git ignore support
5
+ Author: Corey Goldberg
6
+ Maintainer: Corey Goldberg
7
+ License-Expression: MIT
8
+ Project-URL: homepage, https://github.com/cgoldberg/treex
9
+ Project-URL: repository, https://github.com/cgoldberg/treex
10
+ Keywords: tree,cli,directories,files,git,gitignore,gitoxide,metadata,python,rust
11
+ Classifier: Environment :: Console
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Python :: 3.15
18
+ Classifier: Programming Language :: Rust
19
+ Requires-Python: >=3.13
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # treex
25
+
26
+ ## Git-aware directory tree utility
27
+
28
+ Command-line directory tree utility with metadata and Git ignore support.
29
+
30
+ - **Development:** [GitHub][github-repo]
31
+ - **Releases:** [PyPI][pypi-home]
32
+ - **License:** [MIT][mit-license]
33
+ - **Copyright:** © 2026 [Corey Goldberg][github-profile]
34
+
35
+ ----
36
+
37
+ ## About
38
+
39
+ `treex` recursively scans a directory and displays its contents as a tree,
40
+ including file and directory metadata. It respects `.gitignore` rules inside Git
41
+ repositories and provides additional options for filtering and customizing the
42
+ output.
43
+
44
+ ### Features
45
+
46
+ - Respects `.gitignore` rules inside Git repositories
47
+ - Displays human-readable file sizes
48
+ - Displays recursive directory sizes
49
+ - Displays line counts for text files
50
+ - Identifies binary and unreadable files by type
51
+ - Optionally displays modification timestamps
52
+ - Optionally includes or excludes paths using glob patterns
53
+ - Optionally includes or excludes paths using regular expressions
54
+ - Optionally limits the depth of the displayed tree
55
+
56
+ ----
57
+
58
+ ## Status
59
+
60
+ <table>
61
+ <tr>
62
+ <td>Latest Version</td>
63
+ <td>
64
+ <a href="https://pypi.org/project/treex-cli">
65
+ <img src="https://img.shields.io/pypi/v/treex-cli.svg">
66
+ </a>
67
+ </td>
68
+ </tr>
69
+ <tr>
70
+ <td>Tests</td>
71
+ <td>
72
+ <a href="https://github.com/cgoldberg/treex/actions/workflows/test.yml">
73
+ <img src="https://github.com/cgoldberg/treex/actions/workflows/test.yml/badge.svg">
74
+ </a>
75
+ </td>
76
+ </tr>
77
+ <tr>
78
+ <td>Supported Python Versions</td>
79
+ <td>
80
+ <a href="https://pypi.org/project/treex-cli">
81
+ <img src="https://img.shields.io/pypi/pyversions/treex-cli">
82
+ </a>
83
+ </td>
84
+ </tr>
85
+ </table>
86
+
87
+ ----
88
+
89
+ ## Requirements
90
+
91
+ - Python 3.13+
92
+ - UTF-8-compatible terminal
93
+
94
+ ----
95
+
96
+ ## Installation
97
+
98
+ Install `treex` from [PyPI][pypi-home] using your preferred Python package
99
+ manager:
100
+
101
+ - **pip**: `pip install treex-cli`
102
+ - **pipx**: `pipx install treex-cli`
103
+ - **uv**: `uv tool install treex-cli`
104
+
105
+ Prebuilt wheels are available for common platforms. See
106
+ [Platform Support](#platform-support) for details.
107
+
108
+ ----
109
+
110
+ ## Git ignore support
111
+
112
+ `treex` uses [Gitoxide][gitoxide-home] (`gix`), a native Rust implementation of
113
+ Git functionality, to evaluate `.gitignore` rules without invoking the Git
114
+ executable. Git does not need to be installed for ignore matching to work.
115
+
116
+ When the specified directory is inside a Git working tree, `treex` respects
117
+ applicable `.gitignore` rules by default. Outside a Git working tree, Git ignore
118
+ filtering is disabled. Use `--all` to include files ignored by Git.
119
+
120
+ ----
121
+
122
+ ## Usage
123
+
124
+ ```
125
+ usage: treex [-h] [-V] [-a] [-m] [-q] [-d] [-s] [-i PATTERN] [-e PATTERN]
126
+ [-I PATTERN] [-E PATTERN] [-D DEPTH]
127
+ [directory]
128
+
129
+ List directory contents as a tree with file and directory metadata.
130
+
131
+ positional arguments:
132
+ directory directory to scan (default: current directory)
133
+
134
+ options:
135
+ -h, --help show this help message and exit
136
+ -V, --version show version information and exit
137
+ -a, --all show all files, including those ignored by Git
138
+ -m, --modified show file modification times
139
+ -q, --quiet don't show file metadata
140
+ -d, --dirs show directories only
141
+ -s, --summary show summary only
142
+ -i, --include PATTERN include paths matching a glob pattern
143
+ -e, --exclude PATTERN exclude paths matching a glob pattern
144
+ -I, --include-regex PATTERN include paths matching a regular expression
145
+ -E, --exclude-regex PATTERN exclude paths matching a regular expression
146
+ -D, --depth DEPTH limit levels displayed below starting directory
147
+ ```
148
+
149
+ ### Filtering paths
150
+
151
+ Use `--include` and `--exclude` to filter paths by glob pattern:
152
+
153
+ ```
154
+ treex --include '*.py'
155
+ treex --exclude 'tests/*.py'
156
+ treex --include '*.py' --exclude 'test_*.py'
157
+ ```
158
+
159
+ Use `--include-regex` and `--exclude-regex` to filter paths by regular
160
+ expression:
161
+
162
+ ```
163
+ treex --include-regex '^src/'
164
+ treex --exclude-regex '(^|/)tests/'
165
+ treex --include-regex '\.py$' --exclude-regex '(^|/)test_[^/]*\.py$'
166
+ ```
167
+
168
+ Patterns match paths relative to the starting directory, using forward slashes
169
+ as separators. Matching is case-sensitive. Glob patterns use `*` to match any
170
+ characters, including directory separators. Regular expressions use Python's
171
+ regex syntax. Exclusions take precedence over inclusions.
172
+
173
+ Note: quote glob and regex patterns to prevent the shell from interpreting them
174
+ before `treex` receives them.
175
+
176
+ ----
177
+
178
+ ## Screenshot
179
+
180
+ ![Screenshot](https://raw.githubusercontent.com/cgoldberg/treex/refs/heads/main/screenshot.png)
181
+
182
+ ## Platform support
183
+
184
+ Prebuilt wheels are available for Python 3.13+ on the following platforms:
185
+
186
+ | Platform | Architecture |
187
+ | ------------- | ----------------------------- |
188
+ | Linux (glibc) | x86_64, aarch64 |
189
+ | Linux (musl) | x86_64, aarch64 |
190
+ | macOS | Apple Silicon (ARM64) |
191
+ | Windows | 32-bit (x86), 64-bit (x86_64) |
192
+
193
+ These prebuilt [binary wheels][python-wheels] let you install the `treex-cli`
194
+ package without compiling the Rust extension yourself.
195
+
196
+ ### Installing on other platforms
197
+
198
+ If a prebuilt binary is not available for your platform, you can install the
199
+ package from the [source distribution on PyPI][pypi-files] or directly from the
200
+ [source repository][github-repo]. This requires a Rust toolchain.
201
+
202
+ For more information about installing Rust, see the
203
+ [official Rust installation guide][rust-install]
204
+
205
+ [github-profile]: https://github.com/cgoldberg
206
+ [github-repo]: https://github.com/cgoldberg/treex
207
+ [gitoxide-home]: https://rustutils.com/tools/gitoxide
208
+ [mit-license]: https://raw.githubusercontent.com/cgoldberg/treex/refs/heads/main/LICENSE
209
+ [pypi-files]: https://pypi.org/project/treex-cli/#files
210
+ [pypi-home]: https://pypi.org/project/treex-cli
211
+ [python-wheels]: https://packaging.python.org/en/latest/specifications/binary-distribution-format
212
+ [rust-install]: https://www.rust-lang.org/tools/install
@@ -0,0 +1,9 @@
1
+ treex/__init__.py,sha256=Bm3KzqFFQlDEqB02-g_PJumVWHMnYFxon1D4j7Glw6A,69
2
+ treex/_native.pyd,sha256=VuFtEzG35JtgFwR5zL84ZG0WE4gcf4BkKjVPU9Zg6QI,2063360
3
+ treex/treex.py,sha256=ifdeEOraXR81D4Q3vYj1oorxcxZq0EAejrTdyvziDmE,18361
4
+ treex_cli-0.2.0.dist-info/licenses/LICENSE,sha256=wR0s0UvmKKWAHQx2MsbQ1SVq7wYoyBlj9G13YiEBtYU,1092
5
+ treex_cli-0.2.0.dist-info/METADATA,sha256=p5q2gt3ZvYe0mjY4t8cZsezC_qHYxZvOypYqOG-BoXg,7122
6
+ treex_cli-0.2.0.dist-info/WHEEL,sha256=UDpYo6hQfdddzU3J0xB_VgXoPsit6UeN_XKOQCX2bW8,100
7
+ treex_cli-0.2.0.dist-info/entry_points.txt,sha256=O25Pgu70GDYn0X_bgwlRnhCljpzrjLhodZcdOLmsSlk,43
8
+ treex_cli-0.2.0.dist-info/top_level.txt,sha256=Q2lsPiM6enBh4T-ZtRWruXPJPoyJTORjjQNeJtEl-I8,6
9
+ treex_cli-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: false
4
+ Tag: cp313-abi3-win_amd64
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ treex = treex.treex:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Corey Goldberg
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.
@@ -0,0 +1 @@
1
+ treex