feat: add directory tree visualization
Renders a visual tree with file sizes, configurable depth, and hidden file filtering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
461bdc404e
commit
35ededc06b
1 changed files with 83 additions and 0 deletions
83
luminos_lib/tree.py
Normal file
83
luminos_lib/tree.py
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
"""Directory tree visualization."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def build_tree(path, max_depth=3, show_hidden=False, _depth=0):
|
||||||
|
"""Build a nested dict representing the directory tree with file sizes."""
|
||||||
|
name = os.path.basename(path) or path
|
||||||
|
node = {"name": name, "path": path, "type": "directory", "children": []}
|
||||||
|
|
||||||
|
try:
|
||||||
|
entries = sorted(os.listdir(path))
|
||||||
|
except PermissionError:
|
||||||
|
node["error"] = "permission denied"
|
||||||
|
return node
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
if not show_hidden and entry.startswith("."):
|
||||||
|
continue
|
||||||
|
full = os.path.join(path, entry)
|
||||||
|
if os.path.isdir(full):
|
||||||
|
if _depth < max_depth:
|
||||||
|
child = build_tree(full, max_depth, show_hidden, _depth + 1)
|
||||||
|
node["children"].append(child)
|
||||||
|
else:
|
||||||
|
node["children"].append({
|
||||||
|
"name": entry, "path": full,
|
||||||
|
"type": "directory", "truncated": True,
|
||||||
|
})
|
||||||
|
elif os.path.isfile(full):
|
||||||
|
try:
|
||||||
|
size = os.path.getsize(full)
|
||||||
|
except OSError:
|
||||||
|
size = 0
|
||||||
|
node["children"].append({
|
||||||
|
"name": entry, "path": full,
|
||||||
|
"type": "file", "size": size,
|
||||||
|
})
|
||||||
|
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
def _human_size(nbytes):
|
||||||
|
"""Convert bytes to human-readable string."""
|
||||||
|
for unit in ("B", "KB", "MB", "GB", "TB"):
|
||||||
|
if nbytes < 1024:
|
||||||
|
if unit == "B":
|
||||||
|
return f"{nbytes} {unit}"
|
||||||
|
return f"{nbytes:.1f} {unit}"
|
||||||
|
nbytes /= 1024
|
||||||
|
return f"{nbytes:.1f} PB"
|
||||||
|
|
||||||
|
|
||||||
|
def render_tree(node, prefix="", is_last=True, is_root=True):
|
||||||
|
"""Render the tree dict as a visual string."""
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
if is_root:
|
||||||
|
lines.append(f"{node['name']}/")
|
||||||
|
else:
|
||||||
|
connector = "\u2514\u2500\u2500 " if is_last else "\u251c\u2500\u2500 "
|
||||||
|
label = node["name"]
|
||||||
|
if node["type"] == "file":
|
||||||
|
label += f" ({_human_size(node.get('size', 0))})"
|
||||||
|
elif node.get("truncated"):
|
||||||
|
label += "/ ..."
|
||||||
|
elif node["type"] == "directory":
|
||||||
|
label += "/"
|
||||||
|
if node.get("error"):
|
||||||
|
label += f" [{node['error']}]"
|
||||||
|
lines.append(prefix + connector + label)
|
||||||
|
|
||||||
|
children = node.get("children", [])
|
||||||
|
for i, child in enumerate(children):
|
||||||
|
if is_root:
|
||||||
|
child_prefix = ""
|
||||||
|
else:
|
||||||
|
child_prefix = prefix + (" " if is_last else "\u2502 ")
|
||||||
|
lines.append(
|
||||||
|
render_tree(child, child_prefix, i == len(children) - 1, False)
|
||||||
|
)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
Loading…
Reference in a new issue