feat: wire up sidebar panels and REPL with IPC handlers, preload channels, and CSS

This commit is contained in:
2026-03-04 16:17:38 +05:30
parent 59ef2028f8
commit d3afd7ad86
6 changed files with 381 additions and 4 deletions
+12
View File
@@ -1536,6 +1536,18 @@
</div> </div>
</div> </div>
<!-- Bottom Panel (REPL Output) -->
<div class="bottom-panel collapsed" id="bottom-panel">
<div class="bottom-panel-header">
<span class="bottom-panel-title">Output</span>
<div class="bottom-panel-actions">
<button class="bottom-panel-btn" id="repl-clear" title="Clear output">Clear</button>
<button class="bottom-panel-btn" id="bottom-panel-toggle" title="Toggle panel">&#x25BC;</button>
</div>
</div>
<div class="bottom-panel-content" id="repl-output"></div>
</div>
<div class="status-bar" id="status-bar"> <div class="status-bar" id="status-bar">
<div class="status-bar-left"> <div class="status-bar-left">
<span class="status-item" id="status-text">Ready</span> <span class="status-item" id="status-text">Ready</span>
+160
View File
@@ -3926,3 +3926,163 @@ ipcMain.on('insert-generated-content', (event, content) => {
mainWindow.webContents.send('insert-content', content); mainWindow.webContents.send('insert-content', content);
} }
}); });
// ============================================
// File Explorer IPC Handlers
// ============================================
ipcMain.handle('list-directory', async (event, dirPath) => {
try {
if (!dirPath) {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory']
});
if (result.canceled || !result.filePaths[0]) return null;
dirPath = result.filePaths[0];
}
const entries = fs.readdirSync(dirPath, { withFileTypes: true })
.filter(e => !e.name.startsWith('.'))
.sort((a, b) => {
if (a.isDirectory() && !b.isDirectory()) return -1;
if (!a.isDirectory() && b.isDirectory()) return 1;
return a.name.localeCompare(b.name);
})
.map(e => ({
name: e.name,
isDirectory: e.isDirectory(),
path: path.join(dirPath, e.name)
}));
return { path: dirPath, entries };
} catch (err) {
console.error('list-directory error:', err);
return null;
}
});
// Open a file by path (from explorer panel)
ipcMain.on('open-file-path', (event, filePath) => {
try {
if (!fs.existsSync(filePath)) return;
const stat = fs.statSync(filePath);
if (stat.size > MAX_FILE_SIZE) return;
currentFile = filePath;
const content = fs.readFileSync(filePath, 'utf-8');
mainWindow.webContents.send('file-opened', { path: filePath, content });
} catch (err) {
console.error('open-file-path error:', err);
}
});
// ============================================
// Git IPC Handlers
// ============================================
const simpleGit = require('simple-git');
function getGitRepo() {
// Use the directory of the current file, or the app's working directory
const dir = currentFile ? path.dirname(currentFile) : process.cwd();
return simpleGit(dir);
}
ipcMain.handle('git-status', async () => {
try {
const git = getGitRepo();
return await git.status();
} catch (err) {
return { error: 'Not a git repository' };
}
});
ipcMain.handle('git-stage', async (event, { files }) => {
try {
const git = getGitRepo();
await git.add(files);
return await git.status();
} catch (err) {
return { error: err.message };
}
});
ipcMain.handle('git-commit', async (event, { message }) => {
try {
const git = getGitRepo();
return await git.commit(message);
} catch (err) {
return { error: err.message };
}
});
ipcMain.handle('git-log', async () => {
try {
const git = getGitRepo();
return await git.log({ maxCount: 20 });
} catch (err) {
return { error: err.message };
}
});
// ============================================
// Snippets IPC Handlers
// ============================================
const snippetsPath = path.join(app.getPath('userData'), 'snippets.json');
function loadSnippets() {
try {
if (fs.existsSync(snippetsPath)) {
return JSON.parse(fs.readFileSync(snippetsPath, 'utf-8'));
}
} catch (err) { console.error('Failed to load snippets:', err); }
return [];
}
function saveSnippetsFile(snippets) {
fs.writeFileSync(snippetsPath, JSON.stringify(snippets, null, 2));
}
ipcMain.handle('get-snippets', async () => loadSnippets());
ipcMain.handle('save-snippet', async (event, snippet) => {
const snippets = loadSnippets();
const existing = snippets.findIndex(s => s.id === snippet.id);
if (existing >= 0) snippets[existing] = snippet;
else snippets.push(snippet);
saveSnippetsFile(snippets);
return snippets;
});
ipcMain.handle('delete-snippet', async (event, id) => {
const snippets = loadSnippets().filter(s => s.id !== id);
saveSnippetsFile(snippets);
return snippets;
});
// ============================================
// Code Execution (REPL) IPC Handler
// ============================================
ipcMain.handle('execute-code', async (event, { code, language }) => {
const timeout = 10000;
return new Promise((resolve) => {
let cmd, args;
if (language === 'javascript' || language === 'js') {
cmd = 'node';
args = ['-e', code];
} else if (language === 'python' || language === 'py') {
cmd = process.platform === 'win32' ? 'python' : 'python3';
args = ['-c', code];
} else if (language === 'bash' || language === 'sh') {
cmd = process.platform === 'win32' ? 'cmd' : 'bash';
args = process.platform === 'win32' ? ['/c', code] : ['-c', code];
} else {
resolve({ error: `Unsupported language: ${language}` });
return;
}
execFile(cmd, args, { timeout, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
resolve({
stdout: stdout || '',
stderr: stderr || '',
error: err?.killed ? 'Execution timed out (10s limit)' : (err?.message || null)
});
});
});
});
+21 -1
View File
@@ -97,7 +97,27 @@ const ALLOWED_SEND_CHANNELS = [
'save-pasted-image', 'save-pasted-image',
// Templates // Templates
'load-template' 'load-template',
// File Explorer
'list-directory',
// Git
'git-status',
'git-stage',
'git-commit',
'git-log',
// Snippets
'get-snippets',
'save-snippet',
'delete-snippet',
// Code execution (REPL)
'execute-code',
// File open by path
'open-file-path'
]; ];
const ALLOWED_RECEIVE_CHANNELS = [ const ALLOWED_RECEIVE_CHANNELS = [
+46 -3
View File
@@ -12,6 +12,10 @@ const { createEditor } = require('./editor/codemirror-setup');
const { undo, redo } = require('@codemirror/commands'); const { undo, redo } = require('@codemirror/commands');
const { SidebarManager } = require('./sidebar/sidebar-manager'); const { SidebarManager } = require('./sidebar/sidebar-manager');
const { renderTemplatesPanel } = require('./sidebar/templates-panel'); const { renderTemplatesPanel } = require('./sidebar/templates-panel');
const { renderExplorerPanel } = require('./sidebar/explorer-panel');
const { renderGitPanel } = require('./sidebar/git-panel');
const { renderSnippetsPanel } = require('./sidebar/snippets-panel');
const { ReplPanel } = require('./repl/repl-panel');
const { CommandPalette } = require('./command-palette'); const { CommandPalette } = require('./command-palette');
const { PrintPreview } = require('./print-preview'); const { PrintPreview } = require('./print-preview');
@@ -488,6 +492,26 @@ class TabManager {
pre.parentElement.replaceChild(img, pre); pre.parentElement.replaceChild(img, pre);
}); });
// Add Run buttons to executable code blocks (REPL)
const codeBlocks = preview.querySelectorAll('pre code[class*="language-"]');
codeBlocks.forEach(block => {
const lang = block.className.match(/language-(\w+)/)?.[1];
if (['javascript', 'js', 'python', 'py', 'bash', 'sh'].includes(lang)) {
const runBtn = document.createElement('button');
runBtn.className = 'code-run-btn';
runBtn.textContent = '\u25B6 Run';
runBtn.addEventListener('click', async () => {
if (replPanel) {
replPanel.show();
const result = await ipcRenderer.invoke('execute-code', { code: block.textContent, language: lang });
replPanel.appendOutput(`[${lang}]`, result);
}
});
block.parentElement.style.position = 'relative';
block.parentElement.appendChild(runBtn);
}
});
} catch (error) { } catch (error) {
console.error('Error rendering preview:', error); console.error('Error rendering preview:', error);
preview.innerHTML = '<p class="error">Error rendering preview. Please check your markdown syntax.</p>'; preview.innerHTML = '<p class="error">Error rendering preview. Please check your markdown syntax.</p>';
@@ -1118,23 +1142,42 @@ class TabManager {
// Initialize tab manager // Initialize tab manager
let tabManager; let tabManager;
let replPanel;
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
tabManager = new TabManager(); tabManager = new TabManager();
replPanel = new ReplPanel();
// Initialize sidebar // Initialize sidebar
const sidebarManager = new SidebarManager(); const sidebarManager = new SidebarManager();
let explorerCurrentDir = null;
sidebarManager.registerPanel('explorer', { sidebarManager.registerPanel('explorer', {
title: 'Explorer', title: 'Explorer',
render: (container) => { container.innerHTML = '<p style="color:#888;padding:8px;">File explorer coming soon...</p>'; } render: (container) => renderExplorerPanel(container, {
listDirectory: (dir) => ipcRenderer.invoke('list-directory', dir),
onFileOpen: (filePath) => ipcRenderer.send('open-file-path', filePath),
currentDir: explorerCurrentDir,
})
}); });
sidebarManager.registerPanel('git', { sidebarManager.registerPanel('git', {
title: 'Git', title: 'Git',
render: (container) => { container.innerHTML = '<p style="color:#888;padding:8px;">Git panel coming soon...</p>'; } render: (container) => renderGitPanel(container, {
gitStatus: () => ipcRenderer.invoke('git-status'),
gitDiff: (file) => ipcRenderer.invoke('git-diff', { file }),
gitStage: (files) => ipcRenderer.invoke('git-stage', { files }),
gitCommit: (message) => ipcRenderer.invoke('git-commit', { message }),
gitLog: () => ipcRenderer.invoke('git-log'),
})
}); });
sidebarManager.registerPanel('snippets', { sidebarManager.registerPanel('snippets', {
title: 'Snippets', title: 'Snippets',
render: (container) => { container.innerHTML = '<p style="color:#888;padding:8px;">Snippets coming soon...</p>'; } render: (container) => renderSnippetsPanel(container, {
getSnippets: () => ipcRenderer.invoke('get-snippets'),
saveSnippet: (s) => ipcRenderer.invoke('save-snippet', s),
deleteSnippet: (id) => ipcRenderer.invoke('delete-snippet', id),
onInsert: (code) => tabManager.insertAtCursor(code),
})
}); });
sidebarManager.registerPanel('templates', { sidebarManager.registerPanel('templates', {
title: 'Templates', title: 'Templates',
+105
View File
@@ -161,3 +161,108 @@ body[class*="dark"] .panel-list-item-title {
body[class*="dark"] .panel-list-item-desc { body[class*="dark"] .panel-list-item-desc {
color: #888; color: #888;
} }
/* Explorer Panel */
.explorer-toolbar {
display: flex;
gap: 4px;
margin-bottom: 8px;
}
.explorer-path {
flex: 1;
padding: 6px 8px;
border: 1px solid var(--gray-300, #d1d5db);
border-radius: 6px;
font-size: 12px;
background: white;
overflow: hidden;
text-overflow: ellipsis;
}
.explorer-browse-btn {
border: 1px solid var(--gray-300, #d1d5db);
border-radius: 6px;
background: white;
cursor: pointer;
padding: 4px 8px;
}
.tree-item {
padding: 3px 0;
cursor: pointer;
font-size: 13px;
user-select: none;
}
.tree-item:hover {
background: var(--gray-100, #f3f4f6);
border-radius: 4px;
}
.tree-icon {
margin-right: 4px;
font-size: 12px;
}
.tree-children {
padding-left: 16px;
}
.tree-folder.collapsed .tree-children {
display: none;
}
.tree-name {
font-size: 13px;
}
/* Git Panel */
.git-section { margin-bottom: 16px; }
.git-section-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--gray-500); margin-bottom: 8px; }
.git-file { display: flex; align-items: center; padding: 4px 6px; border-radius: 4px; font-size: 13px; gap: 6px; }
.git-file:hover { background: var(--gray-100, #f3f4f6); }
.git-file-status { font-weight: 700; font-family: monospace; width: 16px; text-align: center; }
.git-file-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.git-stage-btn { border: none; background: var(--gray-200); border-radius: 4px; cursor: pointer; font-size: 14px; width: 22px; height: 22px; }
.git-commit-input { width: 100%; padding: 8px; border: 1px solid var(--gray-300); border-radius: 6px; font-size: 13px; font-family: inherit; resize: vertical; box-sizing: border-box; }
.git-commit-btn { width: 100%; margin-top: 8px; padding: 8px; background: var(--primary-dark, #5661b3); color: white; border: none; border-radius: 6px; font-size: 13px; cursor: pointer; }
.git-commit-btn:hover { opacity: 0.9; }
.git-log-entry { padding: 6px 0; border-bottom: 1px solid var(--gray-100); }
.git-log-msg { font-size: 13px; }
.git-log-meta { font-size: 11px; color: var(--gray-500); margin-top: 2px; }
.git-info { font-size: 13px; color: var(--gray-500); padding: 8px 0; }
.git-loading { font-size: 13px; color: var(--gray-400); }
/* Snippets Panel */
.snippets-toolbar { display: flex; gap: 4px; margin-bottom: 8px; }
.snippets-search { flex: 1; padding: 6px 10px; border: 1px solid var(--gray-300); border-radius: 6px; font-size: 13px; }
.snippets-add-btn { width: 32px; border: 1px solid var(--gray-300); border-radius: 6px; background: white; font-size: 18px; cursor: pointer; }
.snippet-item { padding: 8px; border: 1px solid var(--gray-200); border-radius: 6px; margin-bottom: 6px; }
.snippet-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
.snippet-name { font-size: 13px; font-weight: 500; }
.snippet-lang { font-size: 11px; background: var(--gray-100); padding: 2px 6px; border-radius: 4px; color: var(--gray-500); }
.snippet-preview { font-size: 12px; background: var(--gray-50); padding: 6px; border-radius: 4px; margin: 4px 0; overflow: hidden; max-height: 60px; }
.snippet-preview code { font-family: 'JetBrains Mono', monospace; }
.snippet-actions { display: flex; gap: 4px; }
.snippet-insert { font-size: 12px; padding: 4px 8px; border: 1px solid var(--gray-300); border-radius: 4px; background: white; cursor: pointer; }
.snippet-delete { font-size: 14px; padding: 4px 8px; border: 1px solid var(--gray-300); border-radius: 4px; background: white; cursor: pointer; color: #ef4444; }
/* Dark theme for sidebar panels */
body[class*="dark"] .explorer-path,
body[class*="dark"] .explorer-browse-btn,
body[class*="dark"] .snippets-search,
body[class*="dark"] .snippets-add-btn,
body[class*="dark"] .snippet-insert,
body[class*="dark"] .snippet-delete {
background: #2d2d2d;
border-color: #444;
color: #ccc;
}
body[class*="dark"] .git-commit-input {
background: #2d2d2d;
border-color: #444;
color: #ccc;
}
body[class*="dark"] .snippet-item {
border-color: #444;
}
body[class*="dark"] .snippet-preview {
background: #2d2d2d;
}
body[class*="dark"] .tree-item:hover,
body[class*="dark"] .git-file:hover {
background: #333;
}
+37
View File
@@ -290,6 +290,43 @@ body {
line-height: 1.6; line-height: 1.6;
} }
/* Bottom Panel (REPL Output) */
.bottom-panel { display: flex; flex-direction: column; border-top: 1px solid var(--gray-200, #e5e7eb); flex-shrink: 0; }
.bottom-panel.collapsed { height: 0; overflow: hidden; border: none; }
.bottom-panel:not(.collapsed) { height: 200px; }
.bottom-panel-header { display: flex; justify-content: space-between; align-items: center; padding: 6px 12px; background: var(--gray-50, #f9fafb); border-bottom: 1px solid var(--gray-200); }
.bottom-panel-title { font-size: 13px; font-weight: 600; color: var(--gray-600); }
.bottom-panel-actions { display: flex; gap: 4px; }
.bottom-panel-btn { font-size: 12px; padding: 4px 8px; border: 1px solid var(--gray-300); border-radius: 4px; background: white; cursor: pointer; }
.bottom-panel-content { flex: 1; overflow-y: auto; padding: 12px; font-family: 'JetBrains Mono', monospace; font-size: 13px; background: #1e1e1e; color: #d4d4d4; }
.repl-entry { margin-bottom: 8px; }
.repl-command { color: #569cd6; margin-bottom: 2px; }
.repl-stdout { color: #d4d4d4; white-space: pre-wrap; }
.repl-stderr { color: #f44747; white-space: pre-wrap; }
.repl-error { color: #f44747; }
/* Code Run Button */
.code-run-btn {
position: absolute;
top: 6px;
right: 6px;
padding: 4px 10px;
font-size: 12px;
background: var(--primary-dark, #5661b3);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s;
}
pre:hover .code-run-btn { opacity: 1; }
/* Dark theme bottom panel */
body[class*="dark"] .bottom-panel-header { background: #1e1e1e; border-color: #333; }
body[class*="dark"] .bottom-panel-btn { background: #2d2d2d; border-color: #444; color: #ccc; }
body[class*="dark"] .bottom-panel-title { color: #ccc; }
/* Status Bar */ /* Status Bar */
.status-bar { .status-bar {
display: flex; display: flex;