codetree-cli 0.2.2__py3-none-any.whl → 0.3.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.
codetree/__init__.py CHANGED
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
codetree/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
codetree/cli.py CHANGED
@@ -1,97 +1,164 @@
1
- import os
2
- import argparse
3
- from collections import defaultdict
4
-
5
- def is_text_file(file_path):
6
- """Check if a file is a text file based on its content."""
7
- try:
8
- with open(file_path, 'r', encoding='utf-8') as file:
9
- file.read()
10
- return True
11
- except (UnicodeDecodeError, IOError):
12
- return False
13
-
14
- def count_lines_of_code(file_path):
15
- """Counts the number of lines in a file if it's a text file."""
16
- if not is_text_file(file_path):
17
- return 0 # Skip non-text files
18
-
19
- try:
20
- with open(file_path, 'r', encoding='utf-8') as file:
21
- lines = file.readlines()
22
- return len(lines)
23
- except Exception as e:
24
- print(f"Error reading {file_path}: {e}")
25
- return 0
26
-
27
- def print_directory_tree(path):
28
- """Prints the directory structure with files and their line counts."""
29
- folder_files = defaultdict(list)
30
- folder_count = 0
31
- checked_files = 0
32
-
33
- # Walk through the directory and get files with line counts
34
- for root, dirs, files in os.walk(path):
35
- # Skip hidden directories
36
- dirs[:] = [d for d in dirs if not d.startswith('.')]
37
-
38
- rel_root = os.path.relpath(root, path)
39
- folder_name = rel_root if rel_root != '.' else 'root' # Show root separately
40
- folder_count += 1
41
-
42
- for file in sorted(files):
43
- if file.startswith('.'):
44
- continue
45
-
46
- file_path = os.path.join(root, file)
47
- lines = count_lines_of_code(file_path)
48
- if lines > 0:
49
- folder_files[folder_name].append((file, lines))
50
- checked_files += 1
51
-
52
- # Print directory tree with line counts
53
- for folder in sorted(folder_files.keys()):
54
- print(f"|-- {folder}/")
55
- for file, lines in folder_files[folder]:
56
- print(f" └── {file} -> {lines} line")
57
- print("")
58
-
59
- return folder_count, checked_files
60
-
61
- def scan_directory(path):
62
- """Scan the directory and prints summary of files and line counts."""
63
- print(f"\nScanning directory: {path}")
64
- total_lines = 0
65
-
66
- folder_count, checked_files = print_directory_tree(path)
67
-
68
- # Count total lines
69
- for root, dirs, files in os.walk(path):
70
- dirs[:] = [d for d in dirs if not d.startswith('.')]
71
-
72
- for file in files:
73
- if file.startswith('.'):
74
- continue
75
-
76
- file_path = os.path.join(root, file)
77
- lines = count_lines_of_code(file_path)
78
- total_lines += lines
79
-
80
- print(f"\nTotal files checked: {checked_files}")
81
- print(f"Total lines of code: {total_lines}")
82
- print(f"Total folders: {folder_count}")
83
-
84
- def main():
85
- """Main function to handle argument parsing and execution."""
86
- parser = argparse.ArgumentParser(description="Count lines of code in a project directory.")
87
- parser.add_argument('path', nargs='?', default=".", help="Path to the project directory (default: current directory)")
88
- args = parser.parse_args()
89
-
90
- print(f"Checking path: {args.path}")
91
- if os.path.exists(args.path):
92
- scan_directory(args.path)
93
- else:
94
- print(f"Path '{args.path}' not found.")
95
-
96
- if __name__ == "__main__":
97
- main()
1
+ """Command line entry point. Opens the GUI by default; ``--cli`` prints to the terminal."""
2
+
3
+ import argparse
4
+ import os
5
+ import sys
6
+
7
+ from . import __version__
8
+ from .core import (DirNode, ScanOptions, ScanResult, format_size, scan, sorted_languages,
9
+ to_csv, to_json, to_markdown)
10
+
11
+ SORT_KEYS = {
12
+ "name": lambda n: n.name.lower(),
13
+ "lines": lambda n: -n.total,
14
+ "code": lambda n: -n.code,
15
+ }
16
+
17
+
18
+ class _Style:
19
+ def __init__(self, enabled: bool):
20
+ self.on = enabled
21
+
22
+ def __call__(self, text, code):
23
+ return f"\033[{code}m{text}\033[0m" if self.on else str(text)
24
+
25
+
26
+ def _print_tree(node: DirNode, c: _Style, sort: str, depth: int, prefix: str = "", level: int = 0) -> None:
27
+ key = SORT_KEYS[sort]
28
+ items = [("d", d) for d in sorted(node.dirs, key=key)] + [("f", f) for f in sorted(node.files, key=key)]
29
+ for i, (kind, item) in enumerate(items):
30
+ last = i == len(items) - 1
31
+ branch = "└── " if last else "├── "
32
+ if kind == "d":
33
+ print(f"{prefix}{branch}{c(item.name + '/', '1;34')} {c(f'{item.total:,} lines', '90')}")
34
+ if depth <= 0 or level + 1 < depth:
35
+ _print_tree(item, c, sort, depth, prefix + (" " if last else "│ "), level + 1)
36
+ else:
37
+ print(f"{prefix}{branch}{item.name} {c(f'{item.total:,} lines', '32')}"
38
+ f" {c('(' + item.language + ')', '90')}")
39
+
40
+
41
+ def _print_report(result: ScanResult, args, c: _Style) -> None:
42
+ print(c(f"\n{result.root.name}/", "1;36"))
43
+ _print_tree(result.root, c, args.sort, args.depth)
44
+
45
+ langs = sorted_languages(result)
46
+ if langs:
47
+ print(c("\nLanguages", "1"))
48
+ width = max(len(l.name) for l in langs)
49
+ total = result.total_lines or 1
50
+ print(f" {'Language'.ljust(width)} {'Files':>6} {'Code':>9} {'Comment':>9} {'Blank':>9} {'Share':>7}")
51
+ for l in langs:
52
+ bar = "█" * max(1, round(l.total / total * 20))
53
+ print(f" {l.name.ljust(width)} {l.files:>6} {l.code:>9,} {l.comment:>9,} {l.blank:>9,} "
54
+ f"{l.total / total:>6.1%} {c(bar, '36')}")
55
+
56
+ if args.top:
57
+ print(c(f"\nTop {args.top} largest files", "1"))
58
+ for f in sorted(result.files, key=lambda f: f.total, reverse=True)[: args.top]:
59
+ print(f" {f.total:>8,} {os.path.relpath(f.path, result.root.path)}")
60
+
61
+ r = result.root
62
+ print(c("\nSummary", "1"))
63
+ print(f" Files: {r.file_count:,}")
64
+ print(f" Folders: {result.folder_count:,}")
65
+ print(f" Lines: {r.total:,} (code {r.code:,} · comments {r.comment:,} · blank {r.blank:,})")
66
+ print(f" Size: {format_size(r.size)}")
67
+ print(f" Time: {result.elapsed:.2f}s")
68
+ if result.skipped_binary or result.skipped_large:
69
+ print(c(f" Skipped: {result.skipped_binary} binary, {result.skipped_large} too large", "90"))
70
+ for err in result.errors[:5]:
71
+ print(c(f" ! {err}", "33"), file=sys.stderr)
72
+
73
+
74
+ def build_parser() -> argparse.ArgumentParser:
75
+ p = argparse.ArgumentParser(
76
+ prog="codetree",
77
+ description="Analyze a project's lines of code. Opens a graphical window by default.")
78
+ p.add_argument("path", nargs="?", default=".", help="project directory (default: current directory)")
79
+ p.add_argument("--cli", action="store_true", help="print the report in the terminal instead of opening the GUI")
80
+ p.add_argument("--json", metavar="FILE", help="write a JSON report ('-' for stdout); implies --cli")
81
+ p.add_argument("--csv", metavar="FILE", help="write a CSV report ('-' for stdout); implies --cli")
82
+ p.add_argument("--md", metavar="FILE", help="write a Markdown report ('-' for stdout); implies --cli")
83
+ p.add_argument("-i", "--ignore", action="append", default=[], metavar="PATTERN",
84
+ help="glob pattern to ignore (repeatable), e.g. -i '*.min.js' -i tests")
85
+ p.add_argument("-e", "--ext", action="append", default=[], metavar="EXT",
86
+ help="only count these extensions (repeatable), e.g. -e py -e js")
87
+ p.add_argument("--hidden", action="store_true", help="include hidden files and folders")
88
+ p.add_argument("--no-gitignore", action="store_true", help="do not read .gitignore")
89
+ p.add_argument("--no-default-ignores", action="store_true", help="do not skip node_modules, venv, build, ...")
90
+ p.add_argument("--max-size", type=float, default=5, metavar="MB", help="skip files larger than this (default 5)")
91
+ p.add_argument("--sort", choices=sorted(SORT_KEYS), default="name", help="tree sort order (CLI)")
92
+ p.add_argument("--depth", type=int, default=0, help="limit tree depth, 0 = unlimited (CLI)")
93
+ p.add_argument("--top", type=int, default=0, metavar="N", help="list the N largest files (CLI)")
94
+ p.add_argument("--no-color", action="store_true", help="disable colored output")
95
+ p.add_argument("-V", "--version", action="version", version=f"codetree {__version__}")
96
+ return p
97
+
98
+
99
+ def _emit(target: str, text: str) -> None:
100
+ if target == "-":
101
+ sys.stdout.write(text)
102
+ else:
103
+ with open(target, "w", encoding="utf-8", newline="") as fh:
104
+ fh.write(text)
105
+ print(f"Saved {target}", file=sys.stderr)
106
+
107
+
108
+ def main(argv=None) -> int:
109
+ args = build_parser().parse_args(argv)
110
+ path = os.path.abspath(args.path)
111
+ if not os.path.isdir(path):
112
+ print(f"codetree: '{args.path}' is not a directory.", file=sys.stderr)
113
+ return 2
114
+
115
+ options = ScanOptions(
116
+ include_hidden=args.hidden,
117
+ use_default_ignores=not args.no_default_ignores,
118
+ respect_gitignore=not args.no_gitignore,
119
+ ignore=args.ignore,
120
+ extensions=args.ext,
121
+ max_size=int(args.max_size * 1024 * 1024),
122
+ )
123
+
124
+ exports = [(args.json, to_json), (args.csv, to_csv), (args.md, to_markdown)]
125
+ wants_cli = args.cli or any(t for t, _ in exports)
126
+
127
+ if not wants_cli:
128
+ try:
129
+ from .gui import run_gui
130
+ except ImportError as e:
131
+ print(f"codetree: GUI unavailable ({e}); falling back to terminal output.", file=sys.stderr)
132
+ else:
133
+ try:
134
+ run_gui(path, options)
135
+ return 0
136
+ except Exception as e: # e.g. no display
137
+ print(f"codetree: could not open the GUI ({e}); falling back to terminal output.", file=sys.stderr)
138
+
139
+ for stream in (sys.stdout, sys.stderr):
140
+ if hasattr(stream, "reconfigure"):
141
+ stream.reconfigure(encoding="utf-8", errors="replace")
142
+ try:
143
+ result = scan(path, options)
144
+ except KeyboardInterrupt:
145
+ return 130
146
+ stdout_taken = any(t == "-" for t, _ in exports)
147
+ for target, fn in exports:
148
+ if target:
149
+ _emit(target, fn(result))
150
+ if not stdout_taken:
151
+ color = not args.no_color and sys.stdout.isatty() and "NO_COLOR" not in os.environ
152
+ if color and os.name == "nt":
153
+ os.system("") # enable ANSI escapes on Windows consoles
154
+ _print_report(result, args, _Style(color))
155
+ return 0
156
+
157
+
158
+ def main_gui() -> int:
159
+ """Entry point for the ``codetree-gui`` launcher (no console window on Windows)."""
160
+ return main(sys.argv[1:] if len(sys.argv) > 1 else ["."])
161
+
162
+
163
+ if __name__ == "__main__":
164
+ sys.exit(main())