From d3afd7ad8644fda4af22cc03ab4d0cea2e648a28 Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Wed, 4 Mar 2026 16:17:38 +0530 Subject: [PATCH] feat: wire up sidebar panels and REPL with IPC handlers, preload channels, and CSS --- src/index.html | 12 ++++ src/main.js | 160 +++++++++++++++++++++++++++++++++++++++++ src/preload.js | 22 +++++- src/renderer.js | 49 ++++++++++++- src/styles-sidebar.css | 105 +++++++++++++++++++++++++++ src/styles.css | 37 ++++++++++ 6 files changed, 381 insertions(+), 4 deletions(-) diff --git a/src/index.html b/src/index.html index 7912757..7f8ca89 100644 --- a/src/index.html +++ b/src/index.html @@ -1536,6 +1536,18 @@ + + +
Ready diff --git a/src/main.js b/src/main.js index 42cf052..b6e9efb 100644 --- a/src/main.js +++ b/src/main.js @@ -3925,4 +3925,164 @@ ipcMain.on('insert-generated-content', (event, content) => { if (mainWindow && !mainWindow.isDestroyed()) { 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) + }); + }); + }); }); \ No newline at end of file diff --git a/src/preload.js b/src/preload.js index c076d34..334006e 100644 --- a/src/preload.js +++ b/src/preload.js @@ -97,7 +97,27 @@ const ALLOWED_SEND_CHANNELS = [ 'save-pasted-image', // 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 = [ diff --git a/src/renderer.js b/src/renderer.js index e638e39..e8e1167 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -12,6 +12,10 @@ const { createEditor } = require('./editor/codemirror-setup'); const { undo, redo } = require('@codemirror/commands'); const { SidebarManager } = require('./sidebar/sidebar-manager'); 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 { PrintPreview } = require('./print-preview'); @@ -488,6 +492,26 @@ class TabManager { 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) { console.error('Error rendering preview:', error); preview.innerHTML = '

Error rendering preview. Please check your markdown syntax.

'; @@ -1118,23 +1142,42 @@ class TabManager { // Initialize tab manager let tabManager; +let replPanel; document.addEventListener('DOMContentLoaded', () => { tabManager = new TabManager(); + replPanel = new ReplPanel(); // Initialize sidebar const sidebarManager = new SidebarManager(); + let explorerCurrentDir = null; + sidebarManager.registerPanel('explorer', { title: 'Explorer', - render: (container) => { container.innerHTML = '

File explorer coming soon...

'; } + render: (container) => renderExplorerPanel(container, { + listDirectory: (dir) => ipcRenderer.invoke('list-directory', dir), + onFileOpen: (filePath) => ipcRenderer.send('open-file-path', filePath), + currentDir: explorerCurrentDir, + }) }); sidebarManager.registerPanel('git', { title: 'Git', - render: (container) => { container.innerHTML = '

Git panel coming soon...

'; } + 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', { title: 'Snippets', - render: (container) => { container.innerHTML = '

Snippets coming soon...

'; } + 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', { title: 'Templates', diff --git a/src/styles-sidebar.css b/src/styles-sidebar.css index e735570..c32aad6 100644 --- a/src/styles-sidebar.css +++ b/src/styles-sidebar.css @@ -161,3 +161,108 @@ body[class*="dark"] .panel-list-item-title { body[class*="dark"] .panel-list-item-desc { 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; +} diff --git a/src/styles.css b/src/styles.css index a901a8a..639f488 100644 --- a/src/styles.css +++ b/src/styles.css @@ -290,6 +290,43 @@ body { 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 { display: flex;