file-tree-display 1.0.0__py3-none-any.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.
@@ -0,0 +1,33 @@
1
+ """file-tree-display - A utility for generating a visually structured directory tree.
2
+ Copyright (c) 2025 Yaron Dayan
3
+ """
4
+
5
+ from pathlib import Path
6
+ from importlib.metadata import version
7
+
8
+ from nano_dev_utils.common import (
9
+ update,
10
+ str2file,
11
+ PredicateBuilder,
12
+ FilterSet,
13
+ load_cfg_file,
14
+ )
15
+
16
+ from .ftd import FileTreeDisplay
17
+ from ._constants import PKG_NAME
18
+
19
+ filetree_display = FileTreeDisplay(root_dir=str(Path.cwd()))
20
+ predicate_builder = PredicateBuilder
21
+
22
+ __version__ = version(PKG_NAME)
23
+
24
+ __all__ = [
25
+ 'update',
26
+ 'str2file',
27
+ 'PredicateBuilder',
28
+ 'predicate_builder',
29
+ 'FilterSet',
30
+ 'filetree_display',
31
+ 'FileTreeDisplay',
32
+ 'load_cfg_file',
33
+ ]
@@ -0,0 +1,129 @@
1
+ """CLI interface for FileTreeDisplay"""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from .ftd import FileTreeDisplay
11
+ from nano_dev_utils.common import load_cfg_file
12
+ from ._constants import DEFAULT_SFX
13
+
14
+
15
+ def parse() -> argparse.Namespace:
16
+ parser = argparse.ArgumentParser(
17
+ description='Display or export a formatted file tree of a directory.',
18
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
19
+ )
20
+
21
+ parser.add_argument('--cfg', type=str, help='Path to JSON config file.')
22
+ parser.add_argument('--root-dir', '-r', type=str, help='Root directory to display.')
23
+ parser.add_argument('--filepath', '-o', type=str, help='Output file path.')
24
+ parser.add_argument(
25
+ '--ignore-dirs', nargs='*', default=None, help='Directories to ignore.'
26
+ )
27
+ parser.add_argument(
28
+ '--ignore-files', nargs='*', default=None, help='Files to ignore.'
29
+ )
30
+ parser.add_argument(
31
+ '--include-dirs', nargs='*', default=None, help='Directories to include.'
32
+ )
33
+ parser.add_argument(
34
+ '--include-files', nargs='*', default=None, help='Files to include.'
35
+ )
36
+ parser.add_argument(
37
+ '--style',
38
+ '-s',
39
+ choices=['classic', 'dash', 'arrow', 'plus'],
40
+ default='classic',
41
+ help='Tree connector style.',
42
+ )
43
+ parser.add_argument(
44
+ '--indent', '-i', type=int, default=2, help='Indent width per level.'
45
+ )
46
+ parser.add_argument(
47
+ '--files-first',
48
+ '-f',
49
+ action='store_true',
50
+ default=False,
51
+ help='List files before directories.',
52
+ )
53
+ parser.add_argument(
54
+ '--skip-sorting', action='store_true', default=False, help='Disable sorting.'
55
+ )
56
+ parser.add_argument(
57
+ '--sort-key',
58
+ choices=['natural', 'lex', 'custom'],
59
+ default='natural',
60
+ help='Sorting mode.',
61
+ )
62
+ parser.add_argument(
63
+ '--reverse', action='store_true', default=False, help='Reverse sort order.'
64
+ )
65
+ parser.add_argument(
66
+ '--no-save', action='store_true', default=False, help='Do not save to file.'
67
+ )
68
+ parser.add_argument(
69
+ '--printout',
70
+ '-p',
71
+ action='store_true',
72
+ default=False,
73
+ help='Print tree to stdout.',
74
+ )
75
+ parser.add_argument(
76
+ '--version', '-v', action='version', version=f'{FileTreeDisplay.get_version()}'
77
+ )
78
+ return parser.parse_args()
79
+
80
+
81
+ def merge_config(
82
+ cli_args: argparse.Namespace, cfg_dict: dict[str, Any]
83
+ ) -> dict[str, Any]:
84
+ """Merge CLI args and config file into a unified options dict."""
85
+ merged = {**cfg_dict}
86
+ for k, v in vars(cli_args).items():
87
+ if v is not None and k != 'cfg':
88
+ merged[k] = v
89
+ return merged
90
+
91
+
92
+ def main() -> None:
93
+ args = parse()
94
+ cfg_dict = load_cfg_file(args.cfg)
95
+ opts = merge_config(args, cfg_dict)
96
+
97
+ root_dir = Path(opts.get('root_dir') or Path.cwd())
98
+ if not root_dir.exists():
99
+ sys.exit(f"Error: root directory '{root_dir}' does not exist.")
100
+
101
+ filepath = opts.get('filepath')
102
+ if not filepath and not opts.get('no-save'):
103
+ filepath = str(root_dir.with_name(f'{root_dir.name}{DEFAULT_SFX}'))
104
+
105
+ ftd = FileTreeDisplay(
106
+ root_dir=str(root_dir),
107
+ filepath=filepath,
108
+ ignore_dirs=opts.get('ignore_dirs') or [],
109
+ ignore_files=opts.get('ignore_files') or [],
110
+ include_dirs=opts.get('include_dirs') or [],
111
+ include_files=opts.get('include_files') or [],
112
+ style=opts.get('style', 'classic'),
113
+ indent=int(opts.get('indent', 2)),
114
+ files_first=bool(opts.get('files_first', False)),
115
+ skip_sorting=bool(opts.get('skip_sorting', False)),
116
+ sort_key_name=opts.get('sort_key', 'natural'),
117
+ reverse=bool(opts.get('reverse', False)),
118
+ save2file=not opts.get('no_save', False),
119
+ printout=opts.get('printout', False),
120
+ )
121
+
122
+ try:
123
+ ftd.file_tree_display()
124
+ except Exception as e:
125
+ sys.exit(f'Error: {e}')
126
+
127
+
128
+ if __name__ == '__main__':
129
+ main()
@@ -0,0 +1 @@
1
+ __version__ = '1.0.0'
@@ -0,0 +1,2 @@
1
+ PKG_NAME = 'file-tree-display'
2
+ DEFAULT_SFX = '_filetree.txt'
@@ -0,0 +1,325 @@
1
+ import io
2
+ import os
3
+ import re
4
+
5
+ from collections.abc import Generator
6
+ from itertools import chain
7
+ from pathlib import Path
8
+ from typing_extensions import Callable, Any
9
+ from typing import Iterable
10
+
11
+ from importlib.metadata import version, PackageNotFoundError
12
+ from nano_dev_utils.common import str2file, FilterSet, PredicateBuilder
13
+ from ._constants import PKG_NAME
14
+
15
+ _NUM_SPLIT = re.compile(r'(\d+)').split
16
+
17
+
18
+ class FileTreeDisplay:
19
+ """Generate and display a visual file tree of a directory.
20
+
21
+ This class builds a directory tree structure and yields formatted
22
+ visual representations of directories and files.
23
+ Supports exclusion lists, configurable indentation, and custom prefix styles.
24
+ """
25
+
26
+ __slots__ = (
27
+ 'root_path',
28
+ 'filepath',
29
+ 'ignore_dirs',
30
+ 'ignore_files',
31
+ 'include_dirs',
32
+ 'include_files',
33
+ 'style',
34
+ 'indent',
35
+ 'files_first',
36
+ 'skip_sorting',
37
+ 'sort_key_name',
38
+ 'reverse',
39
+ 'custom_sort',
40
+ 'save2file',
41
+ 'printout',
42
+ 'style_dict',
43
+ 'sort_keys',
44
+ 'pb',
45
+ 'dir_filter',
46
+ 'file_filter',
47
+ )
48
+
49
+ def __init__(
50
+ self,
51
+ root_dir: str | None = None,
52
+ filepath: str | None = None,
53
+ ignore_dirs: FilterSet = None,
54
+ ignore_files: FilterSet = None,
55
+ include_dirs: FilterSet = None,
56
+ include_files: FilterSet = None,
57
+ style: str = 'classic',
58
+ indent: int = 2,
59
+ files_first: bool = False,
60
+ skip_sorting: bool = False,
61
+ sort_key_name: str = 'natural',
62
+ reverse: bool = False,
63
+ custom_sort: Callable[[str], Any] | None = None,
64
+ save2file: bool = True,
65
+ printout: bool = False,
66
+ ) -> None:
67
+ """Initialize the FileTreeDisplay instance.
68
+
69
+ Args:
70
+ root_dir (str): Root directory to traverse.
71
+ filepath: str | None: full output file path.
72
+ ignore_dirs (list[str] | set[str] | None): Directory names or patterns to ignore.
73
+ ignore_files (list[str] | set[str] | None): File names or patterns to ignore.
74
+ include_dirs (list[str] | set[str] | None): Directory names or patterns to include.
75
+ include_files (list[str] | set[str] | None): File names or patterns to include.
76
+ style (str): Character(s) used to represent hierarchy levels. Defaults to " ".
77
+ indent (int): Number of style characters used per hierarchy level. Defaults to 2.
78
+ files_first (bool): Determines whether to list files first. Defaults to False.
79
+ skip_sorting (bool): Skip sorting directly, even if configured.
80
+ sort_key_name (str): sorting key name, e.g. 'lex' for lexicographic or 'custom'. Defaults to 'natural'.
81
+ reverse (bool): reversed sorting.
82
+ custom_sort (Callable[[str], Any] | None):
83
+ save2file (bool): save file tree info to a file.
84
+ printout (bool): print file tree info.
85
+ """
86
+ self.root_path = Path(root_dir) if root_dir else Path.cwd()
87
+ self.filepath = filepath
88
+ self.ignore_dirs = set(ignore_dirs or [])
89
+ self.ignore_files = set(ignore_files or [])
90
+ self.include_dirs = set(include_dirs or [])
91
+ self.include_files = set(include_files or [])
92
+ self.style = style
93
+ self.indent = indent
94
+ self.files_first = files_first
95
+ self.skip_sorting = skip_sorting
96
+ self.sort_key_name = sort_key_name
97
+ self.reverse = reverse
98
+ self.custom_sort = custom_sort
99
+ self.save2file = save2file
100
+ self.printout = printout
101
+
102
+ self.style_dict: dict = {
103
+ 'classic': self.connector_styler('├── ', '└── '),
104
+ 'dash': self.connector_styler('|-- ', '`-- '),
105
+ 'arrow': self.connector_styler('├─> ', '└─> '),
106
+ 'plus': self.connector_styler('+--- ', '\\--- '),
107
+ }
108
+
109
+ self.sort_keys = {
110
+ 'natural': self._nat_key,
111
+ 'lex': self._lex_key,
112
+ 'custom': self.custom_sort,
113
+ }
114
+
115
+ self.pb = PredicateBuilder()
116
+ self.dir_filter = self.pb.build_predicate(self.include_dirs, self.ignore_dirs)
117
+ self.file_filter = self.pb.build_predicate(
118
+ self.include_files, self.ignore_files
119
+ )
120
+
121
+ @staticmethod
122
+ def get_version():
123
+ try:
124
+ return version(PKG_NAME)
125
+ except PackageNotFoundError:
126
+ return 'unknown'
127
+
128
+ def format_style(self) -> dict:
129
+ style, style_dict = self.style, self.style_dict
130
+ style_keys = list(style_dict.keys())
131
+ if style not in style_keys:
132
+ raise ValueError(f"'{style}' is invalid: must be one of {style_keys}\n")
133
+ return style_dict[style]
134
+
135
+ def init(self, *args, **kwargs) -> None:
136
+ self.__init__(*args, **kwargs)
137
+
138
+ def update_predicates(self) -> None:
139
+ self.dir_filter = self.pb.build_predicate(self.include_dirs, self.ignore_dirs)
140
+ self.file_filter = self.pb.build_predicate(
141
+ self.include_files, self.ignore_files
142
+ )
143
+
144
+ @staticmethod
145
+ def _nat_key(name: str) -> list[int | str | Any]:
146
+ """Natural sorting key"""
147
+ return [
148
+ int(part) if part.isdigit() else part.lower() for part in _NUM_SPLIT(name)
149
+ ]
150
+
151
+ @staticmethod
152
+ def _lex_key(name: str) -> str:
153
+ """Lexicographic sorting key"""
154
+ return name.lower()
155
+
156
+ def _resolve_sort_key(self) -> Callable[[str], Any]:
157
+ sort_key_name, sort_keys = self.sort_key_name, self.sort_keys
158
+ key = sort_keys.get(sort_key_name)
159
+ if key is None:
160
+ if self.sort_key_name == 'custom':
161
+ raise ValueError(
162
+ "custom_sort function must be specified when sort_key_name='custom'"
163
+ )
164
+ raise ValueError(
165
+ f'Invalid sort key name: "{sort_key_name}"! '
166
+ f'Currently defined keys are: {list(sort_keys.keys())}'
167
+ )
168
+ return key
169
+
170
+ def file_tree_display(self) -> str:
171
+ """Generates a directory tree and saves it to a text file.
172
+
173
+ Returns:
174
+ str: The complete directory tree as a single CRLF-delimited string.
175
+ """
176
+ root_path_str = str(self.root_path)
177
+ filepath = self.filepath
178
+
179
+ if not self.root_path.is_dir():
180
+ raise NotADirectoryError(f"The path '{root_path_str}' is not a directory.")
181
+
182
+ style = self.format_style()
183
+ sort_key = None if self.skip_sorting else self._resolve_sort_key()
184
+ dir_filter, file_filter = self.dir_filter, self.file_filter
185
+ files_first, reverse = self.files_first, self.reverse
186
+ indent = self.indent
187
+
188
+ iterator = self._build_tree(
189
+ root_path_str,
190
+ prefix='',
191
+ style=style,
192
+ sort_key=sort_key,
193
+ files_first=files_first,
194
+ dir_filter=dir_filter,
195
+ file_filter=file_filter,
196
+ reverse=reverse,
197
+ indent=indent,
198
+ )
199
+
200
+ tree_info = self.get_tree_info(iterator)
201
+
202
+ if self.save2file and filepath:
203
+ str2file(tree_info, filepath)
204
+
205
+ if self.printout:
206
+ print(tree_info)
207
+
208
+ self.get_num_of_entries(tree_info)
209
+
210
+ return tree_info
211
+
212
+ def get_tree_info(self, iterator: Generator[str, None, None]) -> str:
213
+ buf = io.StringIO()
214
+ write = buf.write
215
+ write(f'{self.root_path.name}/\n')
216
+ buf.writelines(f'{line}\n' for line in iterator)
217
+ out = buf.getvalue()
218
+ return out[:-1] if out.endswith('\n') else out
219
+
220
+ def _build_tree(
221
+ self,
222
+ dir_path: str,
223
+ *,
224
+ prefix: str,
225
+ style: dict,
226
+ sort_key: Callable[[str], Any] | None,
227
+ files_first: bool,
228
+ dir_filter: Callable[[str], bool],
229
+ file_filter: Callable[[str], bool],
230
+ reverse: bool,
231
+ indent: int,
232
+ ) -> Generator[str, None, None]:
233
+ """Yields lines representing a formatted folder structure using a recursive DFS.
234
+ The internal recursive generator has runtime consts threaded through to avoid attrib. lookups.
235
+
236
+ Args:
237
+ dir_path (str): The directory path or disk drive currently being traversed.
238
+ prefix (str): Hierarchical prefix applied to each level.
239
+
240
+ Yields:
241
+ str: A formatted text representation of the folder structure.
242
+ """
243
+ branch = style['branch']
244
+ end = style['end']
245
+ vertical = style['vertical']
246
+ space = style['space']
247
+
248
+ recurse = self._build_tree
249
+
250
+ dirs: list[tuple[str, str]] = []
251
+ files: list[str] = []
252
+
253
+ try:
254
+ with os.scandir(dir_path) as it:
255
+ append_dir = dirs.append
256
+ append_file = files.append
257
+ for entry in it:
258
+ name = entry.name
259
+ try:
260
+ is_dir = entry.is_dir(follow_symlinks=False)
261
+ except OSError:
262
+ continue
263
+
264
+ if is_dir:
265
+ if dir_filter(name):
266
+ append_dir((name, entry.path))
267
+ else:
268
+ if file_filter(name):
269
+ append_file(name)
270
+
271
+ except (PermissionError, OSError, FileNotFoundError) as e:
272
+ if isinstance(e, PermissionError):
273
+ yield '[Permission Denied]'
274
+ else:
275
+ yield '[Error reading directory]'
276
+ return
277
+
278
+ if sort_key:
279
+ dirs.sort(key=lambda d: sort_key(d[0]), reverse=reverse)
280
+ files.sort(key=sort_key, reverse=reverse)
281
+
282
+ # Compute combined sequence without extra temporary tuples where possible
283
+ f_iter: Iterable[tuple[str, None, bool]] = ((f, None, False) for f in files)
284
+ d_iter: Iterable[tuple[str, str, bool]] = ((d[0], d[1], True) for d in dirs)
285
+ seq: Iterable[tuple[str, str | None, bool]] = (
286
+ chain(f_iter, d_iter) if files_first else chain(d_iter, f_iter)
287
+ )
288
+
289
+ combined = list(seq)
290
+ last_index = len(combined) - 1
291
+
292
+ for idx, (name, path, is_dir) in enumerate(combined):
293
+ is_last = idx == last_index
294
+ connector = end if is_last else branch
295
+ formatted_name = f'{name}/' if is_dir else name
296
+ yield f'{prefix}{connector}{formatted_name}'
297
+ extension = space if is_last else vertical
298
+
299
+ if is_dir and path:
300
+ yield from recurse(
301
+ path,
302
+ prefix=prefix + extension,
303
+ style=style,
304
+ sort_key=sort_key,
305
+ files_first=files_first,
306
+ dir_filter=dir_filter,
307
+ file_filter=file_filter,
308
+ reverse=reverse,
309
+ indent=indent,
310
+ )
311
+
312
+ def connector_styler(self, branch: str, end: str) -> dict:
313
+ indent = self.indent
314
+ return {
315
+ 'space': ' ' * indent,
316
+ 'vertical': f'│{" " * (indent - 1)}',
317
+ 'branch': branch,
318
+ 'end': end,
319
+ }
320
+
321
+ @staticmethod
322
+ def get_num_of_entries(tree_info: str) -> None:
323
+ num_of_folders = tree_info.count('/') - 1
324
+ num_of_files = tree_info.count('\n') - num_of_folders
325
+ print(f'Scanned {num_of_folders} folders and {num_of_files} files.')
@@ -0,0 +1,344 @@
1
+ Metadata-Version: 2.4
2
+ Name: file_tree_display
3
+ Version: 1.0.0
4
+ Summary: A utility for generating a visually structured directory tree.
5
+ Project-URL: Homepage, https://github.com/yaronday/file_tree_display
6
+ Project-URL: Issues, https://github.com/yaronday/file_tree_display/issues
7
+ Project-URL: license, https://github.com/yaronday/file_tree_display/blob/master/LICENSE
8
+ Author-email: Yaron Dayan <yaronday77@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: build>=1.3.0
15
+ Requires-Dist: nano-dev-utils>=1.6.2
16
+ Description-Content-Type: text/markdown
17
+
18
+ # file_tree_display
19
+
20
+ A utility for generating a visually structured directory tree.
21
+
22
+ ## Modules
23
+
24
+ ## `file_tree_display.py`
25
+
26
+ This core module supports recursive traversal, customizable hierarchy styles, and inclusion / exclusion
27
+ patterns for directories and files.
28
+ Output can be displayed in the console or saved to a file.
29
+
30
+
31
+ #### Key Features
32
+
33
+ - Recursively displays and logs directory trees
34
+ - Efficient directory traversal
35
+ - Blazing fast (see Benchmarks below)
36
+ - Generates human-readable file tree structure
37
+ - Supports including / ignoring specific directories or files via pattern matching
38
+ - Customizable tree display output
39
+ - Optionally saves the resulting tree to a text file
40
+ - Lightweight, flexible and easily configurable
41
+
42
+
43
+ ### Benchmarks
44
+
45
+ The measurements were carried out on unfiltered folders containing multiple files und subdirectories, using SSD.
46
+ Avg. time was measured over 20 runs per configuration, using `timeit` decorator I've implemented in this package.
47
+
48
+ Comparing FileTreeDisplay (FTD) with
49
+ [win_tree_wrapper](https://github.com/yaronday/nano_dev_utils/blob/master/benchmark/win_tree_wrapper.py)
50
+ (Windows [tree](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/tree)
51
+ wrapper which I've implemented for this purpose).
52
+ [Benchtest code](https://github.com/yaronday/nano_dev_utils/blob/master/benchmark/benchtest.py)
53
+
54
+ ### Performance Comparison — FTD vs.`tree`
55
+
56
+ <table>
57
+ <tr><th>Test Context </th><th>Results</th></tr>
58
+ <tr><td>
59
+
60
+ | Metric | Test1 | Test2 |
61
+ |:---------------------|:-------|:-----------|
62
+ | **Files** | 10553 | 138492 |
63
+ | **Folders** | 1235 | 20428 |
64
+ | **Wrapper Overhead** | ~30 ms | negligible |
65
+
66
+ </td><td>
67
+
68
+ | Tool | T1 (s) | T2 (s) | Relative Speed |
69
+ |:---------|:------:|:------:|:--------------:|
70
+ | **FTD** | 0.196 | 2.900* | — |
71
+ | **tree** | 0.390 | 5.018 | ~2x slower |
72
+
73
+ </td></tr> </table>
74
+
75
+ ***Without sorting** FTD takes 162 ms and 2.338 s for Test1 and Test2, respectively.
76
+ FTD is roughly **1.7x–2.4x faster** than the native `tree` binary across both datasets.
77
+
78
+ ### Brief Analysis
79
+
80
+ ### I. Linear scaling as a function of entries
81
+ FTD performance scales almost perfectly linearly with total entries:
82
+
83
+ * **T1:** 10 k files → 0.2 s
84
+ * **T2:** 138 k files → 2.9 s
85
+ → ~14x more files → ~15x more runtime => expected by linearity.
86
+
87
+ ### II. Figuring out why `tree` is nearly 2 times slower than my FTD
88
+ Although `tree` is implemented in C, it incurs more I/O work:
89
+ * Performs full `lstat()` on each entry (permissions, timestamps, etc.).
90
+ * Prints incrementally to `stdout` → many system calls (syscalls).
91
+ * Handles color / formatting output.
92
+
93
+ My FTD avoids this by:
94
+
95
+ * Using `os.scandir()` (caching stat info).
96
+ * Filtering and sorting in-memory.
97
+ * Buffering output before optional print/write.
98
+
99
+ Result: lower syscall count and fewer I/O stalls.
100
+
101
+ ### III. Python overhead is clearly negligible
102
+ Even at 2.9 s for ~160K entries, throughput ~55K entries/s — close to filesystem limits on SSDs.
103
+ Measured wrapper overhead (~30 ms) is < 1 % of total runtime.
104
+
105
+ ### Key Insights
106
+
107
+ | Observation | Explanation |
108
+ |:-------------------------------|:----------------------------------------------------|
109
+ | **FTD ~2x faster than `tree`** | Avoids per-file printing and redundant stats. |
110
+ | **I/O-bound execution** | Filesystem metadata fetch dominates total time. |
111
+ | **Linear runtime scaling** | Recursive generator design adds no hidden overhead. |
112
+ | **Stable memory footprint** | Uses streaming generators and `StringIO` buffering. |
113
+
114
+ ### Conclusions
115
+
116
+ * **FTD outperforms `tree` by roughly 2x** on both moderate and large datasets.
117
+ * **Runtime scales linearly** with total directory entries.
118
+ * **Python layer overhead is negligible** — performance is bounded by kernel I/O.
119
+
120
+
121
+ #### Class Overview
122
+
123
+ **`FileTreeDisplay`**
124
+ Constructs and manages the visual representation of a folder structure of a path or of a disk drive.
125
+
126
+ **Initialization Parameters**
127
+
128
+ | Parameter | Type | Description |
129
+ |:--------------------------------------|:------------------------------------|:---------------------------------------------------------------------------------|
130
+ | `root_dir` | `str` | Path to the directory to scan. |
131
+ | `filepath` | `str / None` | Optional output destination for the saved file tree. |
132
+ | `ignore_dirs` | `list[str] or set[str] or None` | Directory names or patterns to skip. |
133
+ | `ignore_files` | `list[str] or set[str] or None` | File names or patterns to skip. |
134
+ | `include_dirs` | `list[str] or set[str] or None` | Only include specified folder names or patterns. |
135
+ | `include_files` | `list[str] or set[str] or None` | Only include specified file names or patterns, '*.pdf' - only include pdfs. |
136
+ | `style` | `str` | Characters used to mark hierarchy levels. Defaults to `'classic'`. |
137
+ | `indent` | `int` | Number of style characters per level. Defaults `2`. |
138
+ | `files_first` | `bool` | Determines whether to list files first. Defaults to False. |
139
+ | `skip_sorting` | `bool` | Skip sorting directly, even if configured. |
140
+ | `sort_key_name` | `str` | Sort key. Lexicographic ('lex') or 'custom'. Defaults to 'natural'. |
141
+ | `reverse` | `bool` | Reversed sorting order. |
142
+ | `custom_sort` | `Callable[[str], Any] / None` | Custom sort key function. |
143
+ | `title` | `str` | Custom title shown in the output. |
144
+ | `save2file` | `bool` | Save file tree (folder structure) info into a file. |
145
+ | `printout` | `bool` | Print file tree info. |
146
+
147
+ #### Core Methods
148
+
149
+ - `file_tree_display(save2file: bool = True) -> str | None`
150
+ Generates the directory tree. If `save2file=True`, saves the output; otherwise prints it directly.
151
+
152
+ - `_build_tree(dir_path, *, prefix, style, sort_key,
153
+ files_first, dir_filter, file_filter, reverse, indent) -> Generator[str, None, None]`
154
+ Recursively traverses the directory tree in depth-first order (DFS) and yields formatted lines representing the file and folder structure.
155
+
156
+ | Parameter | Type | Description |
157
+ |-------------------------------------|-------------------------|------------------------------------------------------------------------------|
158
+ | **`dir_path`** | `str` | Path to the directory being traversed. |
159
+ | **`prefix`** | `str` | Current indentation prefix for nested entries. |
160
+ | **`style`** | `dict[str, str]` | Connector style mapping with keys: `branch`, `end`, `space`, and `vertical`. |
161
+ | **`sort_key`** | `Callable[[str], Any]` | Function used to sort directory and file names. |
162
+ | **`files_first`** | `bool` | If `True`, list files before subdirectories. |
163
+ | **`dir_filter`**, **`file_filter`** | `Callable[[str], bool]` | Predicates to include or exclude directories and files. |
164
+ | **`reverse`** | `bool` | If `True`, reverses the sort order. |
165
+ | **`indent`** | `int` | Number of spaces (or repeated characters) per indentation level. |
166
+
167
+
168
+ #### Example Usage
169
+
170
+ ```python
171
+ from pathlib import Path
172
+ from nano_dev_utils.file_tree_display import FileTreeDisplay
173
+
174
+ root = r'c:/your_root_dir'
175
+ target_path = r'c:/your_target_path'
176
+ filename = 'filetree.md'
177
+ filepath = str(Path(target_path, filename))
178
+
179
+ ftd = FileTreeDisplay(root_dir=root,
180
+ ignore_dirs=['.git', 'node_modules', '.idea'],
181
+ ignore_files=['.gitignore', '*.toml'],
182
+ style='classic',
183
+ include_dirs=['src', 'tests', 'snapshots'],
184
+ filepath=filepath,
185
+ sort_key_name='custom',
186
+ custom_sort=(lambda x: any(ext in x.lower() for ext in ('jpg', 'png'))),
187
+ files_first=True,
188
+ reverse=True
189
+ )
190
+ ftd.file_tree_display()
191
+ ```
192
+
193
+ #### Custom connector style
194
+ You can define and register your own connector styles at runtime by adding entries to style_dict:
195
+
196
+ ```Python
197
+ from nano_dev_utils.file_tree_display import FileTreeDisplay
198
+ ftd = FileTreeDisplay(root_dir=".")
199
+ ftd.style_dict["plus2"] = ftd.connector_styler("+-- ", "+== ")
200
+ ftd.style = "plus2"
201
+ ftd.printout = True
202
+ ftd.file_tree_display()
203
+ ```
204
+
205
+
206
+ #### Error Handling
207
+
208
+ The module raises well-defined exceptions for common issues:
209
+
210
+ - `NotADirectoryError` when the path is not a directory
211
+ - `PermissionError` for unreadable directories or write-protected files
212
+ - `OSError` for general I/O or write failures
213
+
214
+
215
+ ## FTD CLI — File Tree Display
216
+
217
+ The **FTD CLI** (`ftd`) is a lightweight, configurable command-line tool for displaying or exporting a formatted file tree of a given directory.
218
+ It’s designed for flexibility, allowing users to control inclusion/exclusion rules, visual style, sorting behavior, and output options.
219
+
220
+ ---
221
+
222
+ ### Features
223
+
224
+ * Generate a clean, formatted file tree of any directory
225
+ * Choose from multiple connector styles (`classic`, `dash`, `arrow`, `plus`)
226
+ * Fine-grained control over included/excluded files and directories
227
+ * Flexible sorting modes (`natural`, `lex`, or `custom`)
228
+ * Optionally export the tree to a file or print directly to stdout
229
+ * Supports configuration via JSON config file
230
+
231
+ ---
232
+
233
+ ### Usage
234
+
235
+ ```bash
236
+ ftd [OPTIONS]
237
+ ```
238
+
239
+ Example:
240
+
241
+ ```bash
242
+ ftd --root-dir ./src --style arrow --files-first --printout
243
+ ```
244
+
245
+ This displays the tree structure of the `src` directory using the *arrow* connector style, listing files before directories, and prints the output to the console.
246
+
247
+ ---
248
+
249
+ ### Command-Line Options
250
+
251
+ | Option | Alias | Type | Description | Default |
252
+ |-------------------|-------|--------|------------------------------------------------------------|-----------|
253
+ | `--cfg` | – | `str` | Path to a JSON configuration file. | – |
254
+ | `--root-dir` | `-r` | `str` | Root directory to display. | – |
255
+ | `--filepath` | `-o` | `str` | Output file path for exported tree. | – |
256
+ | `--ignore-dirs` | – | `str…` | Directories to exclude from the tree. | – |
257
+ | `--ignore-files` | – | `str…` | Files to exclude from the tree. | – |
258
+ | `--include-dirs` | – | `str…` | Directories to explicitly include. | – |
259
+ | `--include-files` | – | `str…` | Files to explicitly include. | – |
260
+ | `--style` | `-s` | `str` | Tree connector style (`classic`, `dash`, `arrow`, `plus`). | `classic` |
261
+ | `--indent` | `-i` | `int` | Indent width (spaces) per directory level. | `2` |
262
+ | `--files-first` | `-f` | flag | List files before directories. | `False` |
263
+ | `--skip-sorting` | – | flag | Disable sorting altogether. | `False` |
264
+ | `--sort-key` | – | `str` | Sorting key: `natural`, `lex`, or `custom`. | `natural` |
265
+ | `--reverse` | – | flag | Reverse the sort order. | `False` |
266
+ | `--no-save` | – | flag | Do not save output to file (useful with `--printout`). | `False` |
267
+ | `--printout` | `-p` | flag | Print the generated tree to stdout. | `False` |
268
+ | `--version` | `-v` | flag | Show the current version of FTD and exit. | – |
269
+
270
+ ---
271
+
272
+ ### Configuration via JSON file
273
+
274
+ FTD can also load parameters from a JSON config file:
275
+
276
+ ```json
277
+ {
278
+ "root_dir": "./project",
279
+ "ignore_dirs": ["__pycache__", "build"],
280
+ "ignore_files": [".DS_Store"],
281
+ "style": "dash",
282
+ "files_first": true,
283
+ "printout": true
284
+ }
285
+ ```
286
+
287
+ Run it using:
288
+
289
+ ```bash
290
+ ftd --cfg path/to/config.json
291
+ ```
292
+
293
+ Command-line arguments always **override** config file values.
294
+
295
+ ---
296
+
297
+ ### Examples
298
+
299
+ Display the current directory with default settings:
300
+
301
+ ```bash
302
+ ftd -r .
303
+ ```
304
+
305
+ Display a tree with dashed connectors, ignoring test folders, and print it:
306
+
307
+ ```bash
308
+ ftd -r ./src --ignore-dirs tests --style dash --printout
309
+ ```
310
+
311
+ Export a formatted tree to a file:
312
+
313
+ ```bash
314
+ ftd -r ./app --filepath tree.txt
315
+ ```
316
+
317
+ Reverse lexical sort with 4-space indentation:
318
+
319
+ ```bash
320
+ ftd -r ./data --sort-key lex --reverse --indent 4
321
+ ```
322
+
323
+ ---
324
+
325
+ ### 🧾 Output Example
326
+
327
+ ```text
328
+ src
329
+ ├── main.py
330
+ ├── utils
331
+ │ ├── helpers.py
332
+ │ └── validators.py
333
+ └── tests
334
+ ├── test_main.py
335
+ └── test_utils.py
336
+ ```
337
+
338
+ ---
339
+
340
+ ***
341
+
342
+ ## License
343
+ This project is licensed under the MIT License.
344
+ See [LICENSE](https://github.com/yaronday/file_tree_display/blob/master/LICENSE) for details.
@@ -0,0 +1,10 @@
1
+ file_tree_display/__init__.py,sha256=jkyGGykmiDBixkjDQD2N1p4cXQzbU8YlpmyiY4tUOyg,718
2
+ file_tree_display/__main__.py,sha256=o74jt4m-8ycgmmKELMGXwdQDnQBJtx9ymHgJCI4l-9Y,4160
3
+ file_tree_display/__version__.py,sha256=crlYURA_2Ujkpz7iVPHovvxZRtvt4QBXF5AAFJzRfz0,23
4
+ file_tree_display/_constants.py,sha256=C4oom9OD9QrJ6wW53ULhmuHLCIKw9pmXkHjLrjOcMAU,63
5
+ file_tree_display/ftd.py,sha256=T8OJXpry9BgcTlfg5MPGb-yM_SpKHZgluG2OwElC2ys,11764
6
+ file_tree_display-1.0.0.dist-info/METADATA,sha256=8FdkRn1OwNq2WukopnNQvDOSsI03L0SImi5lUacyU6o,15505
7
+ file_tree_display-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ file_tree_display-1.0.0.dist-info/entry_points.txt,sha256=L_gNQlsvydWniPChrLbA6a36_5-YuxVqMq9i4a6cEU8,56
9
+ file_tree_display-1.0.0.dist-info/licenses/LICENSE,sha256=Muenl7Bw_LdtHZtlOMAP7Kt97gDCq8WWp2605eDWhHU,1089
10
+ file_tree_display-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ftd = file_tree_display.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Yaron Dayan
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.