treex-cli 0.1.1__tar.gz → 0.1.3__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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: treex-cli
3
- Version: 0.1.1
4
- Summary: List directory contents in a tree format with file metadata.
3
+ Version: 0.1.3
4
+ Summary: List directory contents as a tree with file metadata
5
5
  Author: Corey Goldberg
6
6
  Maintainer: Corey Goldberg
7
7
  License-Expression: MIT
@@ -25,7 +25,7 @@ Dynamic: license-file
25
25
 
26
26
  # treex
27
27
 
28
- ## List directory contents in a tree format with file metadata.
28
+ ## List directory contents as a tree with file metadata
29
29
 
30
30
  - Copyright (c) 2026 [Corey Goldberg][github-profile]
31
31
  - Development: [GitHub][github-repo]
@@ -36,18 +36,14 @@ Dynamic: license-file
36
36
 
37
37
  ## About
38
38
 
39
- `treex` is a command-line utility (similar to [tree][tree-home]) that recursively
40
- scans a directory and prints its contents as a tree using Unicode box-drawing
41
- characters. File names include their human-readable size and, optionally,
42
- modification time. Text files also include their line count, while binary and
43
- unreadable files are marked with their type. When Git is installed, it respects
44
- `.gitignore` rules.
39
+ `treex` is a command-line utility that recursively scans a directory and
40
+ displays its contents as a tree along with file metadata.
45
41
 
46
- Requirements:
47
-
48
- - Python 3.10+
49
- - Git 2.0+ (optional)
50
- - UTF-8-compatible terminal
42
+ - File names show human-readable sizes
43
+ - Text files show line counts
44
+ - Binary and unreadable files show their type
45
+ - Modification timestamps are optional
46
+ - Respects `.gitignore` rules when Git is installed
51
47
 
52
48
  ----
53
49
 
@@ -82,6 +78,14 @@ Requirements:
82
78
 
83
79
  ----
84
80
 
81
+ ## Requirements
82
+
83
+ - Python 3.10+
84
+ - Git 2.0+ (optional)
85
+ - UTF-8-compatible terminal
86
+
87
+ ----
88
+
85
89
  ## Installation
86
90
 
87
91
  Install `treex` from [PyPI][pypi-home] using either `pip` or `pipx`:
@@ -95,9 +99,9 @@ Install `treex` from [PyPI][pypi-home] using either `pip` or `pipx`:
95
99
 
96
100
  ```
97
101
  $ treex --help
98
- usage: treex [-h] [-a] [-m] [-s] [-w N] [directory]
102
+ usage: treex [-h] [-a] [-m] [-s] [directory]
99
103
 
100
- List directory contents in a tree format with file metadata.
104
+ List directory contents as a tree with file metadata.
101
105
 
102
106
  positional arguments:
103
107
  directory directory to scan (default: current directory)
@@ -107,7 +111,6 @@ options:
107
111
  -a, --all show all files (including those ignored by Git)
108
112
  -m, --modified show file modification times
109
113
  -s, --summary show summary only
110
- -w, --width N starting column for file metadata (default: 50)
111
114
  ```
112
115
 
113
116
  ----
@@ -120,4 +123,3 @@ options:
120
123
  [github-repo]: https://github.com/cgoldberg/treex
121
124
  [pypi-home]: https://pypi.org/project/treex-cli
122
125
  [mit-license]: https://raw.githubusercontent.com/cgoldberg/treex/refs/heads/main/LICENSE
123
- [tree-home]: https://oldmanprogrammer.net/source.php?dir=projects/tree
@@ -1,6 +1,6 @@
1
1
  # treex
2
2
 
3
- ## List directory contents in a tree format with file metadata.
3
+ ## List directory contents as a tree with file metadata
4
4
 
5
5
  - Copyright (c) 2026 [Corey Goldberg][github-profile]
6
6
  - Development: [GitHub][github-repo]
@@ -11,18 +11,14 @@
11
11
 
12
12
  ## About
13
13
 
