from __future__ import annotations
from dataclasses import dataclass
from functools import lru_cache
from os import scandir
import os.path
from rich.console import RenderableType
import rich.repr
from rich.text import Text
from textual.message import Message
from textual._types import MessageTarget
from textual.widgets._tree_control import TreeControl, TreeNode
[docs]
@dataclass
class DirEntry:
path: str
is_dir: bool
[docs]
class DirectoryTree(TreeControl[DirEntry]):
[docs]
@rich.repr.auto
class FileClick(Message, bubble=True):
def __init__(self, sender: MessageTarget, path: str) -> None:
self.path = path
super().__init__(sender)
def __init__(
self,
path: str,
*,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
) -> None:
self.path = os.path.expanduser(path.rstrip("/"))
label = os.path.basename(self.path)
data = DirEntry(self.path, True)
super().__init__(label, data, name=name, id=id, classes=classes)
self.root.tree.guide_style = "black"
[docs]
def render_node(self, node: TreeNode[DirEntry]) -> RenderableType:
return self.render_tree_label(
node,
node.data.is_dir,
node.expanded,
node.is_cursor,
node.id == self.hover_node,
self.has_focus,
)
[docs]
@lru_cache(maxsize=1024 * 32)
def render_tree_label(
self,
node: TreeNode[DirEntry],
is_dir: bool,
expanded: bool,
is_cursor: bool,
is_hover: bool,
has_focus: bool,
) -> RenderableType:
meta = {
"@click": f"click_label({node.id})",
"tree_node": node.id,
"cursor": node.is_cursor,
}
label = Text(node.label) if isinstance(node.label, str) else node.label
if is_hover:
label.stylize("underline")
if is_dir:
label.stylize("bold")
icon = "📂" if expanded else "📁"
else:
icon = "📄"
label.highlight_regex(r"\..*$", "italic")
if label.plain.startswith("."):
label.stylize("dim")
if is_cursor and has_focus:
cursor_style = self.get_component_styles("tree--cursor").rich_style
label.stylize(cursor_style)
icon_label = Text(f"{icon} ", no_wrap=True, overflow="ellipsis") + label
icon_label.apply_meta(meta)
return icon_label
[docs]
def on_styles_updated(self) -> None:
self.render_tree_label.cache_clear()
[docs]
def on_mount(self) -> None:
self.call_later(self.load_directory, self.root)
[docs]
async def load_directory(self, node: TreeNode[DirEntry]):
path = node.data.path
directory = sorted(
list(scandir(path)), key=lambda entry: (not entry.is_dir(), entry.name)
)
for entry in directory:
node.add(entry.name, DirEntry(entry.path, entry.is_dir()))
node.loaded = True
node.expand()
self.refresh(layout=True)
[docs]
async def on_tree_control_node_selected(
self, message: TreeControl.NodeSelected[DirEntry]
) -> None:
dir_entry = message.node.data
if not dir_entry.is_dir:
await self.emit(self.FileClick(self, dir_entry.path))
else:
if not message.node.loaded:
await self.load_directory(message.node)
message.node.expand()
else:
await self.emit(self.FileClick(self, dir_entry.path))
message.node.toggle()