dirlens 1.0.0 → 1.0.1

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.
Files changed (2) hide show
  1. package/dirlens.py +193 -0
  2. package/package.json +3 -3
package/dirlens.py ADDED
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ dirlens – ファイルサイズ付きディレクトリツリー表示ツール
4
+ 対応環境: macOS / Linux / Windows (Python 3.8+)
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import argparse
10
+ from pathlib import Path
11
+
12
+ # ─── カラー設定 ──────────────────────────────────────────────
13
+ def _enable_color():
14
+ if not hasattr(sys.stdout, "isatty") or not sys.stdout.isatty():
15
+ return False
16
+ if os.name == "nt":
17
+ # Windows: VT100モードを有効にする
18
+ try:
19
+ import ctypes
20
+ kernel32 = ctypes.windll.kernel32
21
+ kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
22
+ except Exception:
23
+ pass
24
+ return bool(
25
+ os.environ.get("WT_SESSION") # Windows Terminal
26
+ or os.environ.get("TERM_PROGRAM") # VS Code 等
27
+ or os.environ.get("TERM")
28
+ or os.environ.get("ANSICON")
29
+ )
30
+ return True
31
+
32
+ USE_COLOR = _enable_color()
33
+
34
+ RESET = "\033[0m"
35
+ BOLD = "\033[1m"
36
+ DIM = "\033[2m"
37
+ BLUE = "\033[34m"
38
+ CYAN = "\033[36m"
39
+ GREEN = "\033[32m"
40
+ MAGENTA = "\033[35m"
41
+
42
+ def c(text, *codes):
43
+ """ANSIカラーを適用する。カラー無効時はそのまま返す。"""
44
+ return ("".join(codes) + text + RESET) if USE_COLOR else text
45
+
46
+ # ─── サイズ表示 ───────────────────────────────────────────────
47
+ def fmt_size(n):
48
+ """バイト数を人が読みやすい文字列に変換する。"""
49
+ if n == 0:
50
+ return "0 bytes"
51
+ for unit, factor in (("TB", 1 << 40), ("GB", 1 << 30), ("MB", 1 << 20), ("KB", 1 << 10)):
52
+ if n >= factor:
53
+ s = f"{n / factor:.2f}".rstrip("0").rstrip(".")
54
+ return f"{s} {unit}"
55
+ return f"{n} {'byte' if n == 1 else 'bytes'}"
56
+
57
+ # ─── ディレクトリサイズ(キャッシュ付き) ─────────────────────
58
+ _cache = {}
59
+
60
+ def dir_size(path):
61
+ """ディレクトリ以下の合計バイト数を再帰的に計算する(シンボリックリンクは追わない)。"""
62
+ if path in _cache:
63
+ return _cache[path]
64
+ total = 0
65
+ try:
66
+ with os.scandir(path) as it:
67
+ for e in it:
68
+ try:
69
+ if e.is_file(follow_symlinks=False):
70
+ total += e.stat(follow_symlinks=False).st_size
71
+ elif e.is_dir(follow_symlinks=False):
72
+ total += dir_size(e.path)
73
+ except OSError:
74
+ pass
75
+ except OSError:
76
+ pass
77
+ _cache[path] = total
78
+ return total
79
+
80
+ # ─── ツリー描画 ───────────────────────────────────────────────
81
+ PIPE = "│ "
82
+ FORK = "├── "
83
+ LAST = "└── "
84
+ BLANK = " "
85
+
86
+ def render(path, prefix, depth, max_depth, show_all, by_size, stats):
87
+ """ディレクトリ内容を再帰的にツリー表示する。"""
88
+ if max_depth is not None and depth >= max_depth:
89
+ return
90
+
91
+ try:
92
+ raw = list(os.scandir(path))
93
+ except PermissionError:
94
+ print(f"{prefix}{LAST}{c('[アクセス拒否]', DIM)}")
95
+ return
96
+
97
+ # 隠しファイルのフィルタ(オプション依存)
98
+ entries = [e for e in raw if show_all or not e.name.startswith(".")]
99
+
100
+ # ディレクトリ(シンボリックリンク除く)とそれ以外(ファイル+シンボリックリンク)に分類
101
+ dirs = [e for e in entries if e.is_dir(follow_symlinks=False)]
102
+ files = [e for e in entries if not e.is_dir(follow_symlinks=False)]
103
+
104
+ def entry_size(e):
105
+ try:
106
+ return e.stat(follow_symlinks=True).st_size
107
+ except OSError:
108
+ return 0
109
+
110
+ # ソート:名前順 or サイズ順
111
+ if by_size:
112
+ dirs.sort(key=lambda e: dir_size(e.path), reverse=True)
113
+ files.sort(key=lambda e: entry_size(e), reverse=True)
114
+ else:
115
+ dirs.sort(key=lambda e: e.name.casefold())
116
+ files.sort(key=lambda e: e.name.casefold())
117
+
118
+ # ディレクトリを先に、次にファイル
119
+ combined = dirs + files
120
+
121
+ for i, entry in enumerate(combined):
122
+ is_last = (i == len(combined) - 1)
123
+ branch = LAST if is_last else FORK
124
+ cont = BLANK if is_last else PIPE
125
+
126
+ if entry.is_dir(follow_symlinks=False):
127
+ sz = dir_size(entry.path)
128
+ stats["dirs"] += 1
129
+ name = c(f"{entry.name}/", BOLD, CYAN)
130
+ size = c(f"({fmt_size(sz)})", DIM)
131
+ print(f"{prefix}{branch}{name} {size}")
132
+ render(entry.path, prefix + cont, depth + 1, max_depth, show_all, by_size, stats)
133
+ else:
134
+ sz = entry_size(entry)
135
+ sym = " →" if entry.is_symlink() else ""
136
+ stats["files"] += 1
137
+ name = c(f"{entry.name}{sym}", MAGENTA if entry.is_symlink() else GREEN)
138
+ size = c(f"({fmt_size(sz)})", DIM)
139
+ print(f"{prefix}{branch}{name} {size}")
140
+
141
+ # ─── エントリポイント ─────────────────────────────────────────
142
+ def main():
143
+ global USE_COLOR
144
+ sys.setrecursionlimit(10_000)
145
+
146
+ ap = argparse.ArgumentParser(
147
+ prog="dirlens",
148
+ description="ファイルサイズ付きのディレクトリツリーを表示します",
149
+ formatter_class=argparse.RawDescriptionHelpFormatter,
150
+ epilog=(
151
+ "使用例:\n"
152
+ " dirlens カレントディレクトリを表示\n"
153
+ " dirlens ~/Desktop 指定したディレクトリを表示\n"
154
+ " dirlens -d 2 深さ 2 階層まで表示\n"
155
+ " dirlens -a 隠しファイル (.xxx) も表示\n"
156
+ " dirlens -s サイズの大きい順に表示\n"
157
+ " dirlens --no-color カラーなしで表示"
158
+ ),
159
+ )
160
+ ap.add_argument("path", nargs="?", default=".", help="対象ディレクトリ(省略時はカレント)")
161
+ ap.add_argument("-d", "--depth", type=int, metavar="N", help="表示する最大の深さ")
162
+ ap.add_argument("-a", "--all", action="store_true", help="隠しファイルも表示する")
163
+ ap.add_argument("-s", "--sort-size", action="store_true", help="サイズが大きい順に並べる")
164
+ ap.add_argument("--no-color", action="store_true", help="カラー表示を無効化する")
165
+ args = ap.parse_args()
166
+
167
+ if args.no_color:
168
+ USE_COLOR = False
169
+
170
+ target = Path(args.path).resolve()
171
+ if not target.exists():
172
+ print(f"エラー: '{args.path}' が見つかりません", file=sys.stderr)
173
+ sys.exit(1)
174
+ if not target.is_dir():
175
+ print(f"エラー: '{args.path}' はディレクトリではありません", file=sys.stderr)
176
+ sys.exit(1)
177
+
178
+ # ドライブルート(Windows の C:\ 等)対応
179
+ root_label = target.name if target.name else str(target)
180
+
181
+ root_sz = dir_size(str(target))
182
+ root_name = c(f"{root_label}/", BOLD, BLUE)
183
+ root_size = c(f"({fmt_size(root_sz)})", DIM)
184
+ print(f"{root_name} {root_size}")
185
+
186
+ stats = {"files": 0, "dirs": 0}
187
+ render(str(target), "", 0, args.depth, args.all, args.sort_size, stats)
188
+
189
+ print()
190
+ print(c(f" {stats['dirs']} ディレクトリ, {stats['files']} ファイル", DIM))
191
+
192
+ if __name__ == "__main__":
193
+ main()
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "dirlens",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Directory tree viewer with file sizes / ファイルサイズ付きディレクトリツリー表示ツール",
5
5
  "bin": {
6
6
  "dirlens": "./bin/dirlens"
7
7
  },
8
8
  "files": [
9
9
  "bin/",
10
- "dirlens",
10
+ "dirlens.py",
11
11
  "dirlens.bat",
12
12
  "README.md"
13
13
  ],
@@ -24,4 +24,4 @@
24
24
  "type": "git",
25
25
  "url": "https://github.com/igarinpiano/dirlens.git"
26
26
  }
27
- }
27
+ }