14
- `treex` is a command-line utility (similar to [tree][tree-home]) that recursively
15
- scans a directory and prints its contents as a tree using Unicode box-drawing
16
- characters. File names include their human-readable size and, optionally,
17
- modification time. Text files also include their line count, while binary and
18
- unreadable files are marked with their type. When Git is installed, it respects
19
- `.gitignore` rules.
14
+ `treex` is a command-line utility that recursively scans a directory and
15
+ displays its contents as a tree along with file metadata.
20
16
 
21
- Requirements:
22
-
23
- - Python 3.10+
24
- - Git 2.0+ (optional)
25
- - UTF-8-compatible terminal
17
+ - File names show human-readable sizes
18
+ - Text files show line counts
19
+ - Binary and unreadable files show their type
20
+ - Modification timestamps are optional
21
+ - Respects `.gitignore` rules when Git is installed
26
22
 
27
23
  ----
28
24
 
@@ -57,6 +53,14 @@ Requirements:
57
53
 
58
54
  ----
59
55
 
56
+ ## Requirements
57
+
58
+ - Python 3.10+
59
+ - Git 2.0+ (optional)
60
+ - UTF-8-compatible terminal
61
+
62
+ ----
63
+
60
64
  ## Installation
61
65
 
62
66
  Install `treex` from [PyPI][pypi-home] using either `pip` or `pipx`:
@@ -70,9 +74,9 @@ Install `treex` from [PyPI][pypi-home] using either `pip` or `pipx`:
70
74
 
71
75
  ```
72
76
  $ treex --help
73
- usage: treex [-h] [-a] [-m] [-s] [-w N] [directory]
77
+ usage: treex [-h] [-a] [-m] [-s] [directory]
74
78
 
75
- List directory contents in a tree format with file metadata.
79
+ List directory contents as a tree with file metadata.
76
80
 
77
81
  positional arguments:
78
82
  directory directory to scan (default: current directory)
@@ -82,7 +86,6 @@ options:
82
86
  -a, --all show all files (including those ignored by Git)
83
87
  -m, --modified show file modification times
84
88
  -s, --summary show summary only
85
- -w, --width N starting column for file metadata (default: 50)
86
89
  ```
87
90
 
88
91
  ----
@@ -95,4 +98,3 @@ options:
95
98
  [github-repo]: https://github.com/cgoldberg/treex
96
99
  [pypi-home]: https://pypi.org/project/treex-cli
97
100
  [mit-license]: https://raw.githubusercontent.com/cgoldberg/treex/refs/heads/main/LICENSE
98
- [tree-home]: https://oldmanprogrammer.net/source.php?dir=projects/tree
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "treex-cli"
7
- version = "0.1.1"
8
- description="List directory contents in a tree format with file metadata."
7
+ version = "0.1.3"
8
+ description="List directory contents as a tree with file metadata"
9
9
  license = "MIT"
10
10
  license-files = ["LICENSE"]
11
11
  authors = [{name = "Corey Goldberg"}]
