context_generator/alpha.py

719 lines
25 KiB
Python

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()