dirlens 1.0.4 → 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.
Files changed (3) hide show
  1. package/README.md +97 -58
  2. package/dirlens.py +427 -227
  3. 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
- # Markdown コードブロック形式で出力(AI チャットへのペースト用)
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
- # 組み合わせ例:gitignore 除外 + Python のみ + 日付表示
163
- dirlens -g -t py --date
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` | `-d N` | 表示する最大の深さ |
180
- | `--all` | `-a` | 隠しファイル・ディレクトリも表示 |
181
- | `--sort-size` | `-s` | サイズが大きい順に並べる |
182
- | `--gitignore` | `-g` | `.gitignore` に記載されたファイルを除外(サブディレクトリも対応) |
183
- | `--date` | — | 最終更新日時を相対表示(例: 3日前) |
184
- | `--type EXT` | `-t EXT` | 指定した拡張子のファイルのみ表示(例: `-t py`) |
185
- | `--markdown` | `-m` | Markdown コードブロック形式で出力(カラー自動無効) |
186
- | `--no-color` | — | カラー表示を無効化(リダイレクト時に推奨) |
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
- kernel32 = ctypes.windll.kernel32
22
- kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
17
+ k = ctypes.windll.kernel32
18
+ k.SetConsoleMode(k.GetStdHandle(-11), 7)
23
19
  except Exception:
24
20
  pass
25
- return bool(
26
- os.environ.get("WT_SESSION")
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
- RESET = "\033[0m"
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
- 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
-
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
- 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}"
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 // 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}年前"
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
- """指定ディレクトリの .gitignore パターンを読み込む。"""
83
- patterns = []
84
- path = os.path.join(directory, ".gitignore")
85
- if os.path.isfile(path):
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(path, encoding="utf-8", errors="ignore") as f:
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
- patterns.append(line)
92
- except OSError:
93
- pass
94
- return patterns
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
- return True
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
- _cache = {}
175
+ # ─── ディレクトリサイズ ───────────────────────────────────────
176
+ _sz_cache = {}
121
177
 
122
178
  def dir_size(path):
123
- if path in _cache:
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
- pass
136
- except OSError:
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
- 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:
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
- local_pats)]
170
- nd = sum(1 for e in entries if e.is_dir(follow_symlinks=False))
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
- PIPE = "│ "
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
- # このディレクトリの .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 # 新しいリストを作成(親に影響しない)
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
- try:
211
- raw = list(os.scandir(path))
212
- except PermissionError:
213
- print(f"{prefix}{LAST}{c('[アクセス拒否]', DIM)}")
214
- return
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
- entries = [e for e in raw if show_all or not e.name.startswith(".")]
250
+ return dirs, files
217
251
 
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)]
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
- if type_ext:
229
- files = [f for f in files
230
- if os.path.splitext(f.name)[1].lower() == type_ext]
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: return e.stat(follow_symlinks=True).st_size
268
+ try: return e.stat(follow_symlinks=True).st_size
234
269
  except OSError: return 0
235
-
236
270
  def emtime(e):
237
- try: return e.stat(follow_symlinks=True).st_mtime
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 = LAST if is_last else FORK
252
- cont = BLANK if is_last else PIPE
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, show_all, active_pats, root, type_ext, use_gitignore)
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
- parts.append(fmt_date(entry.stat(follow_symlinks=False).st_mtime))
263
- except OSError:
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, opts, stats, active_pats)
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
- key = ext if ext else "(no ext)"
278
- stats["extensions"][key] = stats["extensions"].get(key, 0) + 1
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
- parts.append(fmt_date(mt))
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 カレントディレクトリを表示\n"
303
- " dirlens ~/Desktop 指定したディレクトリを表示\n"
304
- " dirlens -d 2 深さ 2 階層まで表示\n"
305
- " dirlens -a 隠しファイル (.xxx) も表示\n"
306
- " dirlens -s サイズの大きい順に表示\n"
307
- " dirlens -g .gitignore のファイルを除外\n"
308
- " dirlens --date 最終更新日時を表示\n"
309
- " dirlens -t py .py ファイルのみ表示\n"
310
- " dirlens -m Markdown コードブロックで出力\n"
311
- " dirlens --no-color カラーなしで表示"
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
- ap.add_argument("path", nargs="?", default=".", help="対象ディレクトリ(省略時はカレント)")
316
- ap.add_argument("-d", "--depth", type=int, metavar="N", help="表示する最大の深さ")
317
- ap.add_argument("-a", "--all", action="store_true", help="隠しファイルも表示する")
318
- ap.add_argument("-s", "--sort-size", action="store_true", help="サイズが大きい順に並べる")
319
- ap.add_argument("-g", "--gitignore", action="store_true", help=".gitignore のファイルを除外する(サブディレクトリも対応)")
320
- ap.add_argument("--date", action="store_true", help="最終更新日時を表示する")
321
- ap.add_argument("-t", "--type", metavar="EXT", help="指定した拡張子のみ表示 (例: py, md)")
322
- ap.add_argument("-m", "--markdown", action="store_true", help="Markdown コードブロックで出力")
323
- ap.add_argument("--no-color", action="store_true", help="カラー表示を無効化する")
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
- # ルートの .gitignore を起点として読み込む
515
+ cfg = Cfg(args, str(target))
338
516
  active_pats = load_gitignore(str(target)) if args.gitignore else []
339
- type_ext = ("." + args.type.lstrip(".")).lower() if args.type else None
340
- opts = (args.depth, args.all, args.sort_size, args.date,
341
- args.gitignore, str(target), type_ext)
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), args.all, active_pats, str(target), type_ext, args.gitignore)
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
- parts.append(fmt_date(target.stat().st_mtime))
355
- except OSError:
356
- pass
545
+ try: root_parts.append(fmt_date(target.stat().st_mtime))
546
+ except OSError: pass
357
547
 
358
- root_name = c(f"{root_label}/", BOLD, BLUE)
359
- root_meta = c(f"({', '.join(parts)})", DIM)
360
- print(f"{root_name} {root_meta}")
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, opts, stats, active_pats)
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
- summary += " (.gitignore 適用済み)"
370
- if type_ext:
371
- summary += f" (フィルタ: {type_ext})"
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
- sorted_exts = sorted(stats["extensions"].items(), key=lambda x: -x[1])
376
- ext_line = " " + " ".join(f"{ext} ×{n}" for ext, n in sorted_exts[:8])
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()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dirlens",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Directory tree viewer with file sizes / ファイルサイズ付きディレクトリツリー表示ツール",
5
5
  "bin": {
6
6
  "dirlens": "./bin/dirlens"