dirlens 1.0.5 → 1.0.6
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 +97 -58
- package/dirlens.py +427 -227
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,44 +5,6 @@
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
## 出力例
|
|
9
|
-
|
|
10
|
-
```
|
|
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
|
-
│ │ └── logo.png (512 KB)
|
|
17
|
-
│ ├── src/ (0 dirs, 1 file, 80 KB)
|
|
18
|
-
│ │ └── util.py (80 KB)
|
|
19
|
-
│ └── main.py (120 KB)
|
|
20
|
-
├── archive.zip (3 MB)
|
|
21
|
-
└── readme.txt (50 KB)
|
|
22
|
-
|
|
23
|
-
5 ディレクトリ, 5 ファイル
|
|
24
|
-
.py ×2 .txt ×1 .zip ×1 .png ×1
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
## 特徴
|
|
30
|
-
|
|
31
|
-
- **クロスプラットフォーム** — macOS / Linux / Windows
|
|
32
|
-
- **カラー表示** — ディレクトリ・ファイル・シンボリックリンクを色で識別
|
|
33
|
-
- **自動サイズ変換** — bytes / KB / MB / GB / TB
|
|
34
|
-
- **ディレクトリサイズ** — サブディレクトリの合計サイズを自動計算
|
|
35
|
-
- **アイテム数表示** — 各ディレクトリの直下にある dirs / files 数を表示
|
|
36
|
-
- **拡張子統計** — ツリー全体のファイル種別を集計してサマリーに表示
|
|
37
|
-
- **`.gitignore` 対応** — `-g` で `node_modules/` などを自動除外
|
|
38
|
-
- **最終更新日時** — `--date` で各ファイル・ディレクトリの更新日時を相対表示
|
|
39
|
-
- **拡張子フィルタ** — `-t py` など指定した拡張子のみ表示
|
|
40
|
-
- **Markdown出力** — `-m` でコードブロック形式に出力、AIチャットにそのままペースト可
|
|
41
|
-
- **隠しファイル対応** — `-a` で表示切り替え(アイテム数・統計にも反映)
|
|
42
|
-
- **サイズ順ソート** — `-s` で大きいものから表示
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
8
|
## インストール
|
|
47
9
|
|
|
48
10
|
### npm(推奨・全プラットフォーム共通)
|
|
@@ -125,6 +87,51 @@ dirlens --help
|
|
|
125
87
|
|
|
126
88
|
---
|
|
127
89
|
|
|
90
|
+
## 出力例
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
Desktop/ (2 dirs, 2 files, 3.74 MB)
|
|
94
|
+
├── EmptyDir/ (0 dirs, 0 files, 0 bytes)
|
|
95
|
+
├── Project/ (2 dirs, 1 file, 712 KB)
|
|
96
|
+
│ ├── assets/ (1 dir, 0 files, 512 KB)
|
|
97
|
+
│ │ └── images/ (0 dirs, 1 file, 512 KB)
|
|
98
|
+
│ │ └── logo.png (512 KB)
|
|
99
|
+
│ ├── src/ (0 dirs, 1 file, 80 KB)
|
|
100
|
+
│ │ └── util.py (80 KB)
|
|
101
|
+
│ └── main.py (120 KB)
|
|
102
|
+
├── archive.zip (3 MB)
|
|
103
|
+
└── readme.txt (50 KB)
|
|
104
|
+
|
|
105
|
+
5 ディレクトリ, 5 ファイル
|
|
106
|
+
.py ×2 .txt ×1 .zip ×1 .png ×1
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 特徴
|
|
112
|
+
|
|
113
|
+
- **クロスプラットフォーム** — macOS / Linux / Windows
|
|
114
|
+
- **カラー表示** — ディレクトリ・ファイル・シンボリックリンクを色で識別
|
|
115
|
+
- **自動サイズ変換** — bytes / KB / MB / GB / TB
|
|
116
|
+
- **ディレクトリサイズ** — サブディレクトリの合計サイズを自動計算
|
|
117
|
+
- **アイテム数表示** — 各ディレクトリの直下にある dirs / files 数を表示
|
|
118
|
+
- **拡張子統計** — ツリー全体のファイル種別を集計してサマリーに表示
|
|
119
|
+
- **`.gitignore` 対応** — `-g` で `node_modules/` などを自動除外(サブディレクトリも対応)
|
|
120
|
+
- **最終更新日時** — `--date` で各ファイル・ディレクトリの更新日時を相対表示
|
|
121
|
+
- **拡張子フィルタ** — `-t py` など指定した拡張子のみ表示
|
|
122
|
+
- **パターンフィルタ** — `--exclude` / `--include` でワイルドカード指定(複数可)
|
|
123
|
+
- **サイズフィルタ** — `--min-size` / `--max-size` で容量による絞り込み
|
|
124
|
+
- **ディスク占有率バー** — `--bar` で親ディレクトリに対する占有率を視覚表示
|
|
125
|
+
- **絵文字アイコン** — `--emoji` で拡張子に応じた絵文字を付与
|
|
126
|
+
- **Markdown出力** — `-m` でコードブロック形式に出力、AIチャットへそのままペースト可
|
|
127
|
+
- **JSON出力** — `--json` で機械可読な構造データを出力、スクリプト連携に
|
|
128
|
+
- **HTMLレポート** — `--html` でブラウザで閲覧できる折りたたみツリーを生成
|
|
129
|
+
- **クリップボードコピー** — `-c` で出力を自動コピー、AIチャットへの貼り付けが一発
|
|
130
|
+
- **隠しファイル対応** — `-a` で表示切り替え(アイテム数・統計にも反映)
|
|
131
|
+
- **サイズ順ソート** — `-s` で大きいものから表示
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
128
135
|
## 使い方
|
|
129
136
|
|
|
130
137
|
```bash
|
|
@@ -134,7 +141,7 @@ dirlens
|
|
|
134
141
|
# 特定のディレクトリを表示
|
|
135
142
|
dirlens ~/Desktop
|
|
136
143
|
|
|
137
|
-
# 深さ 2
|
|
144
|
+
# 深さ 2 階層まで表示
|
|
138
145
|
dirlens -d 2
|
|
139
146
|
|
|
140
147
|
# 隠しファイル・ディレクトリ (.xxx) も表示
|
|
@@ -143,27 +150,50 @@ dirlens -a
|
|
|
143
150
|
# サイズの大きい順に並べる
|
|
144
151
|
dirlens -s
|
|
145
152
|
|
|
146
|
-
# .gitignore
|
|
153
|
+
# .gitignore のファイルを除外(node_modules など)
|
|
147
154
|
dirlens -g
|
|
148
155
|
|
|
149
156
|
# 最終更新日時を相対表示(例: 3日前、2時間前)
|
|
150
157
|
dirlens --date
|
|
151
158
|
|
|
159
|
+
# ディスク占有率バーを表示
|
|
160
|
+
dirlens --bar
|
|
161
|
+
|
|
162
|
+
# 拡張子に応じた絵文字アイコンを表示
|
|
163
|
+
dirlens --emoji
|
|
164
|
+
|
|
152
165
|
# 指定した拡張子のファイルのみ表示
|
|
153
166
|
dirlens -t py
|
|
154
|
-
dirlens -t md
|
|
155
167
|
|
|
156
|
-
#
|
|
168
|
+
# パターンで除外(複数指定可)
|
|
169
|
+
dirlens --exclude '*.log' --exclude 'dist'
|
|
170
|
+
|
|
171
|
+
# パターンで抽出(複数指定可)
|
|
172
|
+
dirlens --include 'test_*'
|
|
173
|
+
|
|
174
|
+
# サイズで絞り込み
|
|
175
|
+
dirlens --min-size 1M # 1MB 以上のみ
|
|
176
|
+
dirlens --max-size 100K # 100KB 以下のみ
|
|
177
|
+
|
|
178
|
+
# Markdown コードブロック形式で出力
|
|
157
179
|
dirlens -m
|
|
158
180
|
|
|
181
|
+
# JSON 形式で出力(スクリプト連携)
|
|
182
|
+
dirlens --json
|
|
183
|
+
|
|
184
|
+
# HTML レポートを生成(デフォルト: dirlens.html)
|
|
185
|
+
dirlens --html
|
|
186
|
+
dirlens --html report.html # ファイル名を指定
|
|
187
|
+
|
|
188
|
+
# 出力をクリップボードにコピー
|
|
189
|
+
dirlens -c
|
|
190
|
+
|
|
159
191
|
# カラーなし(パイプ・ファイル書き出し向け)
|
|
160
192
|
dirlens --no-color
|
|
161
193
|
|
|
162
|
-
#
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
# AI に貼り付けやすい形式で出力
|
|
166
|
-
dirlens -g -m
|
|
194
|
+
# ── AI チャットへの貼り付け(推奨の組み合わせ)────────────────
|
|
195
|
+
# gitignore 除外 → Markdown → クリップボードコピー(そのまま貼れる)
|
|
196
|
+
dirlens -g -m -c
|
|
167
197
|
|
|
168
198
|
# テキストファイルに書き出す
|
|
169
199
|
dirlens --no-color > dirlens.txt
|
|
@@ -173,17 +203,26 @@ dirlens --no-color > dirlens.txt
|
|
|
173
203
|
|
|
174
204
|
## オプション一覧
|
|
175
205
|
|
|
176
|
-
| オプション
|
|
177
|
-
|
|
178
|
-
| `path`
|
|
179
|
-
| `--depth N`
|
|
180
|
-
| `--all`
|
|
181
|
-
| `--sort-size`
|
|
182
|
-
| `--gitignore`
|
|
183
|
-
| `--date`
|
|
184
|
-
| `--type EXT`
|
|
185
|
-
| `--
|
|
186
|
-
| `--
|
|
206
|
+
| オプション | 省略形 | 説明 |
|
|
207
|
+
|---------------------|----------|------------------------------------------------------------|
|
|
208
|
+
| `path` | — | 対象ディレクトリ(省略時はカレント) |
|
|
209
|
+
| `--depth N` | `-d N` | 表示する最大の深さ |
|
|
210
|
+
| `--all` | `-a` | 隠しファイル・ディレクトリも表示 |
|
|
211
|
+
| `--sort-size` | `-s` | サイズが大きい順に並べる |
|
|
212
|
+
| `--gitignore` | `-g` | `.gitignore` に記載されたファイルを除外(サブディレクトリも対応)|
|
|
213
|
+
| `--date` | — | 最終更新日時を相対表示(例: 3日前) |
|
|
214
|
+
| `--type EXT` | `-t EXT` | 指定した拡張子のファイルのみ表示(例: `-t py`) |
|
|
215
|
+
| `--bar` | — | 親ディレクトリに対するディスク占有率バーを表示 |
|
|
216
|
+
| `--emoji` | — | 拡張子に応じた絵文字アイコンを表示 |
|
|
217
|
+
| `--exclude PATTERN` | — | 除外パターン(複数指定可、例: `--exclude '*.log'`) |
|
|
218
|
+
| `--include PATTERN` | — | このパターンのみ表示(複数指定可) |
|
|
219
|
+
| `--min-size SIZE` | — | 指定サイズ以上のファイルのみ表示(例: `1M`, `500K`) |
|
|
220
|
+
| `--max-size SIZE` | — | 指定サイズ以下のファイルのみ表示(例: `10M`) |
|
|
221
|
+
| `--markdown` | `-m` | Markdown コードブロック形式で出力(カラー自動無効) |
|
|
222
|
+
| `--json` | — | JSON 形式で標準出力に出力 |
|
|
223
|
+
| `--html [FILE]` | — | HTML レポートを生成(デフォルト: `dirlens.html`) |
|
|
224
|
+
| `--copy` | `-c` | 出力をクリップボードにコピー |
|
|
225
|
+
| `--no-color` | — | カラー表示を無効化(リダイレクト時に推奨) |
|
|
187
226
|
|
|
188
227
|
---
|
|
189
228
|
|
package/dirlens.py
CHANGED
|
@@ -4,11 +4,7 @@ dirlens – ファイルサイズ付きディレクトリツリー表示ツー
|
|
|
4
4
|
対応環境: macOS / Linux / Windows (Python 3.8+)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import os
|
|
8
|
-
import sys
|
|
9
|
-
import argparse
|
|
10
|
-
import fnmatch
|
|
11
|
-
import datetime
|
|
7
|
+
import io, json, os, sys, argparse, fnmatch, datetime, subprocess
|
|
12
8
|
from pathlib import Path
|
|
13
9
|
|
|
14
10
|
# ─── カラー設定 ──────────────────────────────────────────────
|
|
@@ -18,110 +14,169 @@ def _enable_color():
|
|
|
18
14
|
if os.name == "nt":
|
|
19
15
|
try:
|
|
20
16
|
import ctypes
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
k = ctypes.windll.kernel32
|
|
18
|
+
k.SetConsoleMode(k.GetStdHandle(-11), 7)
|
|
23
19
|
except Exception:
|
|
24
20
|
pass
|
|
25
|
-
return bool(
|
|
26
|
-
|
|
27
|
-
or os.environ.get("TERM_PROGRAM")
|
|
28
|
-
or os.environ.get("TERM")
|
|
29
|
-
or os.environ.get("ANSICON")
|
|
30
|
-
)
|
|
21
|
+
return bool(os.environ.get("WT_SESSION") or os.environ.get("TERM_PROGRAM")
|
|
22
|
+
or os.environ.get("TERM") or os.environ.get("ANSICON"))
|
|
31
23
|
return True
|
|
32
24
|
|
|
33
25
|
USE_COLOR = _enable_color()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
BOLD = "\033[1m"
|
|
37
|
-
DIM = "\033[2m"
|
|
38
|
-
BLUE = "\033[34m"
|
|
39
|
-
CYAN = "\033[36m"
|
|
40
|
-
GREEN = "\033[32m"
|
|
41
|
-
MAGENTA = "\033[35m"
|
|
26
|
+
RESET = "\033[0m"; BOLD = "\033[1m"; DIM = "\033[2m"
|
|
27
|
+
BLUE = "\033[34m"; CYAN = "\033[36m"; GREEN = "\033[32m"; MAGENTA = "\033[35m"
|
|
42
28
|
|
|
43
29
|
def c(text, *codes):
|
|
44
30
|
return ("".join(codes) + text + RESET) if USE_COLOR else text
|
|
45
31
|
|
|
46
32
|
|
|
47
|
-
# ───
|
|
33
|
+
# ─── フォーマット ─────────────────────────────────────────────
|
|
48
34
|
def fmt_size(n):
|
|
49
|
-
if n == 0:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return f"{s} {unit}"
|
|
55
|
-
return f"{n} {'byte' if n == 1 else 'bytes'}"
|
|
56
|
-
|
|
35
|
+
if n == 0: return "0 bytes"
|
|
36
|
+
for unit, f in (("TB",1<<40),("GB",1<<30),("MB",1<<20),("KB",1<<10)):
|
|
37
|
+
if n >= f:
|
|
38
|
+
return f"{str(f'{n/f:.2f}').rstrip('0').rstrip('.')} {unit}"
|
|
39
|
+
return f"{n} {'byte' if n==1 else 'bytes'}"
|
|
57
40
|
|
|
58
|
-
# ─── アイテム数表示 ────────────────────────────────────────────
|
|
59
41
|
def fmt_count(nd, nf):
|
|
60
|
-
|
|
61
|
-
f_str = f"{nf} {'file' if nf == 1 else 'files'}"
|
|
62
|
-
return f"{d}, {f_str}"
|
|
42
|
+
return f"{nd} {'dir' if nd==1 else 'dirs'}, {nf} {'file' if nf==1 else 'files'}"
|
|
63
43
|
|
|
64
|
-
|
|
65
|
-
# ─── 日時表示 ─────────────────────────────────────────────────
|
|
66
44
|
def fmt_date(mtime):
|
|
67
|
-
sec = int((datetime.datetime.now() -
|
|
68
|
-
datetime.datetime.fromtimestamp(mtime)).total_seconds())
|
|
45
|
+
sec = int((datetime.datetime.now() - datetime.datetime.fromtimestamp(mtime)).total_seconds())
|
|
69
46
|
if sec < 60: return "今"
|
|
70
|
-
if sec < 3600: return f"{sec
|
|
71
|
-
if sec < 86400: return f"{sec
|
|
72
|
-
|
|
73
|
-
if
|
|
74
|
-
if
|
|
75
|
-
if
|
|
76
|
-
if
|
|
77
|
-
return f"{
|
|
47
|
+
if sec < 3600: return f"{sec//60}分前"
|
|
48
|
+
if sec < 86400: return f"{sec//3600}時間前"
|
|
49
|
+
d = sec // 86400
|
|
50
|
+
if d == 1: return "昨日"
|
|
51
|
+
if d < 7: return f"{d}日前"
|
|
52
|
+
if d < 30: return f"{d//7}週間前"
|
|
53
|
+
if d < 365: return f"{d//30}ヶ月前"
|
|
54
|
+
return f"{d//365}年前"
|
|
55
|
+
|
|
56
|
+
def fmt_bar(part, total, width=10):
|
|
57
|
+
pct = min(100, int(part * 100 / total)) if total else 0
|
|
58
|
+
filled = round(pct * width / 100)
|
|
59
|
+
return f"[{'█'*filled}{'░'*(width-filled)}]{pct:4d}%"
|
|
60
|
+
|
|
61
|
+
def parse_size(s):
|
|
62
|
+
s = s.strip()
|
|
63
|
+
for sfx, mult in [("TB",1<<40),("GB",1<<30),("MB",1<<20),("KB",1<<10),
|
|
64
|
+
("T",1<<40),("G",1<<30),("M",1<<20),("K",1<<10)]:
|
|
65
|
+
if s.upper().endswith(sfx):
|
|
66
|
+
try: return int(float(s[:-len(sfx)]) * mult)
|
|
67
|
+
except ValueError: break
|
|
68
|
+
try: return int(s)
|
|
69
|
+
except ValueError:
|
|
70
|
+
raise argparse.ArgumentTypeError(f"無効なサイズ: '{s}'(例: 50M, 1G, 500K)")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ─── クリップボード ───────────────────────────────────────────
|
|
74
|
+
def copy_to_clipboard(text):
|
|
75
|
+
try:
|
|
76
|
+
if sys.platform == "darwin":
|
|
77
|
+
subprocess.run(["pbcopy"], input=text.encode(), check=True,
|
|
78
|
+
stderr=subprocess.DEVNULL)
|
|
79
|
+
return True
|
|
80
|
+
if sys.platform == "win32":
|
|
81
|
+
subprocess.run(["clip"], input=text.encode("utf-16"), check=True,
|
|
82
|
+
stderr=subprocess.DEVNULL)
|
|
83
|
+
return True
|
|
84
|
+
for cmd in [["wl-copy"],
|
|
85
|
+
["xclip", "-selection", "clipboard"],
|
|
86
|
+
["xsel", "--clipboard", "--input"]]:
|
|
87
|
+
try:
|
|
88
|
+
subprocess.run(cmd, input=text.encode(), check=True,
|
|
89
|
+
stderr=subprocess.DEVNULL)
|
|
90
|
+
return True
|
|
91
|
+
except FileNotFoundError:
|
|
92
|
+
continue
|
|
93
|
+
return False
|
|
94
|
+
except Exception:
|
|
95
|
+
return False
|
|
96
|
+
|
|
78
97
|
|
|
98
|
+
# ─── 絵文字 ───────────────────────────────────────────────────
|
|
99
|
+
_EMOJI_EXT = {
|
|
100
|
+
".py":"🐍", ".js":"🟨", ".ts":"🔷", ".jsx":"⚛️", ".tsx":"⚛️",
|
|
101
|
+
".rs":"🦀", ".go":"🐹", ".rb":"💎", ".java":"☕", ".kt":"🟣",
|
|
102
|
+
".c":"🔧", ".cpp":"🔧", ".h":"🔧", ".cs":"🔵", ".php":"🐘",
|
|
103
|
+
".swift":"🍊",".dart":"🎯",
|
|
104
|
+
".json":"📋",".yaml":"⚙️",".yml":"⚙️", ".toml":"⚙️", ".xml":"📰",
|
|
105
|
+
".csv":"📊", ".sql":"🗄️", ".db":"🗄️", ".ini":"⚙️", ".env":"🔑",
|
|
106
|
+
".md":"📝", ".txt":"📄", ".pdf":"📕", ".doc":"📘", ".docx":"📘",
|
|
107
|
+
".html":"🌐",".css":"🎨", ".scss":"🎨",
|
|
108
|
+
".png":"🖼️", ".jpg":"🖼️", ".jpeg":"🖼️",".gif":"🖼️",
|
|
109
|
+
".svg":"🎨", ".ico":"🖼️", ".webp":"🖼️",
|
|
110
|
+
".mp4":"🎬", ".mov":"🎬", ".mp3":"🎵", ".wav":"🎵", ".flac":"🎵",
|
|
111
|
+
".zip":"📦", ".tar":"📦", ".gz":"📦", ".rar":"📦", ".7z":"📦",
|
|
112
|
+
".sh":"📜", ".bash":"📜",".zsh":"📜", ".bat":"📜", ".ps1":"📜",
|
|
113
|
+
}
|
|
114
|
+
_EMOJI_NAME = {
|
|
115
|
+
"dockerfile":"🐳","makefile":"⚙️","license":"⚖️",
|
|
116
|
+
".gitignore":"🚫","package.json":"📦","requirements.txt":"📋",
|
|
117
|
+
"pyproject.toml":"⚙️","cargo.toml":"📦","readme.md":"📖",
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
def get_emoji(name, is_dir=False):
|
|
121
|
+
if is_dir: return "📁"
|
|
122
|
+
lower = name.lower()
|
|
123
|
+
return _EMOJI_NAME.get(lower) or _EMOJI_EXT.get(os.path.splitext(lower)[1], "📄")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ─── .gitignore ───────────────────────────────────────────────
|
|
127
|
+
_gi_cache = {}
|
|
79
128
|
|
|
80
|
-
# ─── .gitignore サポート ──────────────────────────────────────
|
|
81
129
|
def load_gitignore(directory):
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if os.path.isfile(
|
|
130
|
+
if directory in _gi_cache: return _gi_cache[directory]
|
|
131
|
+
pats = []
|
|
132
|
+
p = os.path.join(directory, ".gitignore")
|
|
133
|
+
if os.path.isfile(p):
|
|
86
134
|
try:
|
|
87
|
-
with open(
|
|
135
|
+
with open(p, encoding="utf-8", errors="ignore") as f:
|
|
88
136
|
for line in f:
|
|
89
137
|
line = line.strip()
|
|
90
138
|
if line and not line.startswith("#"):
|
|
91
|
-
|
|
92
|
-
except OSError:
|
|
93
|
-
|
|
94
|
-
return
|
|
139
|
+
pats.append(line)
|
|
140
|
+
except OSError: pass
|
|
141
|
+
_gi_cache[directory] = pats
|
|
142
|
+
return pats
|
|
95
143
|
|
|
96
144
|
def is_ignored(name, rel_path, is_dir, patterns):
|
|
97
|
-
"""gitignore パターンにマッチするか判定する(簡易実装)。"""
|
|
98
145
|
rel = rel_path.replace("\\", "/")
|
|
99
146
|
for pat in patterns:
|
|
100
|
-
if pat.startswith("!"):
|
|
101
|
-
continue
|
|
147
|
+
if pat.startswith("!"): continue
|
|
102
148
|
dir_only = pat.endswith("/")
|
|
103
149
|
p = pat.rstrip("/")
|
|
104
|
-
if dir_only and not is_dir:
|
|
105
|
-
continue
|
|
150
|
+
if dir_only and not is_dir: continue
|
|
106
151
|
if p.startswith("/"):
|
|
107
|
-
if fnmatch.fnmatch(rel, p.lstrip("/")):
|
|
108
|
-
return True
|
|
152
|
+
if fnmatch.fnmatch(rel, p.lstrip("/")): return True
|
|
109
153
|
else:
|
|
110
|
-
if fnmatch.fnmatch(name, p):
|
|
111
|
-
|
|
112
|
-
if fnmatch.fnmatch(rel, p):
|
|
113
|
-
return True
|
|
114
|
-
if fnmatch.fnmatch(rel, "*/" + p):
|
|
115
|
-
return True
|
|
154
|
+
if fnmatch.fnmatch(name, p): return True
|
|
155
|
+
if fnmatch.fnmatch(rel, p): return True
|
|
156
|
+
if fnmatch.fnmatch(rel, "*/" + p): return True
|
|
116
157
|
return False
|
|
117
158
|
|
|
159
|
+
def _extend_pats(active_pats, path, cfg):
|
|
160
|
+
"""サブディレクトリの .gitignore を読み込んでパターンを拡張する(ルートは除く)。"""
|
|
161
|
+
if not cfg.use_gitignore: return active_pats
|
|
162
|
+
if os.path.normpath(path) == os.path.normpath(cfg.root): return active_pats
|
|
163
|
+
local = load_gitignore(path)
|
|
164
|
+
if not local: return active_pats
|
|
165
|
+
rel_dir = os.path.relpath(path, cfg.root).replace("\\", "/")
|
|
166
|
+
adjusted = []
|
|
167
|
+
for pat in local:
|
|
168
|
+
if not pat.startswith("!") and pat.startswith("/"):
|
|
169
|
+
adjusted.append("/" + rel_dir + pat)
|
|
170
|
+
else:
|
|
171
|
+
adjusted.append(pat)
|
|
172
|
+
return active_pats + adjusted
|
|
173
|
+
|
|
118
174
|
|
|
119
|
-
# ───
|
|
120
|
-
|
|
175
|
+
# ─── ディレクトリサイズ ───────────────────────────────────────
|
|
176
|
+
_sz_cache = {}
|
|
121
177
|
|
|
122
178
|
def dir_size(path):
|
|
123
|
-
if path in
|
|
124
|
-
return _cache[path]
|
|
179
|
+
if path in _sz_cache: return _sz_cache[path]
|
|
125
180
|
total = 0
|
|
126
181
|
try:
|
|
127
182
|
with os.scandir(path) as it:
|
|
@@ -131,113 +186,92 @@ def dir_size(path):
|
|
|
131
186
|
total += e.stat(follow_symlinks=False).st_size
|
|
132
187
|
elif e.is_dir(follow_symlinks=False):
|
|
133
188
|
total += dir_size(e.path)
|
|
134
|
-
except OSError:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
pass
|
|
138
|
-
_cache[path] = total
|
|
189
|
+
except OSError: pass
|
|
190
|
+
except OSError: pass
|
|
191
|
+
_sz_cache[path] = total
|
|
139
192
|
return total
|
|
140
193
|
|
|
141
194
|
|
|
142
|
-
# ───
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
195
|
+
# ─── 設定クラス ───────────────────────────────────────────────
|
|
196
|
+
class Cfg:
|
|
197
|
+
def __init__(self, args, root):
|
|
198
|
+
self.root = root
|
|
199
|
+
self.max_depth = args.depth
|
|
200
|
+
self.show_all = args.all
|
|
201
|
+
self.by_size = args.sort_size
|
|
202
|
+
self.show_date = args.date
|
|
203
|
+
self.use_gitignore = args.gitignore
|
|
204
|
+
self.show_bar = args.bar
|
|
205
|
+
self.min_size = parse_size(args.min_size) if args.min_size else None
|
|
206
|
+
self.max_size = parse_size(args.max_size) if args.max_size else None
|
|
207
|
+
self.excludes = args.exclude or []
|
|
208
|
+
self.includes = args.include or []
|
|
209
|
+
self.show_emoji = args.emoji
|
|
210
|
+
self.type_ext = ("." + args.type.lstrip(".")).lower() if args.type else None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ─── 共通フィルタリング ───────────────────────────────────────
|
|
214
|
+
def _filter(path, cfg, active_pats):
|
|
215
|
+
"""エントリを取得してフィルタリングする。"""
|
|
216
|
+
try: raw = list(os.scandir(path))
|
|
217
|
+
except PermissionError: return [], []
|
|
218
|
+
|
|
219
|
+
entries = [e for e in raw if cfg.show_all or not e.name.startswith(".")]
|
|
220
|
+
|
|
221
|
+
if active_pats:
|
|
165
222
|
entries = [e for e in entries
|
|
166
223
|
if not is_ignored(e.name,
|
|
167
|
-
os.path.relpath(e.path, root),
|
|
224
|
+
os.path.relpath(e.path, cfg.root),
|
|
168
225
|
e.is_dir(follow_symlinks=False),
|
|
169
|
-
|
|
170
|
-
|
|
226
|
+
active_pats)]
|
|
227
|
+
|
|
228
|
+
dirs = [e for e in entries if e.is_dir(follow_symlinks=False)]
|
|
171
229
|
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
230
|
|
|
231
|
+
if cfg.excludes:
|
|
232
|
+
dirs = [d for d in dirs if not any(fnmatch.fnmatch(d.name, p) for p in cfg.excludes)]
|
|
233
|
+
files = [f for f in files if not any(fnmatch.fnmatch(f.name, p) for p in cfg.excludes)]
|
|
177
234
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
FORK = "├── "
|
|
181
|
-
LAST = "└── "
|
|
182
|
-
BLANK = " "
|
|
183
|
-
|
|
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
|
-
|
|
192
|
-
if max_depth is not None and depth >= max_depth:
|
|
193
|
-
return
|
|
235
|
+
if cfg.includes:
|
|
236
|
+
files = [f for f in files if any(fnmatch.fnmatch(f.name, p) for p in cfg.includes)]
|
|
194
237
|
|
|
195
|
-
|
|
196
|
-
|
|
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 # 新しいリストを作成(親に影響しない)
|
|
238
|
+
if cfg.type_ext:
|
|
239
|
+
files = [f for f in files if os.path.splitext(f.name)[1].lower() == cfg.type_ext]
|
|
209
240
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
241
|
+
if cfg.min_size is not None or cfg.max_size is not None:
|
|
242
|
+
def in_range(e):
|
|
243
|
+
try: sz = e.stat(follow_symlinks=True).st_size
|
|
244
|
+
except OSError: return True
|
|
245
|
+
if cfg.min_size is not None and sz < cfg.min_size: return False
|
|
246
|
+
if cfg.max_size is not None and sz > cfg.max_size: return False
|
|
247
|
+
return True
|
|
248
|
+
files = [f for f in files if in_range(f)]
|
|
215
249
|
|
|
216
|
-
|
|
250
|
+
return dirs, files
|
|
217
251
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
e.is_dir(follow_symlinks=False),
|
|
223
|
-
active_pats)]
|
|
252
|
+
def count_entries(path, cfg, active_pats):
|
|
253
|
+
pats = _extend_pats(active_pats, path, cfg)
|
|
254
|
+
dirs, files = _filter(path, cfg, pats)
|
|
255
|
+
return len(dirs), len(files)
|
|
224
256
|
|
|
225
|
-
dirs = [e for e in entries if e.is_dir(follow_symlinks=False)]
|
|
226
|
-
files = [e for e in entries if not e.is_dir(follow_symlinks=False)]
|
|
227
257
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
258
|
+
# ─── ツリー描画 ───────────────────────────────────────────────
|
|
259
|
+
PIPE = "│ "; FORK = "├── "; LAST = "└── "; BLANK = " "
|
|
260
|
+
|
|
261
|
+
def render(path, prefix, depth, cfg, stats, active_pats):
|
|
262
|
+
if cfg.max_depth is not None and depth >= cfg.max_depth: return
|
|
263
|
+
|
|
264
|
+
cur_pats = _extend_pats(active_pats, path, cfg)
|
|
265
|
+
dirs, files = _filter(path, cfg, cur_pats)
|
|
231
266
|
|
|
232
267
|
def esz(e):
|
|
233
|
-
try:
|
|
268
|
+
try: return e.stat(follow_symlinks=True).st_size
|
|
234
269
|
except OSError: return 0
|
|
235
|
-
|
|
236
270
|
def emtime(e):
|
|
237
|
-
try:
|
|
271
|
+
try: return e.stat(follow_symlinks=True).st_mtime
|
|
238
272
|
except OSError: return 0
|
|
239
273
|
|
|
240
|
-
if by_size:
|
|
274
|
+
if cfg.by_size:
|
|
241
275
|
dirs.sort(key=lambda e: dir_size(e.path), reverse=True)
|
|
242
276
|
files.sort(key=lambda e: esz(e), reverse=True)
|
|
243
277
|
else:
|
|
@@ -245,49 +279,176 @@ def render(path, prefix, depth, opts, stats, active_pats):
|
|
|
245
279
|
files.sort(key=lambda e: e.name.casefold())
|
|
246
280
|
|
|
247
281
|
combined = dirs + files
|
|
282
|
+
cur_dir_size = dir_size(path)
|
|
248
283
|
|
|
249
284
|
for i, entry in enumerate(combined):
|
|
250
285
|
is_last = (i == len(combined) - 1)
|
|
251
|
-
branch
|
|
252
|
-
cont
|
|
286
|
+
branch = LAST if is_last else FORK
|
|
287
|
+
cont = BLANK if is_last else PIPE
|
|
253
288
|
|
|
254
289
|
if entry.is_dir(follow_symlinks=False):
|
|
255
290
|
sz = dir_size(entry.path)
|
|
256
|
-
nd, nf = count_entries(entry.path,
|
|
291
|
+
nd, nf = count_entries(entry.path, cfg, cur_pats)
|
|
257
292
|
stats["dirs"] += 1
|
|
258
293
|
|
|
294
|
+
emoji = (get_emoji(entry.name, is_dir=True) + " ") if cfg.show_emoji else ""
|
|
259
295
|
parts = [fmt_count(nd, nf), fmt_size(sz)]
|
|
260
|
-
if show_date:
|
|
261
|
-
try:
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
pass
|
|
296
|
+
if cfg.show_date:
|
|
297
|
+
try: parts.append(fmt_date(entry.stat(follow_symlinks=False).st_mtime))
|
|
298
|
+
except OSError: pass
|
|
299
|
+
bar = (" " + fmt_bar(sz, cur_dir_size)) if cfg.show_bar and cur_dir_size else ""
|
|
265
300
|
|
|
266
|
-
name = c(f"{entry.name}/", BOLD, CYAN)
|
|
267
|
-
meta = c(f"({', '.join(parts)})", DIM)
|
|
301
|
+
name = c(f"{emoji}{entry.name}/", BOLD, CYAN)
|
|
302
|
+
meta = c(f"({', '.join(parts)}){bar}", DIM)
|
|
268
303
|
print(f"{prefix}{branch}{name} {meta}")
|
|
269
|
-
render(entry.path, prefix + cont, depth + 1,
|
|
304
|
+
render(entry.path, prefix + cont, depth + 1, cfg, stats, cur_pats)
|
|
270
305
|
|
|
271
306
|
else:
|
|
272
307
|
sz = esz(entry)
|
|
273
308
|
sym = " →" if entry.is_symlink() else ""
|
|
274
309
|
stats["files"] += 1
|
|
275
|
-
|
|
276
310
|
ext = os.path.splitext(entry.name)[1].lower()
|
|
277
|
-
|
|
278
|
-
|
|
311
|
+
stats["extensions"][ext or "(no ext)"] = \
|
|
312
|
+
stats["extensions"].get(ext or "(no ext)", 0) + 1
|
|
279
313
|
|
|
314
|
+
emoji = (get_emoji(entry.name) + " ") if cfg.show_emoji else ""
|
|
280
315
|
parts = [fmt_size(sz)]
|
|
281
|
-
if show_date:
|
|
316
|
+
if cfg.show_date:
|
|
282
317
|
mt = emtime(entry)
|
|
283
|
-
if mt:
|
|
284
|
-
|
|
318
|
+
if mt: parts.append(fmt_date(mt))
|
|
319
|
+
bar = (" " + fmt_bar(sz, cur_dir_size)) if cfg.show_bar and cur_dir_size else ""
|
|
285
320
|
|
|
286
|
-
name = c(f"{entry.name}{sym}", MAGENTA if entry.is_symlink() else GREEN)
|
|
287
|
-
meta = c(f"({', '.join(parts)})", DIM)
|
|
321
|
+
name = c(f"{emoji}{entry.name}{sym}", MAGENTA if entry.is_symlink() else GREEN)
|
|
322
|
+
meta = c(f"({', '.join(parts)}){bar}", DIM)
|
|
288
323
|
print(f"{prefix}{branch}{name} {meta}")
|
|
289
324
|
|
|
290
325
|
|
|
326
|
+
# ─── JSON出力 ─────────────────────────────────────────────────
|
|
327
|
+
def build_json_tree(path, depth, cfg, active_pats):
|
|
328
|
+
cur_pats = _extend_pats(active_pats, path, cfg)
|
|
329
|
+
dirs, files = _filter(path, cfg, cur_pats)
|
|
330
|
+
sz = dir_size(path)
|
|
331
|
+
|
|
332
|
+
def esz(e):
|
|
333
|
+
try: return e.stat(follow_symlinks=True).st_size
|
|
334
|
+
except OSError: return 0
|
|
335
|
+
|
|
336
|
+
if cfg.by_size:
|
|
337
|
+
dirs.sort(key=lambda e: dir_size(e.path), reverse=True)
|
|
338
|
+
files.sort(key=lambda e: esz(e), reverse=True)
|
|
339
|
+
else:
|
|
340
|
+
dirs.sort(key=lambda e: e.name.casefold())
|
|
341
|
+
files.sort(key=lambda e: e.name.casefold())
|
|
342
|
+
|
|
343
|
+
children = []
|
|
344
|
+
if cfg.max_depth is None or depth < cfg.max_depth:
|
|
345
|
+
for d in dirs:
|
|
346
|
+
children.append(build_json_tree(d.path, depth + 1, cfg, cur_pats))
|
|
347
|
+
for f in files:
|
|
348
|
+
f_sz = esz(f)
|
|
349
|
+
children.append({
|
|
350
|
+
"name": f.name, "type": "file",
|
|
351
|
+
"size": f_sz, "size_human": fmt_size(f_sz),
|
|
352
|
+
"ext": os.path.splitext(f.name)[1].lower(),
|
|
353
|
+
"path": os.path.relpath(f.path, cfg.root),
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
name = os.path.basename(path) or path
|
|
357
|
+
return {
|
|
358
|
+
"name": name, "type": "directory",
|
|
359
|
+
"size": sz, "size_human": fmt_size(sz),
|
|
360
|
+
"path": os.path.relpath(path, cfg.root) if path != cfg.root else ".",
|
|
361
|
+
"item_count": {"dirs": len(dirs), "files": len(files)},
|
|
362
|
+
"children": children,
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# ─── HTML出力 ─────────────────────────────────────────────────
|
|
367
|
+
def generate_html(root_path, cfg, active_pats):
|
|
368
|
+
def _node(path, depth, cur_pats):
|
|
369
|
+
pats = _extend_pats(cur_pats, path, cfg)
|
|
370
|
+
dirs, files = _filter(path, cfg, pats)
|
|
371
|
+
sz = dir_size(path)
|
|
372
|
+
name = os.path.basename(path) or path
|
|
373
|
+
|
|
374
|
+
def esz(e):
|
|
375
|
+
try: return e.stat(follow_symlinks=True).st_size
|
|
376
|
+
except OSError: return 0
|
|
377
|
+
|
|
378
|
+
if cfg.by_size:
|
|
379
|
+
dirs.sort(key=lambda e: dir_size(e.path), reverse=True)
|
|
380
|
+
files.sort(key=lambda e: esz(e), reverse=True)
|
|
381
|
+
else:
|
|
382
|
+
dirs.sort(key=lambda e: e.name.casefold())
|
|
383
|
+
files.sort(key=lambda e: e.name.casefold())
|
|
384
|
+
|
|
385
|
+
ch = ""
|
|
386
|
+
if cfg.max_depth is None or depth < cfg.max_depth:
|
|
387
|
+
for d in dirs:
|
|
388
|
+
ch += _node(d.path, depth + 1, pats)
|
|
389
|
+
else:
|
|
390
|
+
for d in dirs:
|
|
391
|
+
ch += (f'<div class="item dir-leaf">📁 {d.name}/'
|
|
392
|
+
f' <span class="sz">{fmt_size(dir_size(d.path))}</span></div>\n')
|
|
393
|
+
|
|
394
|
+
for f in files:
|
|
395
|
+
f_sz = esz(f)
|
|
396
|
+
ch += (f'<div class="item file">'
|
|
397
|
+
f'<span class="emoji">{get_emoji(f.name)}</span>'
|
|
398
|
+
f'<span class="fname"> {f.name}</span>'
|
|
399
|
+
f'<span class="sz"> {fmt_size(f_sz)}</span></div>\n')
|
|
400
|
+
|
|
401
|
+
nd, nf = len(dirs), len(files)
|
|
402
|
+
opened = " open" if depth == 0 else ""
|
|
403
|
+
return (f'<details{opened}><summary>📁 <strong>{name}/</strong>'
|
|
404
|
+
f' <span class="sz">({fmt_count(nd, nf)}, {fmt_size(sz)})</span>'
|
|
405
|
+
f'</summary><div class="ch">{ch}</div></details>\n')
|
|
406
|
+
|
|
407
|
+
root_name = os.path.basename(root_path) or root_path
|
|
408
|
+
tree = _node(root_path, 0, active_pats)
|
|
409
|
+
|
|
410
|
+
return f'''<!DOCTYPE html>
|
|
411
|
+
<html lang="ja"><head>
|
|
412
|
+
<meta charset="UTF-8">
|
|
413
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
414
|
+
<title>dirlens — {root_name}</title>
|
|
415
|
+
<style>
|
|
416
|
+
*{{box-sizing:border-box;margin:0;padding:0}}
|
|
417
|
+
body{{font-family:Menlo,Consolas,monospace;font-size:14px;background:#1e1e2e;color:#cdd6f4;padding:24px}}
|
|
418
|
+
h1{{color:#89b4fa;margin-bottom:12px;font-size:18px}}
|
|
419
|
+
#q{{background:#313244;border:1px solid #45475a;color:#cdd6f4;padding:6px 12px;
|
|
420
|
+
border-radius:6px;font-size:13px;margin-bottom:16px;width:280px;outline:none}}
|
|
421
|
+
#q:focus{{border-color:#89b4fa}}
|
|
422
|
+
details{{margin-left:18px}}
|
|
423
|
+
summary{{cursor:pointer;padding:2px 6px;border-radius:4px;list-style:none;
|
|
424
|
+
white-space:nowrap;color:#89dceb}}
|
|
425
|
+
summary::-webkit-details-marker{{display:none}}
|
|
426
|
+
summary::before{{content:"▶ ";font-size:10px;opacity:.4}}
|
|
427
|
+
details[open]>summary::before{{content:"▼ "}}
|
|
428
|
+
summary:hover{{background:rgba(255,255,255,.06)}}
|
|
429
|
+
.ch{{border-left:1px solid rgba(255,255,255,.08);margin-left:10px}}
|
|
430
|
+
.item{{padding:2px 6px;white-space:nowrap;margin-left:18px}}
|
|
431
|
+
.item:hover{{background:rgba(255,255,255,.05);border-radius:4px}}
|
|
432
|
+
.fname{{color:#a6e3a1}}
|
|
433
|
+
.sz{{color:#585b70;font-size:12px}}
|
|
434
|
+
.emoji{{width:1.6em;display:inline-block}}
|
|
435
|
+
.hidden{{display:none!important}}
|
|
436
|
+
</style></head><body>
|
|
437
|
+
<h1>🌳 dirlens — {root_name}</h1>
|
|
438
|
+
<input id="q" type="text" placeholder="ファイル名で検索…" oninput="search(this.value)">
|
|
439
|
+
<div id="tree">{tree}</div>
|
|
440
|
+
<script>
|
|
441
|
+
function search(q){{
|
|
442
|
+
q=q.toLowerCase();
|
|
443
|
+
document.querySelectorAll('.file').forEach(el=>{{
|
|
444
|
+
const n=el.querySelector('.fname')?.textContent.toLowerCase()||'';
|
|
445
|
+
el.classList.toggle('hidden',!!q&&!n.includes(q));
|
|
446
|
+
}});
|
|
447
|
+
if(q) document.querySelectorAll('details').forEach(d=>d.open=true);
|
|
448
|
+
}}
|
|
449
|
+
</script></body></html>'''
|
|
450
|
+
|
|
451
|
+
|
|
291
452
|
# ─── エントリポイント ─────────────────────────────────────────
|
|
292
453
|
def main():
|
|
293
454
|
global USE_COLOR
|
|
@@ -299,86 +460,125 @@ def main():
|
|
|
299
460
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
300
461
|
epilog=(
|
|
301
462
|
"使用例:\n"
|
|
302
|
-
" dirlens
|
|
303
|
-
" dirlens
|
|
304
|
-
" dirlens -
|
|
305
|
-
" dirlens
|
|
306
|
-
" dirlens
|
|
307
|
-
" dirlens
|
|
308
|
-
" dirlens --
|
|
309
|
-
" dirlens
|
|
310
|
-
" dirlens -
|
|
311
|
-
" dirlens
|
|
312
|
-
" dirlens > dirlens.txt dirlens.txtに書き出す"
|
|
463
|
+
" dirlens -g -m -c gitignore除外→Markdown→クリップボードコピー\n"
|
|
464
|
+
" dirlens --bar ディスク占有率バーを表示\n"
|
|
465
|
+
" dirlens --min-size 1M 1MB以上のファイルのみ\n"
|
|
466
|
+
" dirlens --exclude '*.log' パターンで除外(複数指定可)\n"
|
|
467
|
+
" dirlens --include 'test_*' パターンで抽出(複数指定可)\n"
|
|
468
|
+
" dirlens --emoji 絵文字アイコンを表示\n"
|
|
469
|
+
" dirlens --json JSON形式で出力\n"
|
|
470
|
+
" dirlens --html HTMLレポートを生成 (dirlens.html)\n"
|
|
471
|
+
" dirlens --no-color カラーなしで表示\n"
|
|
472
|
+
" dirlens > dirlens.txt dirlens.txt に書き出す"
|
|
313
473
|
),
|
|
314
474
|
)
|
|
315
|
-
|
|
316
|
-
ap.add_argument("
|
|
317
|
-
ap.add_argument("-
|
|
318
|
-
ap.add_argument("-
|
|
319
|
-
ap.add_argument("-
|
|
320
|
-
ap.add_argument("--
|
|
321
|
-
ap.add_argument("
|
|
322
|
-
ap.add_argument("-
|
|
323
|
-
ap.add_argument("
|
|
475
|
+
# ── 既存オプション ────────────────────────────────────────
|
|
476
|
+
ap.add_argument("path", nargs="?", default=".")
|
|
477
|
+
ap.add_argument("-d", "--depth", type=int, metavar="N")
|
|
478
|
+
ap.add_argument("-a", "--all", action="store_true")
|
|
479
|
+
ap.add_argument("-s", "--sort-size", action="store_true")
|
|
480
|
+
ap.add_argument("-g", "--gitignore", action="store_true")
|
|
481
|
+
ap.add_argument("--date", action="store_true")
|
|
482
|
+
ap.add_argument("-t", "--type", metavar="EXT")
|
|
483
|
+
ap.add_argument("-m", "--markdown", action="store_true")
|
|
484
|
+
ap.add_argument("--no-color", action="store_true")
|
|
485
|
+
# ── 新規オプション ────────────────────────────────────────
|
|
486
|
+
ap.add_argument("--bar", action="store_true",
|
|
487
|
+
help="親ディレクトリに対するディスク占有率バーを表示")
|
|
488
|
+
ap.add_argument("--min-size", metavar="SIZE",
|
|
489
|
+
help="指定サイズ以上のファイルのみ表示 (例: 1M, 500K)")
|
|
490
|
+
ap.add_argument("--max-size", metavar="SIZE",
|
|
491
|
+
help="指定サイズ以下のファイルのみ表示 (例: 10M)")
|
|
492
|
+
ap.add_argument("--exclude", metavar="PATTERN", action="append",
|
|
493
|
+
help="除外パターン(複数指定可)")
|
|
494
|
+
ap.add_argument("--include", metavar="PATTERN", action="append",
|
|
495
|
+
help="このパターンのみ表示(複数指定可)")
|
|
496
|
+
ap.add_argument("--emoji", action="store_true",
|
|
497
|
+
help="拡張子に応じた絵文字を表示")
|
|
498
|
+
ap.add_argument("--json", action="store_true",
|
|
499
|
+
help="JSON形式で標準出力に出力")
|
|
500
|
+
ap.add_argument("--html", nargs="?", const="dirlens.html", metavar="FILE",
|
|
501
|
+
help="HTMLレポートを生成 (デフォルト: dirlens.html)")
|
|
502
|
+
ap.add_argument("-c", "--copy", action="store_true",
|
|
503
|
+
help="出力をクリップボードにコピー")
|
|
324
504
|
args = ap.parse_args()
|
|
325
505
|
|
|
326
|
-
if args.no_color or args.markdown:
|
|
506
|
+
if args.no_color or args.markdown or args.json:
|
|
327
507
|
USE_COLOR = False
|
|
328
508
|
|
|
329
509
|
target = Path(args.path).resolve()
|
|
330
510
|
if not target.exists():
|
|
331
|
-
print(f"エラー: '{args.path}' が見つかりません", file=sys.stderr)
|
|
332
|
-
sys.exit(1)
|
|
511
|
+
print(f"エラー: '{args.path}' が見つかりません", file=sys.stderr); sys.exit(1)
|
|
333
512
|
if not target.is_dir():
|
|
334
|
-
print(f"エラー: '{args.path}' はディレクトリではありません", file=sys.stderr)
|
|
335
|
-
sys.exit(1)
|
|
513
|
+
print(f"エラー: '{args.path}' はディレクトリではありません", file=sys.stderr); sys.exit(1)
|
|
336
514
|
|
|
337
|
-
|
|
515
|
+
cfg = Cfg(args, str(target))
|
|
338
516
|
active_pats = load_gitignore(str(target)) if args.gitignore else []
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
517
|
+
|
|
518
|
+
# ── JSON ────────────────────────────────────────────────
|
|
519
|
+
if args.json:
|
|
520
|
+
print(json.dumps(build_json_tree(str(target), 0, cfg, active_pats),
|
|
521
|
+
ensure_ascii=False, indent=2))
|
|
522
|
+
return
|
|
523
|
+
|
|
524
|
+
# ── HTML ────────────────────────────────────────────────
|
|
525
|
+
if args.html:
|
|
526
|
+
out = Path(args.html)
|
|
527
|
+
out.write_text(generate_html(str(target), cfg, active_pats), encoding="utf-8")
|
|
528
|
+
print(f"✓ {out} を生成しました ({fmt_size(out.stat().st_size)})")
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
# ── テキスト出力(--copy はバッファ経由)──────────────────
|
|
532
|
+
if args.copy:
|
|
533
|
+
_buf = io.StringIO()
|
|
534
|
+
_old = sys.stdout
|
|
535
|
+
sys.stdout = _buf
|
|
342
536
|
|
|
343
537
|
if args.markdown:
|
|
344
538
|
print("```")
|
|
345
539
|
|
|
346
|
-
# ルートを表示
|
|
347
540
|
root_sz = dir_size(str(target))
|
|
348
|
-
root_nd, root_nf = count_entries(str(target),
|
|
541
|
+
root_nd, root_nf = count_entries(str(target), cfg, active_pats)
|
|
349
542
|
root_label = target.name if target.name else str(target)
|
|
350
|
-
|
|
351
|
-
parts = [fmt_count(root_nd, root_nf), fmt_size(root_sz)]
|
|
543
|
+
root_parts = [fmt_count(root_nd, root_nf), fmt_size(root_sz)]
|
|
352
544
|
if args.date:
|
|
353
|
-
try:
|
|
354
|
-
|
|
355
|
-
except OSError:
|
|
356
|
-
pass
|
|
545
|
+
try: root_parts.append(fmt_date(target.stat().st_mtime))
|
|
546
|
+
except OSError: pass
|
|
357
547
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
548
|
+
root_emoji = (get_emoji(root_label, is_dir=True) + " ") if args.emoji else ""
|
|
549
|
+
print(f"{c(root_emoji + root_label + '/', BOLD, BLUE)} "
|
|
550
|
+
f"{c('(' + ', '.join(root_parts) + ')', DIM)}")
|
|
361
551
|
|
|
362
552
|
stats = {"files": 0, "dirs": 0, "extensions": {}}
|
|
363
|
-
render(str(target), "", 0,
|
|
553
|
+
render(str(target), "", 0, cfg, stats, active_pats)
|
|
364
554
|
|
|
365
|
-
# ─ サマリー ──────────────────────────────────────────────
|
|
366
555
|
print()
|
|
367
556
|
summary = f" {stats['dirs']} ディレクトリ, {stats['files']} ファイル"
|
|
368
|
-
if args.gitignore:
|
|
369
|
-
|
|
370
|
-
if
|
|
371
|
-
|
|
557
|
+
if args.gitignore: summary += " (.gitignore 適用済み)"
|
|
558
|
+
if cfg.type_ext: summary += f" (フィルタ: {cfg.type_ext})"
|
|
559
|
+
if cfg.excludes: summary += f" (除外: {', '.join(cfg.excludes)})"
|
|
560
|
+
if cfg.includes: summary += f" (抽出: {', '.join(cfg.includes)})"
|
|
561
|
+
if cfg.min_size: summary += f" (最小: {fmt_size(cfg.min_size)})"
|
|
562
|
+
if cfg.max_size: summary += f" (最大: {fmt_size(cfg.max_size)})"
|
|
372
563
|
print(c(summary, DIM))
|
|
373
564
|
|
|
374
565
|
if stats["extensions"]:
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
print(c(ext_line, DIM))
|
|
566
|
+
exts = sorted(stats["extensions"].items(), key=lambda x: -x[1])
|
|
567
|
+
print(c(" " + " ".join(f"{e} ×{n}" for e, n in exts[:8]), DIM))
|
|
378
568
|
|
|
379
569
|
if args.markdown:
|
|
380
570
|
print("```")
|
|
381
571
|
|
|
572
|
+
# ── クリップボードコピー ──────────────────────────────────
|
|
573
|
+
if args.copy:
|
|
574
|
+
sys.stdout = _old
|
|
575
|
+
text = _buf.getvalue()
|
|
576
|
+
print(text, end="")
|
|
577
|
+
ok = copy_to_clipboard(text)
|
|
578
|
+
msg = "✓ クリップボードにコピーしました" if ok \
|
|
579
|
+
else "✗ コピー失敗 (pbcopy / xclip / wl-copy が必要)"
|
|
580
|
+
print(c(msg, BOLD, GREEN if ok else DIM), file=sys.stderr)
|
|
581
|
+
|
|
382
582
|
|
|
383
583
|
if __name__ == "__main__":
|
|
384
584
|
main()
|