@@ -68,7 +68,7 @@ ignore = [
68
68
  fixable = ["ALL"]
69
69
 
70
70
  [tool.ruff.lint.per-file-ignores]
71
- "**/test_*.py" = ["S101", "S108"]
71
+ "**/test_*.py" = ["S101", "S108", "SLF001"]
72
72
 
73
73
  [tool.ruff.format]
74
74
  docstring-code-format = true
@@ -2,21 +2,14 @@
2
2
  # SPDX-License-Identifier: MIT
3
3
 
4
4
 
5
- """List directory contents in a tree-like format with file metadata.
5
+ """List directory contents as a tree with file metadata.
6
6
 
7
- The directory tree is rendered using Unicode box-drawing characters. File names are
8
- displayed with their human-readable size and, optionally, modification time. Text
9
- files also include their line count. Binary and unreadable files are marked with
10
- their type.
7
+ The directory tree is rendered using Unicode box-drawing characters.
11
8
 
12
9
  When run inside a Git repository, files and directories ignored by Git
13
10
  are automatically excluded using Git's own ignore rules. If Git is not
14
11
  installed, or the directory is not part of a Git repository, the
15
12
  filesystem is scanned normally without Git filtering.
16
-
17
- Use --all to disable Git ignore filtering and show all files.
18
- Use --width to control the column at which file metadata starts.
19
- Use --modified to show file modification times.
20
13
  """
21
14
 
22
15
  import argparse
@@ -25,8 +18,6 @@ from contextlib import suppress
25
18
  from datetime import datetime
26
19
  from pathlib import Path
27
20
 
28
- DEFAULT_METADATA_COLUMN = 50
29
-
30
21
 
31
22
  class GitIgnore:
32
23
  """Use Git's ignore rules when available."""
@@ -69,7 +60,7 @@ class GitIgnore:
69
60
  return result.returncode == 0
70
61
 
71
62
 
72
- def format_size(size): # noqa: RET503
63
+ def _format_size(size): # noqa: RET503
73
64
  """Format bytes as a human-readable size."""
74
65
  units = ["B", "KB", "MB", "GB", "TB"]
75
66
  for unit in units:
@@ -80,12 +71,12 @@ def format_size(size): # noqa: RET503
80
71
  size /= 1024
81
72
 
82
73
 
83
- def format_modified(timestamp):
74
+ def _format_modified(timestamp):
84
75
  """Format a modification timestamp."""
85
76
  return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M")
86
77
 
87
78
 
88
- def is_binary_file(path, chunk_size=8192):
79
+ def _is_binary_file(path, chunk_size=8192):
89
80
  """Check if the file appears to be binary."""
90
81
  try:
91
82
  with path.open("rb") as f:
@@ -106,7 +97,7 @@ def is_binary_file(path, chunk_size=8192):
106
97
  return True
107
98
 
108
99
 
109
- def count_lines(path):
100
+ def _count_lines(path):
110
101
  """Count lines without loading the entire file into memory."""
111
102
  try:
112
103
  with path.open("rb") as f:
@@ -115,93 +106,115 @@ def count_lines(path):
115
106
  return None
116
107
 
117
108
 
118
- def file_info(path):
119
- """Get file info."""
109
+ def _file_metadata(path):
110
+ """Get file metadata."""
120
111
  try:
121
112
  stat = path.stat()
122
113
  size = stat.st_size
123
- modified = format_modified(stat.st_mtime)
114
+ modified = _format_modified(stat.st_mtime)
124
115
  except OSError:
125
116
  return "?", "[unreadable]", 0, None
126
- size_text = format_size(size)
127
- if is_binary_file(path):
117
+ size_text = _format_size(size)
118
+ if _is_binary_file(path):
128
119
  return size_text, "[binary]", size, modified
129
- lines = count_lines(path)
120
+ lines = _count_lines(path)
130
121
  if lines is None:
131
122
  return size_text, "[unreadable]", size, modified
132
123
  return size_text, f"{lines:,} lines", size, modified
133
124
 
134
125
 
135
- def print_tree(
136
- directory,
137
- prefix="",
138
- stats=None,
139
- gitignore=None,
140
- width=DEFAULT_METADATA_COLUMN,
141
- show_modified=False,
142
- show_tree=True,
143
- ):
144
- """Recursively print the directory tree."""
145
-
146
- def display(message):
147
- if show_tree:
148
- print(message)
149
-
150
- if stats is None:
151
- stats = {
152
- "directories": 0,
153
- "files": 0,
154
- "total_size": 0,
155
- }
156
- path = Path(directory)
126
+ def _get_tree_entries(path, gitignore):
127
+ """Get entries to include in the tree."""
157
128
  try:
158
129
  entries = list(path.iterdir())
159
130
  except PermissionError:
160
- display(prefix + "└── [permission denied]")
161
- return stats
131
+ return None
162
132
  # Filter ignored entries and Git's internal directory
163
- visible_entries = [
133
+ entries = [
164
134
  entry
165
135
  for entry in entries
166
- if not gitignore or (entry.name != ".git" and not gitignore.ignored(entry))
136
+ if not entry.is_symlink()
137
+ and (not gitignore or (entry.name != ".git" and not gitignore.ignored(entry)))
167
138
  ]
168
- # Sort directories before files, group symlinks
169
- # by target type, and sort names case-insensitive.
170
- visible_entries.sort(key=lambda p: (p.is_file(), p.name.casefold()))
171
- for index, entry in enumerate(visible_entries):
172
- is_last = index == len(visible_entries) - 1
139
+ # Group directories first and sort names case-insensitive
140
+ entries.sort(key=lambda p: (p.is_file(), p.name.casefold()))
141
+ return entries
142
+
143
+
144
+ def _collect_tree(
145
+ path,
146
+ prefix,
147
+ stats,
148
+ gitignore,
149
+ rows,
150
+ show_modified,
151
+ ):
152
+ """Walk the tree once and collect the output rows."""
153
+ entries = _get_tree_entries(path, gitignore)
154
+ if entries is None:
155
+ rows.append((prefix + "└── [permission denied]", None))
156
+ return
157
+ for index, entry in enumerate(entries):
158
+ is_last = index == len(entries) - 1
173
159
  connector = "└── " if is_last else "├── "
160
+ tree_name = prefix + connector + entry.name
174
161
  if entry.is_dir():
175
162
  stats["directories"] += 1
176
- display(f"{prefix}{connector}{entry.name}")
163
+ rows.append((tree_name, None))
164
+ # Indent nested entries, preserving the tree's vertical branch
177
165
  extension = " " if is_last else "│ "
178
- print_tree(
166
+ _collect_tree(
179
167
  entry,
180
168
  prefix + extension,
181
169
  stats,
182
170
  gitignore,
183
- width,
171
+ rows,
184
172
  show_modified,
185
- show_tree,
186
173
  )
187
174
  elif entry.is_file():
188
175
  stats["files"] += 1
189
- size_text, info, size, modified = file_info(entry)
176
+ size_text, info, size, modified = _file_metadata(entry)
190
177
  stats["total_size"] += size
191
- # The complete tree/name portion is padded to the
192
- # specified width so metadata lines up vertically.
193
- tree_name = prefix + connector + entry.name
194
- output = f"{tree_name:<{width}}{size_text:>10} {info}"
195
- if show_modified and modified is not None:
178
+ timestamp = modified if show_modified else None
179
+ rows.append((tree_name, (size_text, info, timestamp)))
180
+
181
+
182
+ def print_tree(
183
+ directory,
184
+ prefix="",
185
+ stats=None,
186
+ gitignore=None,
187
+ show_modified=False,
188
+ show_tree=True,
189
+ ):
190
+ """Print the directory tree with aligned file metadata."""
191
+ if stats is None:
192
+ stats = {"directories": 0, "files": 0, "total_size": 0}
193
+ rows = []
194
+ # Walk the filesystem once and collect the output
195
+ _collect_tree(Path(directory), prefix, stats, gitignore, rows, show_modified)
196
+ if show_tree:
197
+ # Align file metadata with the longest tree/name
198
+ max_tree_name_width = max(
199
+ (len(tree_name) for tree_name, _ in rows),
200
+ default=0,
201
+ )
202
+ for tree_name, file_info_data in rows:
203
+ if file_info_data is None:
204
+ print(tree_name)
205
+ continue
206
+ size_text, info, modified = file_info_data
207
+ output = f"{tree_name:<{max_tree_name_width}}{size_text:>10} {info:<10}"
208
+ if modified is not None:
196
209
  output += f" {modified}"
197
- display(output)
210
+ print(output)
198
211
  return stats
199
212
 
200
213
 
201
214
  def parse_args(argv=None):
202
215
  """Parse command-line arguments."""
203
216
  parser = argparse.ArgumentParser(
204
- description="List directory contents in a tree format with file metadata.",
217
+ description="List directory contents as a tree with file metadata.",
205
218
  )
206
219
  parser.add_argument(
207
220
  "directory",
@@ -228,14 +241,6 @@ def parse_args(argv=None):
228
241
  action="store_true",
229
242
  help="show summary only",
230
243
  )
231
- parser.add_argument(
232
- "-w",
233
- "--width",
234
- type=int,
235
- default=DEFAULT_METADATA_COLUMN,
236
- metavar="N",
237
- help=f"starting column for file metadata (default: {DEFAULT_METADATA_COLUMN})",
238
- )
239
244
  return parser.parse_args(argv)
240
245
 
241
246
 
@@ -245,9 +250,6 @@ def main():
245
250
  if not path.is_dir():
246
251
  print(f"Not a directory: {path}")
247
252
  return 1
248
- if args.width < 1:
249
- print("Width must be greater than zero.")
250
- return 1
251
253
  # Only initialize Git support if it will be used
252
254
  gitignore = None if args.all else GitIgnore(path)
253
255
  show_tree = not args.summary
@@ -256,7 +258,6 @@ def main():
256
258
  stats = print_tree(
257
259
  path,
258
260
  gitignore=gitignore,
259
- width=args.width,
260
261
  show_modified=args.modified,
261
262
  show_tree=show_tree,
262
263
  )
@@ -265,7 +266,7 @@ def main():
265
266
  print(
266
267
  f"{stats['directories']} directories • "
267
268
  f"{stats['files']} files • "
268
- f"{format_size(stats['total_size'])}"
269
+ f"{_format_size(stats['total_size'])}"
269
270
  )
270
271
  return 0
271
272
 
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: treex-cli
3
- Version: 0.1.1
4
- Summary: List directory contents in a tree format with file metadata.
3
+ Version: 0.1.3
4
+ Summary: List directory contents as a tree with file metadata
5
5
  Author: Corey Goldberg
6
6
  Maintainer: Corey Goldberg
7
7
  License-Expression: MIT
@@ -25,7 +25,7 @@ Dynamic: license-file
25
25
 
26
26
  # treex
27
27
 
28
- ## List directory contents in a tree format with file metadata.
28
+ ## List directory contents as a tree with file metadata
29
29
 
30
30
  - Copyright (c) 2026 [Corey Goldberg][github-profile]
31
31
  - Development: [GitHub][github-repo]
@@ -36,18 +36,14 @@ Dynamic: license-file
36
36
 
37
37
  ## About
38
38
 
39
- `treex` is a command-line utility (similar to [tree][tree-home]) that recursively
40
- scans a directory and prints its contents as a tree using Unicode box-drawing
41
- characters. File names include their human-readable size and, optionally,
42
- modification time. Text files also include their line count, while binary and
43
- unreadable files are marked with their type. When Git is installed, it respects
44
- `.gitignore` rules.
39
+ `treex` is a command-line utility that recursively scans a directory and
40
+ displays its contents as a tree along with file metadata.
45
41
 
46
- Requirements:
47
-
48
- - Python 3.10+
49
- - Git 2.0+ (optional)
50
- - UTF-8-compatible terminal
42
+ - File names show human-readable sizes
43
+ - Text files show line counts
44
+ - Binary and unreadable files show their type
45
+ - Modification timestamps are optional
46
+ - Respects `.gitignore` rules when Git is installed
51
47
 
52
48
  ----
53
49
 
@@ -82,6 +78,14 @@ Requirements:
82
78
 
83
79
  ----
84
80
 
81
+ ## Requirements
82
+
83
+ - Python 3.10+
84
+ - Git 2.0+ (optional)
85
+ - UTF-8-compatible terminal
86
+
87
+ ----
88
+
85
89
  ## Installation
86
90
 
87
91
  Install `treex` from [PyPI][pypi-home] using either `pip` or `pipx`:
@@ -95,9 +99,9 @@ Install `treex` from [PyPI][pypi-home] using either `pip` or `pipx`:
95
99
 
96
100
  ```
97
101
  $ treex --help
98
- usage: treex [-h] [-a] [-m] [-s] [-w N] [directory]
102
+ usage: treex [-h] [-a] [-m] [-s] [directory]
99
103
 
100
- List directory contents in a tree format with file metadata.
104
+ List directory contents as a tree with file metadata.
101
105
 
102
106
  positional arguments:
103
107
  directory directory to scan (default: current directory)
@@ -107,7 +111,6 @@ options:
107
111
  -a, --all show all files (including those ignored by Git)
108
112
  -m, --modified show file modification times
109
113
  -s, --summary show summary only
110
- -w, --width N starting column for file metadata (default: 50)
111
114
  ```
112
115
 
113
116
  ----
@@ -120,4 +123,3 @@ options:
120
123
  [github-repo]: https://github.com/cgoldberg/treex
121
124
  [pypi-home]: https://pypi.org/project/treex-cli
122
125
  [mit-license]: https://raw.githubusercontent.com/cgoldberg/treex/refs/heads/main/LICENSE
123
- [tree-home]: https://oldmanprogrammer.net/source.php?dir=projects/tree
File without changes
File without changes