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 +1 -0
- codetree/__main__.py +5 -0
- codetree/cli.py +164 -97
- codetree/core.py +408 -0
- codetree/gui.py +1281 -0
- codetree_cli-0.3.0.dist-info/METADATA +175 -0
- codetree_cli-0.3.0.dist-info/RECORD +11 -0
- {codetree_cli-0.2.2.dist-info → codetree_cli-0.3.0.dist-info}/WHEEL +1 -1
- codetree_cli-0.3.0.dist-info/entry_points.txt +5 -0
- codetree_cli-0.3.0.dist-info/licenses/LICENSE +21 -0
- codetree_cli-0.2.2.dist-info/METADATA +0 -99
- codetree_cli-0.2.2.dist-info/RECORD +0 -7
- codetree_cli-0.2.2.dist-info/entry_points.txt +0 -2
- {codetree_cli-0.2.2.dist-info → codetree_cli-0.3.0.dist-info}/top_level.txt +0 -0
codetree/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
codetree/__main__.py
ADDED
codetree/cli.py
CHANGED
|
@@ -1,97 +1,164 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
""
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
print("")
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
""
|
|
63
|
-
print(f"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"""
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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())
|