dirlens 1.0.1 → 1.0.3
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.
- package/README.md +15 -13
- package/bin/dirlens +0 -0
- package/dirlens.py +224 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
## 出力例
|
|
9
9
|
|
|
10
10
|
```
|
|
11
|
-
Desktop/ (3.74 MB)
|
|
12
|
-
├── EmptyDir/ (0 bytes)
|
|
13
|
-
├── Project/ (712 KB)
|
|
14
|
-
│ ├── assets/ (512 KB)
|
|
15
|
-
│ │ └── images/ (512 KB)
|
|
11
|
+
Desktop/ (2 dirs, 2 files, 3.74 MB)
|
|
12
|
+
├── EmptyDir/ (0 dirs, 0 files, 0 bytes)
|
|
13
|
+
├── Project/ (2 dirs, 1 file, 712 KB)
|
|
14
|
+
│ ├── assets/ (1 dir, 0 files, 512 KB)
|
|
15
|
+
│ │ └── images/ (0 dirs, 1 file, 512 KB)
|
|
16
16
|
│ │ └── logo.png (512 KB)
|
|
17
|
-
│ ├── src/ (80 KB)
|
|
17
|
+
│ ├── src/ (0 dirs, 1 file, 80 KB)
|
|
18
18
|
│ │ └── util.py (80 KB)
|
|
19
19
|
│ └── main.py (120 KB)
|
|
20
20
|
├── archive.zip (3 MB)
|
|
@@ -31,7 +31,8 @@ Desktop/ (3.74 MB)
|
|
|
31
31
|
- **カラー表示** — ディレクトリ・ファイル・シンボリックリンクを色で識別
|
|
32
32
|
- **自動サイズ変換** — bytes / KB / MB / GB / TB
|
|
33
33
|
- **ディレクトリサイズ** — サブディレクトリの合計サイズを自動計算
|
|
34
|
-
-
|
|
34
|
+
- **アイテム数表示** — 各ディレクトリの直下にある dirs / files 数を表示
|
|
35
|
+
- **隠しファイル対応** — `-a` で表示切り替え(アイテム数にも反映)
|
|
35
36
|
- **サイズ順ソート** — `-s` で大きいものから表示
|
|
36
37
|
|
|
37
38
|
---
|
|
@@ -62,16 +63,16 @@ npm uninstall -g dirlens
|
|
|
62
63
|
|
|
63
64
|
### macOS / Linux(スクリプト直接インストール)
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
# 実行権限を付与
|
|
67
|
-
chmod +x dirlens
|
|
66
|
+
GitHubリポジトリから `dirlens.py` をダウンロードして使用します。
|
|
68
67
|
|
|
68
|
+
```bash
|
|
69
69
|
# /usr/local/bin にインストール(どこからでも呼べるようになる)
|
|
70
|
-
sudo
|
|
70
|
+
sudo install -m 755 dirlens.py /usr/local/bin/dirlens
|
|
71
71
|
|
|
72
72
|
# ── または sudo なしでユーザーローカルにインストール ──
|
|
73
73
|
mkdir -p ~/.local/bin
|
|
74
|
-
cp dirlens ~/.local/bin/
|
|
74
|
+
cp dirlens.py ~/.local/bin/dirlens
|
|
75
|
+
chmod +x ~/.local/bin/dirlens
|
|
75
76
|
|
|
76
77
|
# ~/.zshrc(zsh)または ~/.bashrc(bash)に以下を追記:
|
|
77
78
|
export PATH="$HOME/.local/bin:$PATH"
|
|
@@ -89,7 +90,7 @@ dirlens --help
|
|
|
89
90
|
|
|
90
91
|
### Windows(スクリプト直接インストール)
|
|
91
92
|
|
|
92
|
-
1. `dirlens`
|
|
93
|
+
1. `dirlens.py` と `dirlens.bat` を任意のフォルダへ置く
|
|
93
94
|
(例: `C:\Users\ユーザー名\bin\`)
|
|
94
95
|
|
|
95
96
|
2. 同じフォルダに **`dirlens.bat`** を置く(同梱のものを使用):
|
|
@@ -178,3 +179,4 @@ dirlens --no-color > tree.txt
|
|
|
178
179
|
- **シンボリックリンク先のディレクトリ**は展開せず `→` マークで表示
|
|
179
180
|
- 権限がないディレクトリは `[アクセス拒否]` と表示してスキップ
|
|
180
181
|
- 非常に深いディレクトリ(1万階層以上)は `-d` で深さを制限してください
|
|
182
|
+
- **ホームフォルダ(`~/`)やルート(`/`)で実行すると固まる場合があります** — サイズ計算は `-d` の表示制限に関わらず底まで全再帰するため、`~/Library` や iCloud Drive など大容量・ネットワークマウントのディレクトリで時間がかかります。プロジェクトフォルダなど範囲を絞って実行してください
|
package/bin/dirlens
CHANGED
|
File without changes
|
package/dirlens.py
CHANGED
|
@@ -7,6 +7,8 @@ dirlens – ファイルサイズ付きディレクトリツリー表示ツー
|
|
|
7
7
|
import os
|
|
8
8
|
import sys
|
|
9
9
|
import argparse
|
|
10
|
+
import fnmatch
|
|
11
|
+
import datetime
|
|
10
12
|
from pathlib import Path
|
|
11
13
|
|
|
12
14
|
# ─── カラー設定 ──────────────────────────────────────────────
|
|
@@ -14,7 +16,6 @@ def _enable_color():
|
|
|
14
16
|
if not hasattr(sys.stdout, "isatty") or not sys.stdout.isatty():
|
|
15
17
|
return False
|
|
16
18
|
if os.name == "nt":
|
|
17
|
-
# Windows: VT100モードを有効にする
|
|
18
19
|
try:
|
|
19
20
|
import ctypes
|
|
20
21
|
kernel32 = ctypes.windll.kernel32
|
|
@@ -22,8 +23,8 @@ def _enable_color():
|
|
|
22
23
|
except Exception:
|
|
23
24
|
pass
|
|
24
25
|
return bool(
|
|
25
|
-
os.environ.get("WT_SESSION")
|
|
26
|
-
or os.environ.get("TERM_PROGRAM")
|
|
26
|
+
os.environ.get("WT_SESSION")
|
|
27
|
+
or os.environ.get("TERM_PROGRAM")
|
|
27
28
|
or os.environ.get("TERM")
|
|
28
29
|
or os.environ.get("ANSICON")
|
|
29
30
|
)
|
|
@@ -40,12 +41,11 @@ GREEN = "\033[32m"
|
|
|
40
41
|
MAGENTA = "\033[35m"
|
|
41
42
|
|
|
42
43
|
def c(text, *codes):
|
|
43
|
-
"""ANSIカラーを適用する。カラー無効時はそのまま返す。"""
|
|
44
44
|
return ("".join(codes) + text + RESET) if USE_COLOR else text
|
|
45
45
|
|
|
46
|
+
|
|
46
47
|
# ─── サイズ表示 ───────────────────────────────────────────────
|
|
47
48
|
def fmt_size(n):
|
|
48
|
-
"""バイト数を人が読みやすい文字列に変換する。"""
|
|
49
49
|
if n == 0:
|
|
50
50
|
return "0 bytes"
|
|
51
51
|
for unit, factor in (("TB", 1 << 40), ("GB", 1 << 30), ("MB", 1 << 20), ("KB", 1 << 10)):
|
|
@@ -54,11 +54,72 @@ def fmt_size(n):
|
|
|
54
54
|
return f"{s} {unit}"
|
|
55
55
|
return f"{n} {'byte' if n == 1 else 'bytes'}"
|
|
56
56
|
|
|
57
|
+
|
|
58
|
+
# ─── アイテム数表示 ────────────────────────────────────────────
|
|
59
|
+
def fmt_count(nd, nf):
|
|
60
|
+
d = f"{nd} {'dir' if nd == 1 else 'dirs'}"
|
|
61
|
+
f_str = f"{nf} {'file' if nf == 1 else 'files'}"
|
|
62
|
+
return f"{d}, {f_str}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ─── 日時表示 ─────────────────────────────────────────────────
|
|
66
|
+
def fmt_date(mtime):
|
|
67
|
+
sec = int((datetime.datetime.now() -
|
|
68
|
+
datetime.datetime.fromtimestamp(mtime)).total_seconds())
|
|
69
|
+
if sec < 60: return "今"
|
|
70
|
+
if sec < 3600: return f"{sec // 60}分前"
|
|
71
|
+
if sec < 86400: return f"{sec // 3600}時間前"
|
|
72
|
+
days = sec // 86400
|
|
73
|
+
if days == 1: return "昨日"
|
|
74
|
+
if days < 7: return f"{days}日前"
|
|
75
|
+
if days < 30: return f"{days // 7}週間前"
|
|
76
|
+
if days < 365: return f"{days // 30}ヶ月前"
|
|
77
|
+
return f"{days // 365}年前"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ─── .gitignore サポート ──────────────────────────────────────
|
|
81
|
+
def load_gitignore(directory):
|
|
82
|
+
"""指定ディレクトリの .gitignore パターンを読み込む。"""
|
|
83
|
+
patterns = []
|
|
84
|
+
path = os.path.join(directory, ".gitignore")
|
|
85
|
+
if os.path.isfile(path):
|
|
86
|
+
try:
|
|
87
|
+
with open(path, encoding="utf-8", errors="ignore") as f:
|
|
88
|
+
for line in f:
|
|
89
|
+
line = line.strip()
|
|
90
|
+
if line and not line.startswith("#"):
|
|
91
|
+
patterns.append(line)
|
|
92
|
+
except OSError:
|
|
93
|
+
pass
|
|
94
|
+
return patterns
|
|
95
|
+
|
|
96
|
+
def is_ignored(name, rel_path, is_dir, patterns):
|
|
97
|
+
"""gitignore パターンにマッチするか判定する(簡易実装)。"""
|
|
98
|
+
rel = rel_path.replace("\\", "/")
|
|
99
|
+
for pat in patterns:
|
|
100
|
+
if pat.startswith("!"):
|
|
101
|
+
continue
|
|
102
|
+
dir_only = pat.endswith("/")
|
|
103
|
+
p = pat.rstrip("/")
|
|
104
|
+
if dir_only and not is_dir:
|
|
105
|
+
continue
|
|
106
|
+
if p.startswith("/"):
|
|
107
|
+
if fnmatch.fnmatch(rel, p.lstrip("/")):
|
|
108
|
+
return True
|
|
109
|
+
else:
|
|
110
|
+
if fnmatch.fnmatch(name, p):
|
|
111
|
+
return True
|
|
112
|
+
if fnmatch.fnmatch(rel, p):
|
|
113
|
+
return True
|
|
114
|
+
if fnmatch.fnmatch(rel, "*/" + p):
|
|
115
|
+
return True
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
|
|
57
119
|
# ─── ディレクトリサイズ(キャッシュ付き) ─────────────────────
|
|
58
120
|
_cache = {}
|
|
59
121
|
|
|
60
122
|
def dir_size(path):
|
|
61
|
-
"""ディレクトリ以下の合計バイト数を再帰的に計算する(シンボリックリンクは追わない)。"""
|
|
62
123
|
if path in _cache:
|
|
63
124
|
return _cache[path]
|
|
64
125
|
total = 0
|
|
@@ -77,45 +138,112 @@ def dir_size(path):
|
|
|
77
138
|
_cache[path] = total
|
|
78
139
|
return total
|
|
79
140
|
|
|
141
|
+
|
|
142
|
+
# ─── アイテム数カウント ────────────────────────────────────────
|
|
143
|
+
def count_entries(path, show_all, active_pats, root, type_ext, use_gitignore=False):
|
|
144
|
+
try:
|
|
145
|
+
raw = list(os.scandir(path))
|
|
146
|
+
except OSError:
|
|
147
|
+
return 0, 0
|
|
148
|
+
entries = [e for e in raw if show_all or not e.name.startswith(".")]
|
|
149
|
+
|
|
150
|
+
# count_entries の対象ディレクトリ自身の .gitignore も反映する
|
|
151
|
+
local_pats = active_pats
|
|
152
|
+
if use_gitignore:
|
|
153
|
+
local = load_gitignore(path)
|
|
154
|
+
if local:
|
|
155
|
+
rel_dir = os.path.relpath(path, root).replace("\\", "/")
|
|
156
|
+
adjusted = []
|
|
157
|
+
for pat in local:
|
|
158
|
+
if not pat.startswith("!") and pat.startswith("/"):
|
|
159
|
+
adjusted.append("/" + rel_dir + pat)
|
|
160
|
+
else:
|
|
161
|
+
adjusted.append(pat)
|
|
162
|
+
local_pats = active_pats + adjusted
|
|
163
|
+
|
|
164
|
+
if local_pats:
|
|
165
|
+
entries = [e for e in entries
|
|
166
|
+
if not is_ignored(e.name,
|
|
167
|
+
os.path.relpath(e.path, root),
|
|
168
|
+
e.is_dir(follow_symlinks=False),
|
|
169
|
+
local_pats)]
|
|
170
|
+
nd = sum(1 for e in entries if e.is_dir(follow_symlinks=False))
|
|
171
|
+
files = [e for e in entries if not e.is_dir(follow_symlinks=False)]
|
|
172
|
+
if type_ext:
|
|
173
|
+
files = [f for f in files
|
|
174
|
+
if os.path.splitext(f.name)[1].lower() == type_ext]
|
|
175
|
+
return nd, len(files)
|
|
176
|
+
|
|
177
|
+
|
|
80
178
|
# ─── ツリー描画 ───────────────────────────────────────────────
|
|
81
179
|
PIPE = "│ "
|
|
82
180
|
FORK = "├── "
|
|
83
181
|
LAST = "└── "
|
|
84
182
|
BLANK = " "
|
|
85
183
|
|
|
86
|
-
def render(path, prefix, depth,
|
|
87
|
-
"""
|
|
184
|
+
def render(path, prefix, depth, opts, stats, active_pats):
|
|
185
|
+
"""
|
|
186
|
+
active_pats: 現在の階層で有効な gitignore パターンの累積リスト。
|
|
187
|
+
サブディレクトリに入るたびにローカルの .gitignore を読み込んで追記する。
|
|
188
|
+
リストは新規作成して渡すため、兄弟ディレクトリには影響しない。
|
|
189
|
+
"""
|
|
190
|
+
max_depth, show_all, by_size, show_date, use_gitignore, root, type_ext = opts
|
|
191
|
+
|
|
88
192
|
if max_depth is not None and depth >= max_depth:
|
|
89
193
|
return
|
|
90
194
|
|
|
195
|
+
# このディレクトリの .gitignore を読んでパターンを積み重ねる(ルートは main で読み済み)
|
|
196
|
+
if use_gitignore and depth > 0:
|
|
197
|
+
local = load_gitignore(path)
|
|
198
|
+
if local:
|
|
199
|
+
# アンカーパターン(/xxx)をルートからの相対パスに変換する
|
|
200
|
+
# 例: src/.gitignore の /build → /src/build
|
|
201
|
+
rel_dir = os.path.relpath(path, root).replace("\\", "/")
|
|
202
|
+
adjusted = []
|
|
203
|
+
for pat in local:
|
|
204
|
+
if not pat.startswith("!") and pat.startswith("/"):
|
|
205
|
+
adjusted.append("/" + rel_dir + pat)
|
|
206
|
+
else:
|
|
207
|
+
adjusted.append(pat)
|
|
208
|
+
active_pats = active_pats + adjusted # 新しいリストを作成(親に影響しない)
|
|
209
|
+
|
|
91
210
|
try:
|
|
92
211
|
raw = list(os.scandir(path))
|
|
93
212
|
except PermissionError:
|
|
94
213
|
print(f"{prefix}{LAST}{c('[アクセス拒否]', DIM)}")
|
|
95
214
|
return
|
|
96
215
|
|
|
97
|
-
# 隠しファイルのフィルタ(オプション依存)
|
|
98
216
|
entries = [e for e in raw if show_all or not e.name.startswith(".")]
|
|
99
217
|
|
|
100
|
-
|
|
218
|
+
if active_pats:
|
|
219
|
+
entries = [e for e in entries
|
|
220
|
+
if not is_ignored(e.name,
|
|
221
|
+
os.path.relpath(e.path, root),
|
|
222
|
+
e.is_dir(follow_symlinks=False),
|
|
223
|
+
active_pats)]
|
|
224
|
+
|
|
101
225
|
dirs = [e for e in entries if e.is_dir(follow_symlinks=False)]
|
|
102
226
|
files = [e for e in entries if not e.is_dir(follow_symlinks=False)]
|
|
103
227
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
228
|
+
if type_ext:
|
|
229
|
+
files = [f for f in files
|
|
230
|
+
if os.path.splitext(f.name)[1].lower() == type_ext]
|
|
231
|
+
|
|
232
|
+
def esz(e):
|
|
233
|
+
try: return e.stat(follow_symlinks=True).st_size
|
|
234
|
+
except OSError: return 0
|
|
235
|
+
|
|
236
|
+
def emtime(e):
|
|
237
|
+
try: return e.stat(follow_symlinks=True).st_mtime
|
|
238
|
+
except OSError: return 0
|
|
109
239
|
|
|
110
|
-
# ソート:名前順 or サイズ順
|
|
111
240
|
if by_size:
|
|
112
241
|
dirs.sort(key=lambda e: dir_size(e.path), reverse=True)
|
|
113
|
-
files.sort(key=lambda e:
|
|
242
|
+
files.sort(key=lambda e: esz(e), reverse=True)
|
|
114
243
|
else:
|
|
115
244
|
dirs.sort(key=lambda e: e.name.casefold())
|
|
116
245
|
files.sort(key=lambda e: e.name.casefold())
|
|
117
246
|
|
|
118
|
-
# ディレクトリを先に、次にファイル
|
|
119
247
|
combined = dirs + files
|
|
120
248
|
|
|
121
249
|
for i, entry in enumerate(combined):
|
|
@@ -124,19 +252,41 @@ def render(path, prefix, depth, max_depth, show_all, by_size, stats):
|
|
|
124
252
|
cont = BLANK if is_last else PIPE
|
|
125
253
|
|
|
126
254
|
if entry.is_dir(follow_symlinks=False):
|
|
127
|
-
sz
|
|
255
|
+
sz = dir_size(entry.path)
|
|
256
|
+
nd, nf = count_entries(entry.path, show_all, active_pats, root, type_ext, use_gitignore)
|
|
128
257
|
stats["dirs"] += 1
|
|
258
|
+
|
|
259
|
+
parts = [fmt_count(nd, nf), fmt_size(sz)]
|
|
260
|
+
if show_date:
|
|
261
|
+
try:
|
|
262
|
+
parts.append(fmt_date(entry.stat(follow_symlinks=False).st_mtime))
|
|
263
|
+
except OSError:
|
|
264
|
+
pass
|
|
265
|
+
|
|
129
266
|
name = c(f"{entry.name}/", BOLD, CYAN)
|
|
130
|
-
|
|
131
|
-
print(f"{prefix}{branch}{name} {
|
|
132
|
-
render(entry.path, prefix + cont, depth + 1,
|
|
267
|
+
meta = c(f"({', '.join(parts)})", DIM)
|
|
268
|
+
print(f"{prefix}{branch}{name} {meta}")
|
|
269
|
+
render(entry.path, prefix + cont, depth + 1, opts, stats, active_pats)
|
|
270
|
+
|
|
133
271
|
else:
|
|
134
|
-
sz =
|
|
272
|
+
sz = esz(entry)
|
|
135
273
|
sym = " →" if entry.is_symlink() else ""
|
|
136
274
|
stats["files"] += 1
|
|
275
|
+
|
|
276
|
+
ext = os.path.splitext(entry.name)[1].lower()
|
|
277
|
+
key = ext if ext else "(no ext)"
|
|
278
|
+
stats["extensions"][key] = stats["extensions"].get(key, 0) + 1
|
|
279
|
+
|
|
280
|
+
parts = [fmt_size(sz)]
|
|
281
|
+
if show_date:
|
|
282
|
+
mt = emtime(entry)
|
|
283
|
+
if mt:
|
|
284
|
+
parts.append(fmt_date(mt))
|
|
285
|
+
|
|
137
286
|
name = c(f"{entry.name}{sym}", MAGENTA if entry.is_symlink() else GREEN)
|
|
138
|
-
|
|
139
|
-
print(f"{prefix}{branch}{name} {
|
|
287
|
+
meta = c(f"({', '.join(parts)})", DIM)
|
|
288
|
+
print(f"{prefix}{branch}{name} {meta}")
|
|
289
|
+
|
|
140
290
|
|
|
141
291
|
# ─── エントリポイント ─────────────────────────────────────────
|
|
142
292
|
def main():
|
|
@@ -154,6 +304,10 @@ def main():
|
|
|
154
304
|
" dirlens -d 2 深さ 2 階層まで表示\n"
|
|
155
305
|
" dirlens -a 隠しファイル (.xxx) も表示\n"
|
|
156
306
|
" dirlens -s サイズの大きい順に表示\n"
|
|
307
|
+
" dirlens -g .gitignore のファイルを除外\n"
|
|
308
|
+
" dirlens --date 最終更新日時を表示\n"
|
|
309
|
+
" dirlens -t py .py ファイルのみ表示\n"
|
|
310
|
+
" dirlens -m Markdown コードブロックで出力\n"
|
|
157
311
|
" dirlens --no-color カラーなしで表示"
|
|
158
312
|
),
|
|
159
313
|
)
|
|
@@ -161,10 +315,14 @@ def main():
|
|
|
161
315
|
ap.add_argument("-d", "--depth", type=int, metavar="N", help="表示する最大の深さ")
|
|
162
316
|
ap.add_argument("-a", "--all", action="store_true", help="隠しファイルも表示する")
|
|
163
317
|
ap.add_argument("-s", "--sort-size", action="store_true", help="サイズが大きい順に並べる")
|
|
318
|
+
ap.add_argument("-g", "--gitignore", action="store_true", help=".gitignore のファイルを除外する(サブディレクトリも対応)")
|
|
319
|
+
ap.add_argument("--date", action="store_true", help="最終更新日時を表示する")
|
|
320
|
+
ap.add_argument("-t", "--type", metavar="EXT", help="指定した拡張子のみ表示 (例: py, md)")
|
|
321
|
+
ap.add_argument("-m", "--markdown", action="store_true", help="Markdown コードブロックで出力")
|
|
164
322
|
ap.add_argument("--no-color", action="store_true", help="カラー表示を無効化する")
|
|
165
323
|
args = ap.parse_args()
|
|
166
324
|
|
|
167
|
-
if args.no_color:
|
|
325
|
+
if args.no_color or args.markdown:
|
|
168
326
|
USE_COLOR = False
|
|
169
327
|
|
|
170
328
|
target = Path(args.path).resolve()
|
|
@@ -175,19 +333,51 @@ def main():
|
|
|
175
333
|
print(f"エラー: '{args.path}' はディレクトリではありません", file=sys.stderr)
|
|
176
334
|
sys.exit(1)
|
|
177
335
|
|
|
178
|
-
#
|
|
179
|
-
|
|
336
|
+
# ルートの .gitignore を起点として読み込む
|
|
337
|
+
active_pats = load_gitignore(str(target)) if args.gitignore else []
|
|
338
|
+
type_ext = ("." + args.type.lstrip(".")).lower() if args.type else None
|
|
339
|
+
opts = (args.depth, args.all, args.sort_size, args.date,
|
|
340
|
+
args.gitignore, str(target), type_ext)
|
|
341
|
+
|
|
342
|
+
if args.markdown:
|
|
343
|
+
print("```")
|
|
344
|
+
|
|
345
|
+
# ルートを表示
|
|
346
|
+
root_sz = dir_size(str(target))
|
|
347
|
+
root_nd, root_nf = count_entries(str(target), args.all, active_pats, str(target), type_ext, args.gitignore)
|
|
348
|
+
root_label = target.name if target.name else str(target)
|
|
349
|
+
|
|
350
|
+
parts = [fmt_count(root_nd, root_nf), fmt_size(root_sz)]
|
|
351
|
+
if args.date:
|
|
352
|
+
try:
|
|
353
|
+
parts.append(fmt_date(target.stat().st_mtime))
|
|
354
|
+
except OSError:
|
|
355
|
+
pass
|
|
180
356
|
|
|
181
|
-
root_sz = dir_size(str(target))
|
|
182
357
|
root_name = c(f"{root_label}/", BOLD, BLUE)
|
|
183
|
-
|
|
184
|
-
print(f"{root_name} {
|
|
358
|
+
root_meta = c(f"({', '.join(parts)})", DIM)
|
|
359
|
+
print(f"{root_name} {root_meta}")
|
|
185
360
|
|
|
186
|
-
stats = {"files": 0, "dirs": 0}
|
|
187
|
-
render(str(target), "", 0,
|
|
361
|
+
stats = {"files": 0, "dirs": 0, "extensions": {}}
|
|
362
|
+
render(str(target), "", 0, opts, stats, active_pats)
|
|
188
363
|
|
|
364
|
+
# ─ サマリー ──────────────────────────────────────────────
|
|
189
365
|
print()
|
|
190
|
-
|
|
366
|
+
summary = f" {stats['dirs']} ディレクトリ, {stats['files']} ファイル"
|
|
367
|
+
if args.gitignore:
|
|
368
|
+
summary += " (.gitignore 適用済み)"
|
|
369
|
+
if type_ext:
|
|
370
|
+
summary += f" (フィルタ: {type_ext})"
|
|
371
|
+
print(c(summary, DIM))
|
|
372
|
+
|
|
373
|
+
if stats["extensions"]:
|
|
374
|
+
sorted_exts = sorted(stats["extensions"].items(), key=lambda x: -x[1])
|
|
375
|
+
ext_line = " " + " ".join(f"{ext} ×{n}" for ext, n in sorted_exts[:8])
|
|
376
|
+
print(c(ext_line, DIM))
|
|
377
|
+
|
|
378
|
+
if args.markdown:
|
|
379
|
+
print("```")
|
|
380
|
+
|
|
191
381
|
|
|
192
382
|
if __name__ == "__main__":
|
|
193
383
|
main()
|