initial commit
This commit is contained in:
commit
df2724cf25
|
|
@ -0,0 +1,719 @@
|
|||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
import webview
|
||||
|
||||
html_content = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
color: #333;
|
||||
}
|
||||
.header {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.path-input {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.main-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
.left-panel {
|
||||
width: 35%;
|
||||
border-right: 1px solid #ddd;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
background: #ffffff;
|
||||
}
|
||||
.right-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.files-view {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
background: #ffffff;
|
||||
}
|
||||
.output-view {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-top: 2px solid #ddd;
|
||||
}
|
||||
.output-header {
|
||||
padding: 8px;
|
||||
background: #f5f5f5;
|
||||
border-bottom: 1px solid #ddd;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.output-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
background: #fafafa;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.tree-item {
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #333;
|
||||
}
|
||||
.tree-item:hover {
|
||||
background: #e3f2fd;
|
||||
}
|
||||
.checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #999;
|
||||
border-radius: 3px;
|
||||
margin-right: 6px;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.checkbox.selected {
|
||||
background: #4CAF50;
|
||||
border-color: #4CAF50;
|
||||
}
|
||||
.checkbox.unselected,
|
||||
.checkbox.default {
|
||||
background: #f44336;
|
||||
border-color: #f44336;
|
||||
}
|
||||
.tree-item-text {
|
||||
flex: 1;
|
||||
}
|
||||
.indent {
|
||||
display: inline-block;
|
||||
}
|
||||
.folder-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.expand-icon {
|
||||
width: 16px;
|
||||
margin-right: 4px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.file-item {
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #333;
|
||||
}
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
button:hover {
|
||||
background: #1976D2;
|
||||
}
|
||||
button:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.small-btn {
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.empty-message {
|
||||
color: #999;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.status-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
min-width: 120px;
|
||||
}
|
||||
.tree-item.unreadable,
|
||||
.file-item.unreadable {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<input type="text" id="rootPath" class="path-input" placeholder="Enter root directory path..." />
|
||||
<button id="refreshBtn" onclick="refreshTree()">Refresh</button>
|
||||
<span id="statusText" class="status-text"></span>
|
||||
</div>
|
||||
|
||||
<div class="main-container">
|
||||
<div class="left-panel" id="treeView"></div>
|
||||
|
||||
<div class="right-panel">
|
||||
<div class="files-view" id="filesView">
|
||||
<div class="empty-message">Select a folder from the tree</div>
|
||||
</div>
|
||||
|
||||
<div class="output-view">
|
||||
<div class="output-header">
|
||||
<strong>Output Preview</strong>
|
||||
<div>
|
||||
<button class="small-btn" onclick="generateOutput()">Generate</button>
|
||||
<button class="small-btn" onclick="copyOutput()">Copy All</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="output-content" id="outputContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let fileTree = {};
|
||||
let selectionState = {}; // path -> 'selected', 'unselected', 'default'
|
||||
let currentFolder = null;
|
||||
let collapsedFolders = new Set(); // paths of collapsed folders
|
||||
let unreadablePaths = new Set(); // paths that could not be read on disk
|
||||
|
||||
document.getElementById('rootPath').addEventListener('change', async (e) => {
|
||||
const path = e.target.value;
|
||||
if (path) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await loadTree(path);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function refreshTree() {
|
||||
const path = document.getElementById('rootPath').value;
|
||||
if (path) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await loadTree(path);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
const status = document.getElementById('statusText');
|
||||
const rootInput = document.getElementById('rootPath');
|
||||
const refreshBtn = document.getElementById('refreshBtn');
|
||||
if (status) {
|
||||
status.textContent = isLoading ? 'Loading folder tree...' : '';
|
||||
}
|
||||
if (rootInput) {
|
||||
rootInput.disabled = isLoading;
|
||||
}
|
||||
if (refreshBtn) {
|
||||
refreshBtn.disabled = isLoading;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTree(path) {
|
||||
const result = await pywebview.api.load_tree(path);
|
||||
if (result.error) {
|
||||
alert(result.error);
|
||||
return;
|
||||
}
|
||||
fileTree = result.tree;
|
||||
selectionState = {};
|
||||
unreadablePaths = new Set(result.unreadable || []);
|
||||
initializeCollapsedFolders(fileTree);
|
||||
// Ensure the root node is expanded by default
|
||||
collapsedFolders.delete('');
|
||||
renderTree();
|
||||
// Automatically select the root directory to show its files
|
||||
selectFolder('', fileTree);
|
||||
}
|
||||
|
||||
function getEffectiveState(path) {
|
||||
if (selectionState[path]) {
|
||||
return selectionState[path];
|
||||
}
|
||||
|
||||
// Check parent states
|
||||
const parts = path.split(/[\\/]/);
|
||||
// We need to check up to the root (empty string path)
|
||||
for (let i = parts.length - 1; i >= 0; i--) {
|
||||
const parentPath = parts.slice(0, i).join('/');
|
||||
if (selectionState[parentPath]) {
|
||||
const state = selectionState[parentPath];
|
||||
if (state === 'selected' || state === 'unselected') {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default logical state is "default" (no explicit selection)
|
||||
// but we style it visually as red ("don't copy")
|
||||
return 'default';
|
||||
}
|
||||
|
||||
function initializeCollapsedFolders(tree) {
|
||||
collapsedFolders.clear();
|
||||
function walk(node, basePath) {
|
||||
for (const [name, data] of Object.entries(node)) {
|
||||
if (name === '_files') continue;
|
||||
const fullPath = basePath ? `${basePath}/${name}` : name;
|
||||
collapsedFolders.add(fullPath);
|
||||
if (data && typeof data === 'object') {
|
||||
walk(data, fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(tree, '');
|
||||
}
|
||||
|
||||
function renderTree() {
|
||||
const container = document.getElementById('treeView');
|
||||
container.innerHTML = '';
|
||||
|
||||
// Extract a name for the root node from the input path
|
||||
const rootPathInput = document.getElementById('rootPath').value;
|
||||
let rootName = 'Root';
|
||||
if (rootPathInput) {
|
||||
rootName = rootPathInput.split(/[\\/]/).pop() || rootPathInput;
|
||||
}
|
||||
|
||||
// Manually render the root node so it can be selected
|
||||
const rootPath = '';
|
||||
const rootItem = document.createElement('div');
|
||||
rootItem.className = 'tree-item';
|
||||
|
||||
// Indent 0 for root
|
||||
const indent = document.createElement('span');
|
||||
indent.className = 'indent';
|
||||
indent.style.width = '0px';
|
||||
rootItem.appendChild(indent);
|
||||
|
||||
// Expand/Collapse for root
|
||||
const expandIcon = document.createElement('span');
|
||||
expandIcon.className = 'expand-icon';
|
||||
const isCollapsed = collapsedFolders.has(rootPath);
|
||||
expandIcon.textContent = isCollapsed ? '▶' : '▼';
|
||||
expandIcon.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
toggleCollapse(rootPath);
|
||||
};
|
||||
rootItem.appendChild(expandIcon);
|
||||
|
||||
// Checkbox for root
|
||||
const checkbox = document.createElement('span');
|
||||
checkbox.className = `checkbox ${getEffectiveState(rootPath)}`;
|
||||
checkbox.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
toggleSelection(rootPath);
|
||||
};
|
||||
rootItem.appendChild(checkbox);
|
||||
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'folder-icon';
|
||||
icon.textContent = '📁';
|
||||
rootItem.appendChild(icon);
|
||||
|
||||
const text = document.createElement('span');
|
||||
text.className = 'tree-item-text';
|
||||
text.textContent = rootName;
|
||||
text.onclick = () => selectFolder(rootPath, fileTree);
|
||||
rootItem.appendChild(text);
|
||||
|
||||
container.appendChild(rootItem);
|
||||
|
||||
// Render children of root if not collapsed
|
||||
if (!isCollapsed) {
|
||||
// We pass level 1 for children
|
||||
renderNode(fileTree, container, 1, rootPath);
|
||||
}
|
||||
}
|
||||
|
||||
function renderNode(node, container, level, path) {
|
||||
for (const [name, data] of Object.entries(node)) {
|
||||
if (name === '_files') continue;
|
||||
|
||||
const fullPath = path ? `${path}/${name}` : name;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'tree-item';
|
||||
const isUnreadable = unreadablePaths.has(fullPath);
|
||||
if (isUnreadable) {
|
||||
item.classList.add('unreadable');
|
||||
item.title = 'Some or all contents of this folder could not be read';
|
||||
}
|
||||
|
||||
const indent = document.createElement('span');
|
||||
indent.className = 'indent';
|
||||
indent.style.width = (level * 20) + 'px';
|
||||
item.appendChild(indent);
|
||||
|
||||
// Add expand/collapse icon
|
||||
const expandIcon = document.createElement('span');
|
||||
expandIcon.className = 'expand-icon';
|
||||
const isCollapsed = collapsedFolders.has(fullPath);
|
||||
expandIcon.textContent = isCollapsed ? '▶' : '▼';
|
||||
expandIcon.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
toggleCollapse(fullPath);
|
||||
};
|
||||
item.appendChild(expandIcon);
|
||||
|
||||
const checkbox = document.createElement('span');
|
||||
checkbox.className = `checkbox ${getEffectiveState(fullPath)}`;
|
||||
if (isUnreadable) {
|
||||
checkbox.style.opacity = '0.5';
|
||||
checkbox.style.cursor = 'not-allowed';
|
||||
}
|
||||
checkbox.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
if (isUnreadable) {
|
||||
return; // Don't allow toggling selection for unreadable folders
|
||||
}
|
||||
toggleSelection(fullPath);
|
||||
};
|
||||
item.appendChild(checkbox);
|
||||
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'folder-icon';
|
||||
icon.textContent = '📁';
|
||||
item.appendChild(icon);
|
||||
|
||||
const text = document.createElement('span');
|
||||
text.className = 'tree-item-text';
|
||||
text.textContent = name;
|
||||
text.onclick = () => selectFolder(fullPath, data);
|
||||
item.appendChild(text);
|
||||
|
||||
container.appendChild(item);
|
||||
|
||||
// Only render children if not collapsed
|
||||
if (!isCollapsed && typeof data === 'object' && data !== null) {
|
||||
renderNode(data, container, level + 1, fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCollapse(path) {
|
||||
if (collapsedFolders.has(path)) {
|
||||
collapsedFolders.delete(path);
|
||||
} else {
|
||||
collapsedFolders.add(path);
|
||||
}
|
||||
renderTree();
|
||||
}
|
||||
|
||||
function toggleSelection(path) {
|
||||
const current = selectionState[path] || 'default';
|
||||
|
||||
if (current === 'default' || current === 'unselected') {
|
||||
selectionState[path] = 'selected';
|
||||
} else {
|
||||
selectionState[path] = 'unselected';
|
||||
}
|
||||
|
||||
renderTree();
|
||||
if (currentFolder && currentFolder.path === path) {
|
||||
renderFiles(currentFolder.path, currentFolder.data);
|
||||
}
|
||||
}
|
||||
|
||||
function selectFolder(path, data) {
|
||||
currentFolder = { path, data };
|
||||
renderFiles(path, data);
|
||||
}
|
||||
|
||||
function renderFiles(path, data) {
|
||||
const container = document.getElementById('filesView');
|
||||
container.innerHTML = '';
|
||||
|
||||
const items = [];
|
||||
|
||||
// Helper to correctly form path without leading slash if root
|
||||
const makePath = (name) => path ? `${path}/${name}` : name;
|
||||
|
||||
// Add folders
|
||||
for (const [name, subData] of Object.entries(data)) {
|
||||
if (name !== '_files' && typeof subData === 'object') {
|
||||
items.push({ name, type: 'folder', path: makePath(name) });
|
||||
}
|
||||
}
|
||||
|
||||
// Add files
|
||||
if (data._files) {
|
||||
for (const fileName of data._files) {
|
||||
items.push({ name: fileName, type: 'file', path: makePath(fileName) });
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
container.innerHTML = '<div class="empty-message">Empty folder</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach(item => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'file-item';
|
||||
const isUnreadable = unreadablePaths.has(item.path);
|
||||
if (isUnreadable) {
|
||||
div.classList.add('unreadable');
|
||||
div.title = 'This item could not be read (locked or permission denied)';
|
||||
}
|
||||
|
||||
const checkbox = document.createElement('span');
|
||||
checkbox.className = `checkbox ${getEffectiveState(item.path)}`;
|
||||
if (isUnreadable) {
|
||||
checkbox.style.opacity = '0.5';
|
||||
checkbox.style.cursor = 'not-allowed';
|
||||
}
|
||||
checkbox.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
if (isUnreadable) {
|
||||
return; // Don't allow toggling selection for unreadable items
|
||||
}
|
||||
toggleSelection(item.path);
|
||||
// Re-render the files view to update checkbox colors
|
||||
renderFiles(path, data);
|
||||
};
|
||||
div.appendChild(checkbox);
|
||||
|
||||
const icon = document.createElement('span');
|
||||
icon.textContent = item.type === 'folder' ? '📁 ' : '📄 ';
|
||||
div.appendChild(icon);
|
||||
|
||||
const text = document.createElement('span');
|
||||
text.textContent = item.name;
|
||||
div.appendChild(text);
|
||||
|
||||
container.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
async function generateOutput() {
|
||||
const output = document.getElementById('outputContent');
|
||||
output.textContent = 'Generating...';
|
||||
|
||||
const rootPath = document.getElementById('rootPath').value;
|
||||
const result = await pywebview.api.generate_output(rootPath, selectionState);
|
||||
|
||||
if (result.error) {
|
||||
output.textContent = 'Error: ' + result.error;
|
||||
} else {
|
||||
output.textContent = result.content;
|
||||
}
|
||||
}
|
||||
|
||||
function copyOutput() {
|
||||
const output = document.getElementById('outputContent');
|
||||
const text = output.textContent;
|
||||
|
||||
if (!text || text === 'Generating...') {
|
||||
alert('No output to copy');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer modern Clipboard API when available
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
alert('Copied to clipboard!');
|
||||
}).catch(err => {
|
||||
// Fallback for environments where Clipboard API is not permitted
|
||||
if (fallbackCopyText(text)) {
|
||||
alert('Copied to clipboard!');
|
||||
} else {
|
||||
alert('Failed to copy: ' + err);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Older or restricted webview engines: use execCommand-based fallback
|
||||
if (fallbackCopyText(text)) {
|
||||
alert('Copied to clipboard!');
|
||||
} else {
|
||||
alert('Failed to copy: Clipboard API not available');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackCopyText(text) {
|
||||
try {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.left = '-9999px';
|
||||
textarea.style.top = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
const successful = document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
return successful;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
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()
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
# File Tree Selector
|
||||
|
||||
A desktop GUI tool for browsing a directory tree, selecting files/folders, and exporting their contents as a single concatenated text block — useful for feeding code into LLMs.
|
||||
|
||||
## What it does
|
||||
|
||||
- Displays a navigable file tree for any directory you specify
|
||||
- Green checkbox = include in output, red = exclude
|
||||
- Selection cascades: mark a folder green and all its children are included unless individually excluded
|
||||
- Unreadable/permission-denied files are shown with strikethrough and cannot be selected
|
||||
- "Generate" concatenates all selected files into a preview pane; "Copy All" puts it on your clipboard
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8+
|
||||
- `pywebview` and its platform dependencies (see below)
|
||||
|
||||
### Linux (additional system packages required)
|
||||
|
||||
pywebview on Linux requires GTK and WebKit2 — these cannot be installed via pip:
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt install python3-gi python3-gi-cairo gir1.2-webkit2-4.0
|
||||
|
||||
# If on a newer distro (Ubuntu 24+), try:
|
||||
sudo apt install python3-gi python3-gi-cairo gir1.2-webkit2-4.1
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
No additional system packages required. pywebview uses the built-in WebView2 runtime (included with Windows 10/11).
|
||||
|
||||
### macOS
|
||||
|
||||
No additional system packages required. pywebview uses the built-in WebKit.
|
||||
|
||||
## Running the app
|
||||
|
||||
### Linux / macOS
|
||||
|
||||
```bash
|
||||
bash run.sh
|
||||
```
|
||||
|
||||
The script will create a `.venv`, install Python dependencies, and launch the app. On Linux, it checks for GTK dependencies first and exits with instructions if they're missing.
|
||||
|
||||
### Windows
|
||||
|
||||
```powershell
|
||||
.\run.ps1
|
||||
```
|
||||
|
||||
Run from PowerShell. If you get an execution policy error:
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
|
||||
```
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
.
|
||||
├── alpha.py # Main application (latest version)
|
||||
├── run.sh # Setup + launch script for Linux/macOS
|
||||
├── run.ps1 # Setup + launch script for Windows
|
||||
├── requirements.txt # Python dependencies
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. Launch the app
|
||||
2. Type (or paste) an absolute path into the path bar and press Enter
|
||||
3. The left panel shows the folder tree; click a folder to view its contents on the right
|
||||
4. Click a checkbox to toggle include/exclude — red = excluded, green = included
|
||||
5. Use the root folder checkbox to include/exclude everything at once, then carve out exceptions
|
||||
6. Click **Generate** to preview the output, then **Copy All** to copy to clipboard
|
||||
Binary file not shown.
|
|
@ -0,0 +1,42 @@
|
|||
# PowerShell script to ensure virtual environment exists, install dependencies, and run alpha.py
|
||||
|
||||
# Get the script directory
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
Set-Location $scriptDir
|
||||
|
||||
# Path to virtual environment activation script
|
||||
$venvActivate = ".venv\Scripts\Activate.ps1"
|
||||
|
||||
# Function to create virtual environment and install dependencies
|
||||
function Setup-Venv {
|
||||
Write-Host "Creating virtual environment..." -ForegroundColor Green
|
||||
python -m venv .venv
|
||||
|
||||
if (-Not (Test-Path $venvActivate)) {
|
||||
Write-Host "Error: Failed to create virtual environment." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Activating virtual environment..." -ForegroundColor Green
|
||||
& $venvActivate
|
||||
|
||||
if (Test-Path "requirements.txt") {
|
||||
Write-Host "Installing dependencies from requirements.txt..." -ForegroundColor Green
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
} else {
|
||||
Write-Host "No requirements.txt found, skipping dependency installation." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# Check if virtual environment exists
|
||||
if (Test-Path $venvActivate) {
|
||||
Write-Host "Activating virtual environment..." -ForegroundColor Green
|
||||
& $venvActivate
|
||||
} else {
|
||||
Setup-Venv
|
||||
}
|
||||
|
||||
# Run the application
|
||||
Write-Host "Running alpha.py..." -ForegroundColor Green
|
||||
python alpha.py
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
#!/bin/bash
|
||||
# Cross-platform helper to set up .venv, install dependencies, and run alpha.py
|
||||
|
||||
set -e
|
||||
|
||||
# Get the directory of the script
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
VENV_DIR=".venv"
|
||||
|
||||
# List of essential Python packages
|
||||
REQUIRED_PACKAGES=("pywebview")
|
||||
|
||||
# Function to check Linux GTK dependencies
|
||||
check_gtk_linux() {
|
||||
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
# Try importing gi in python
|
||||
python3 - <<EOF 2>/dev/null
|
||||
try:
|
||||
import gi
|
||||
except ImportError:
|
||||
import sys
|
||||
sys.exit(1)
|
||||
EOF
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "\e[31mError: GTK dependencies missing on Linux.\e[0m"
|
||||
echo "Install them with:"
|
||||
echo " sudo apt install python3-gi python3-gi-cairo gir1.2-webkit2-4.0"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to create virtual environment and install dependencies
|
||||
setup_venv() {
|
||||
echo -e "\e[32mCreating virtual environment...\e[0m"
|
||||
python3 -m venv "$VENV_DIR"
|
||||
|
||||
if [ ! -f "$VENV_DIR/bin/activate" ]; then
|
||||
echo -e "\e[31mError: Failed to create virtual environment.\e[0m"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "\e[32mActivating virtual environment...\e[0m"
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
# Upgrade pip first
|
||||
pip install --upgrade pip
|
||||
|
||||
# Install pywebview and other dependencies
|
||||
for pkg in "${REQUIRED_PACKAGES[@]}"; do
|
||||
pip install "$pkg"
|
||||
done
|
||||
|
||||
# Install packages from requirements.txt if it exists
|
||||
if [ -f "requirements.txt" ]; then
|
||||
echo -e "\e[32mInstalling dependencies from requirements.txt...\e[0m"
|
||||
pip install -r requirements.txt
|
||||
else
|
||||
echo -e "\e[33mNo requirements.txt found, skipping additional dependencies.\e[0m"
|
||||
fi
|
||||
|
||||
echo -e "\e[32mVirtual environment ready.\e[0m"
|
||||
}
|
||||
|
||||
# 1️⃣ Check GTK dependencies on Linux
|
||||
check_gtk_linux
|
||||
|
||||
# 2️⃣ Activate existing venv or create one
|
||||
if [ -f "$VENV_DIR/bin/activate" ]; then
|
||||
echo -e "\e[32mActivating existing virtual environment...\e[0m"
|
||||
source "$VENV_DIR/bin/activate"
|
||||
else
|
||||
setup_venv
|
||||
fi
|
||||
|
||||
# 3️⃣ Run the application
|
||||
echo -e "\e[32mRunning alpha.py...\e[0m"
|
||||
python alpha.py
|
||||
Loading…
Reference in New Issue