Skip to content

Commit 2c0cb77

Browse files
committed
Add embedded terminal panel
- Add hideable terminal panel at bottom of window - Toggle with Ctrl+` keyboard shortcut or button in status bar - Uses xterm.js for terminal emulation and node-pty for shell - Auto-cd to current file's directory when opening files - Terminal follows file location changes - Styled to match LOGAN dark theme - Add terminal shortcuts to help modal - Bump version to v0.1.4
1 parent 7dc6f8f commit 2c0cb77

File tree

8 files changed

+467
-10
lines changed

8 files changed

+467
-10
lines changed

package-lock.json

Lines changed: 40 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"name": "logan",
3-
"version": "0.1.3",
3+
"version": "0.1.4",
44
"description": "Fast log file viewer and analyzer for large files with pattern detection, search, and bookmarks",
55
"main": "dist/main/index.js",
66
"scripts": {
77
"build": "tsc && npm run copy-assets",
8-
"copy-assets": "cp src/renderer/index.html src/renderer/styles.css dist/renderer/ && cp -r src/renderer/assets dist/renderer/",
8+
"copy-assets": "cp src/renderer/index.html src/renderer/styles.css dist/renderer/ && cp -r src/renderer/assets dist/renderer/ && mkdir -p dist/renderer/lib && cp node_modules/xterm/css/xterm.css dist/renderer/lib/ && cp node_modules/xterm/lib/xterm.js dist/renderer/lib/ && cp node_modules/xterm-addon-fit/lib/xterm-addon-fit.js dist/renderer/lib/",
99
"start": "npm run build && electron .",
1010
"dev": "npm run build && electron .",
1111
"watch": "tsc -w",
@@ -30,7 +30,11 @@
3030
"electron-builder": "^24.9.1",
3131
"typescript": "^5.3.0"
3232
},
33-
"dependencies": {},
33+
"dependencies": {
34+
"node-pty": "^1.1.0",
35+
"xterm": "^5.3.0",
36+
"xterm-addon-fit": "^0.8.0"
37+
},
3438
"build": {
3539
"appId": "com.solidkey.logan",
3640
"productName": "LOGAN",

src/main/index.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as path from 'path';
33
import * as fs from 'fs';
44
import * as os from 'os';
55
import { spawn } from 'child_process';
6+
import * as pty from 'node-pty';
67
import { FileHandler } from './fileHandler';
78
import { IPC, SearchOptions, Bookmark, Highlight } from '../shared/types';
89
import { analyzerRegistry, AnalyzerOptions } from './analyzers';
@@ -1413,3 +1414,86 @@ ipcMain.handle('split-file', async (_, options: SplitOptions) => {
14131414
return { success: false, error: String(error) };
14141415
}
14151416
});
1417+
1418+
// === Terminal ===
1419+
1420+
let ptyProcess: pty.IPty | null = null;
1421+
1422+
function getDefaultShell(): string {
1423+
if (process.platform === 'win32') {
1424+
return process.env.COMSPEC || 'cmd.exe';
1425+
}
1426+
return process.env.SHELL || '/bin/bash';
1427+
}
1428+
1429+
ipcMain.handle('terminal-create', async (_, options?: { cwd?: string; cols?: number; rows?: number }) => {
1430+
try {
1431+
// Kill existing terminal if any
1432+
if (ptyProcess) {
1433+
ptyProcess.kill();
1434+
ptyProcess = null;
1435+
}
1436+
1437+
const shell = getDefaultShell();
1438+
const cwd = options?.cwd || os.homedir();
1439+
const cols = options?.cols || 80;
1440+
const rows = options?.rows || 24;
1441+
1442+
ptyProcess = pty.spawn(shell, [], {
1443+
name: 'xterm-256color',
1444+
cols,
1445+
rows,
1446+
cwd,
1447+
env: process.env as { [key: string]: string },
1448+
});
1449+
1450+
// Forward terminal output to renderer
1451+
ptyProcess.onData((data: string) => {
1452+
mainWindow?.webContents.send('terminal-data', data);
1453+
});
1454+
1455+
ptyProcess.onExit(({ exitCode }) => {
1456+
mainWindow?.webContents.send('terminal-exit', exitCode);
1457+
ptyProcess = null;
1458+
});
1459+
1460+
return { success: true, pid: ptyProcess.pid };
1461+
} catch (error) {
1462+
return { success: false, error: String(error) };
1463+
}
1464+
});
1465+
1466+
ipcMain.handle('terminal-write', async (_, data: string) => {
1467+
if (ptyProcess) {
1468+
ptyProcess.write(data);
1469+
return { success: true };
1470+
}
1471+
return { success: false, error: 'No terminal process' };
1472+
});
1473+
1474+
ipcMain.handle('terminal-resize', async (_, cols: number, rows: number) => {
1475+
if (ptyProcess) {
1476+
ptyProcess.resize(cols, rows);
1477+
return { success: true };
1478+
}
1479+
return { success: false, error: 'No terminal process' };
1480+
});
1481+
1482+
ipcMain.handle('terminal-kill', async () => {
1483+
if (ptyProcess) {
1484+
ptyProcess.kill();
1485+
ptyProcess = null;
1486+
return { success: true };
1487+
}
1488+
return { success: false, error: 'No terminal process' };
1489+
});
1490+
1491+
ipcMain.handle('terminal-cd', async (_, directory: string) => {
1492+
if (ptyProcess && fs.existsSync(directory)) {
1493+
// Send cd command to terminal
1494+
const cdCmd = process.platform === 'win32' ? `cd /d "${directory}"\r` : `cd "${directory}"\r`;
1495+
ptyProcess.write(cdCmd);
1496+
return { success: true };
1497+
}
1498+
return { success: false, error: 'No terminal process or invalid directory' };
1499+
});

src/preload/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,34 @@ const api = {
165165
// Open external URL in default browser
166166
openExternalUrl: (url: string): Promise<void> =>
167167
ipcRenderer.invoke('open-external-url', url),
168+
169+
// Terminal
170+
terminalCreate: (options?: { cwd?: string; cols?: number; rows?: number }): Promise<{ success: boolean; pid?: number; error?: string }> =>
171+
ipcRenderer.invoke('terminal-create', options),
172+
173+
terminalWrite: (data: string): Promise<{ success: boolean; error?: string }> =>
174+
ipcRenderer.invoke('terminal-write', data),
175+
176+
terminalResize: (cols: number, rows: number): Promise<{ success: boolean; error?: string }> =>
177+
ipcRenderer.invoke('terminal-resize', cols, rows),
178+
179+
terminalKill: (): Promise<{ success: boolean; error?: string }> =>
180+
ipcRenderer.invoke('terminal-kill'),
181+
182+
terminalCd: (directory: string): Promise<{ success: boolean; error?: string }> =>
183+
ipcRenderer.invoke('terminal-cd', directory),
184+
185+
onTerminalData: (callback: (data: string) => void): (() => void) => {
186+
const handler = (_: any, data: string) => callback(data);
187+
ipcRenderer.on('terminal-data', handler);
188+
return () => ipcRenderer.removeListener('terminal-data', handler);
189+
},
190+
191+
onTerminalExit: (callback: (exitCode: number) => void): (() => void) => {
192+
const handler = (_: any, exitCode: number) => callback(exitCode);
193+
ipcRenderer.on('terminal-exit', handler);
194+
return () => ipcRenderer.removeListener('terminal-exit', handler);
195+
},
168196
};
169197

170198
contextBridge.exposeInMainWorld('api', api);

src/renderer/index.html

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; media-src 'self';">
77
<title>LOGAN - Log Analyzer</title>
88
<link rel="stylesheet" href="styles.css">
9+
<link rel="stylesheet" href="lib/xterm.css">
10+
<script src="lib/xterm.js"></script>
11+
<script src="lib/xterm-addon-fit.js"></script>
912
</head>
1013
<body>
1114
<div id="app">
@@ -183,6 +186,17 @@ <h2>LOGAN</h2>
183186
</div>
184187
</div>
185188
</main>
189+
190+
<!-- Terminal Panel -->
191+
<div id="terminal-panel" class="terminal-panel hidden">
192+
<div class="terminal-header">
193+
<span class="terminal-title">Terminal</span>
194+
<div class="terminal-controls">
195+
<button id="btn-terminal-close" class="terminal-btn" title="Close (Ctrl+`)">×</button>
196+
</div>
197+
</div>
198+
<div id="terminal-container" class="terminal-container"></div>
199+
</div>
186200
</div>
187201

