Spaces:
Running
Running
File size: 4,196 Bytes
77bf0e5 31d220e 77bf0e5 31d220e 77bf0e5 31d220e 77bf0e5 31d220e 77bf0e5 31d220e 77bf0e5 31d220e 77bf0e5 31d220e 77bf0e5 31d220e 77bf0e5 31d220e 77bf0e5 31d220e 2156541 31d220e 77bf0e5 31d220e 77bf0e5 31d220e 77bf0e5 31d220e a3bdcf1 31d220e 2156541 31d220e 77bf0e5 31d220e 77bf0e5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | """
File Explorer Component - VS Code style file tree.
"""
import streamlit as st
import os
from pathlib import Path
from typing import Dict, List
def build_file_tree(file_paths: List[str], base_path: str = "") -> Dict:
"""Build a nested dictionary representing the file tree."""
tree = {}
for file_path in file_paths:
if base_path:
try:
rel_path = os.path.relpath(file_path, base_path)
except ValueError:
rel_path = file_path
else:
rel_path = file_path
parts = Path(rel_path).parts
current = tree
for i, part in enumerate(parts):
if i == len(parts) - 1:
current[part] = {"_type": "file", "_path": file_path}
else:
if part not in current:
current[part] = {"_type": "dir", "_children": {}}
current = current.get(part, {}).get("_children", current.get(part, {}))
if "_children" not in current:
current = current
return tree
def get_file_icon(filename: str) -> str:
"""Get icon for file based on extension."""
ext = Path(filename).suffix.lower()
icons = {
".py": "π", ".js": "π", ".ts": "π", ".jsx": "βοΈ", ".tsx": "βοΈ",
".html": "π", ".css": "π¨", ".json": "π", ".md": "π",
".yaml": "βοΈ", ".yml": "βοΈ", ".toml": "βοΈ", ".sql": "ποΈ",
".env": "π", ".gitignore": "π«", ".txt": "π",
}
return icons.get(ext, "π")
def render_file_tree(indexed_files: List[str], base_path: str = ""):
"""Render VS Code style file tree."""
if not indexed_files:
st.caption("No files indexed")
return
# Custom CSS for tree styling
# Styles are now handled globally in components/style.py for modularity
st.markdown(f"**π Files** ({len(indexed_files)})")
# Build and render tree
tree = build_file_tree(indexed_files, base_path)
# Initialize expanded state
if "tree_expanded" not in st.session_state:
st.session_state.tree_expanded = set()
# Render tree items
render_tree_items(tree, 0)
def render_tree_items(tree: Dict, depth: int):
"""Render tree items with proper indentation."""
# Sort: directories first, then files
items = [(k, v) for k, v in tree.items() if not k.startswith("_")]
sorted_items = sorted(items, key=lambda x: (x[1].get("_type") == "file", x[0].lower()))
for name, node in sorted_items:
is_file = node.get("_type") == "file"
indent = "β " * depth # Compact indent for sidebar
if is_file:
# File item
file_path = node.get("_path", "")
icon = get_file_icon(name)
is_selected = st.session_state.get("selected_file") == file_path
# Compact button
btn_label = f"{indent}ββ {icon} {name}"
if st.button(btn_label, key=f"tree_{file_path}", use_container_width=True,
type="primary" if is_selected else "secondary"):
st.session_state.selected_file = file_path
st.rerun()
else:
# Directory item
dir_key = f"dir_{depth}_{name}"
is_expanded = dir_key in st.session_state.tree_expanded
arrow = "βΌ" if is_expanded else "βΆ"
btn_label = f"{indent}{arrow} π {name}"
if st.button(btn_label, key=dir_key, use_container_width=True, type="secondary"):
if is_expanded:
st.session_state.tree_expanded.discard(dir_key)
else:
st.session_state.tree_expanded.add(dir_key)
st.rerun()
# Render children if expanded
if is_expanded:
children = node.get("_children", {})
render_tree_items(children, depth + 1)
def get_indexed_files_from_session() -> List[str]:
"""Get indexed files from session state."""
return st.session_state.get("indexed_files", [])
|