import os
import json
from pathlib import Path
import webview
html_content = """
Select a folder from the tree
"""
class API:
def load_tree(self, root_path):
try:
root = Path(root_path)
if not root.exists() or not root.is_dir():
return {"error": "Invalid directory path"}
unreadable = []
tree = self._build_tree(root, root, "", unreadable)
return {"tree": tree, "unreadable": unreadable}
except Exception as e:
return {"error": str(e)}
def _build_tree(self, root, path, relative_path, unreadable):
tree = {}
files = []
# Try to list directory contents; if we can't, record as unreadable and treat as empty
try:
items = list(path.iterdir())
except (PermissionError, OSError):
rel = relative_path or path.name
unreadable.append(rel.replace("\\", "/"))
return tree
# Sort items and handle errors per entry so one bad file/dir
# doesn't cause the whole folder to appear empty
for item in sorted(items, key=lambda x: (not x.is_dir(), x.name.lower())):
item_rel = f"{relative_path}/{item.name}" if relative_path else item.name
try:
if item.is_dir():
tree[item.name] = self._build_tree(root, item, item_rel, unreadable)
else:
files.append(item.name)
except (PermissionError, OSError):
# Skip entries we can't stat or descend into, but record them
unreadable.append(item_rel.replace("\\", "/"))
continue
if files:
tree['_files'] = files
return tree
def generate_output(self, root_path, selection_state):
try:
root = Path(root_path)
if not root.exists():
return {"error": "Invalid directory path"}
output = []
self._collect_files(root, root, selection_state, output)
content = "\n\n".join(output)
return {"content": content}
except Exception as e:
return {"error": str(e)}
def _collect_files(self, root, current_path, selection_state, output):
relative_path = str(current_path.relative_to(root)).replace('\\', '/')
# We always walk the directory tree, but will only include files
# whose effective state is "selected".
try:
items = list(current_path.iterdir())
except (PermissionError, OSError):
return
for item in sorted(items, key=lambda x: (not x.is_dir(), x.name.lower())):
item_relative = str(item.relative_to(root)).replace('\\', '/')
item_state = self._get_effective_state(item_relative, selection_state)
try:
if item.is_dir():
self._collect_files(root, item, selection_state, output)
elif item.is_file():
# Only include files that are effectively selected
if item_state != 'selected':
continue
try:
with open(item, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
file_entry = f"=== {item_relative} ===\n{content}"
output.append(file_entry)
except Exception:
# Skip files we can't open/read
pass
except (PermissionError, OSError):
# Skip entries we can't stat or descend into
continue
def _get_effective_state(self, path, selection_state):
if path in selection_state:
return selection_state[path]
# Check parent states
parts = path.split('/')
# range(len(parts) - 1, 0, -1) stops before 0.
# We need it to include 0 to check '' (root)
for i in range(len(parts) - 1, -1, -1):
parent_path = '/'.join(parts[:i])
if parent_path in selection_state:
state = selection_state[parent_path]
if state in ['selected', 'unselected']:
return state
# Logical default: no explicit or inherited selection
return 'default'
def main():
api = API()
window = webview.create_window(
'File Tree Selector',
html=html_content,
js_api=api,
width=1200,
height=800
)
webview.start()
if __name__ == '__main__':
main()