188202
<!-- Status Bar -->
@@ -200,6 +214,8 @@ <h2>LOGAN</h2>
200214
</div>
201215
</div>
202216
<div class="status-right">
217+
<button id="btn-terminal-toggle" class="status-btn" title="Toggle Terminal (Ctrl+`)">⌨ Terminal</button>
218+
<span class="status-separator">|</span>
203219
<span id="status-cursor">Ln 1, Col 1</span>
204220
<span class="status-separator">|</span>
205221
<span id="status-size">0 B</span>
@@ -381,6 +397,13 @@ <h4>Bookmarks & Highlights</h4>
381397
<div class="shortcut-item"><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd><span>Save selected lines to file</span></div>
382398
</div>
383399
</div>
400+
<div class="help-section">
401+
<h4>Terminal</h4>
402+
<div class="shortcut-list">
403+
<div class="shortcut-item"><kbd>Ctrl</kbd>+<kbd>`</kbd><span>Toggle terminal panel</span></div>
404+
<div class="shortcut-item"><kbd>Esc</kbd><span>Close terminal (when focused)</span></div>
405+
</div>
406+
</div>
384407
<div class="help-section">
385408
<h4>Tips</h4>
386409
<ul class="tips-list">
@@ -403,7 +426,7 @@ <h4>Search Performance</h4>
403426
</div>
404427
</div>
405428
<div class="modal-footer">
406-
<span class="version-info">LOGAN v0.1.3</span>
429+
<span class="version-info">LOGAN v0.1.4</span>
407430
<button id="btn-close-help" class="primary-btn">Got it!</button>
408431
</div>
409432
</div>

0 commit comments

Comments
 (0)