Skip to content

Commit 52f7fa0

Browse files
committed
feat: add multi ssh app
1 parent c434ba3 commit 52f7fa0

File tree

14 files changed

+3542
-0
lines changed

14 files changed

+3542
-0
lines changed

.editorconfig

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# EditorConfig is awesome: https://editorconfig.org
2+
3+
root = true
4+
5+
[*]
6+
end_of_line = lf
7+
insert_final_newline = true
8+
trim_trailing_whitespace = true
9+
indent_style = space
10+
11+
[{*.ts,*.tsx,*.js}]
12+
indent_size = 4

.github/workflows/format-check.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Format Check
2+
3+
on:
4+
push:
5+
6+
jobs:
7+
format:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@v5
11+
- name: Set up Node.js
12+
uses: actions/setup-node@v4
13+
with:
14+
node-version: "20"
15+
- name: Install pnpm
16+
run: npm install -g pnpm
17+
- name: Install dependencies
18+
run: pnpm install
19+
- name: Check formatting and linting
20+
run: pnpm check

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
dist/
3+
config.yaml

AGENTS.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# AGENTS.md
2+
3+
A comprehensive guide for AI agents working on the multi-ssh project, providing context and instructions for effective development assistance.
4+
5+
## Project Overview
6+
7+
multi-ssh is an electron app
8+
9+
## General Guidelines
10+
11+
- **KISS Principle**: Always strive for simple and easy solutions
12+
- **Documentation**: Keep documentation concise; don't create additional docs unless requested
13+
- **Task Planning**: Create TODO items before starting work, prioritizing document reading first
14+
15+
16+
## Commands
17+
18+
```bash
19+
pnpm start # Start the whole application
20+
```

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Multi SSH
2+
3+
A multi-SSH terminal application built with Electron, providing a unified interface to manage multiple SSH connections simultaneously using xterm.js for terminal emulation and node-pty for pseudo-terminal support.
4+
5+
## Features
6+
7+
- **Multi-Terminal Interface**: Connect to multiple SSH hosts in separate terminal panes
8+
- **Real Terminal Emulation**: Full shell integration with proper PTY support
9+
- **Modern UI**: Dark-themed interface with responsive design
10+
- **Send to All**: Option to broadcast commands to all connected terminals
11+
12+
## Configuration
13+
14+
Create a `config.yaml` file based on `config_sample.yaml` with your SSH hosts:
15+
16+
```yaml
17+
hosts:
18+
- user@host1
19+
- user@host2
20+
- user@host3
21+
```
22+
23+
## Development Notes
24+
25+
### Commands
26+
27+
```bash
28+
pnpm start # Development Mode
29+
pnpm build # Building the Application
30+
pnpm dist # Creating Distribution Package
31+
```
32+
33+
### Project Structure
34+
35+
```
36+
electron-noob/
37+
├── main.js # Main Electron process with PTY integration
38+
├── renderer.js # Renderer process with xterm.js terminals
39+
├── index.html # Main UI layout
40+
```
41+
42+
## License
43+
44+
MIT
45+
46+
[SSH icons created by Freepik - Flaticon](https://www.flaticon.com/free-icons/ssh)

assets/ssh.png

3.87 KB
Loading

biome.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"$schema": "https://biomejs.dev/schemas/2.3.0/schema.json",
3+
"root": true,
4+
"vcs": {
5+
"enabled": true,
6+
"clientKind": "git",
7+
"useIgnoreFile": true
8+
},
9+
"files": {
10+
"includes": ["*.js", "biome.json"]
11+
},
12+
"formatter": {
13+
"enabled": true,
14+
"formatWithErrors": false,
15+
"attributePosition": "auto",
16+
"useEditorconfig": true
17+
},
18+
"linter": {
19+
"enabled": true,
20+
"rules": {
21+
"recommended": true
22+
}
23+
},
24+
"javascript": {
25+
"formatter": {
26+
"quoteStyle": "double"
27+
}
28+
},
29+
"assist": {
30+
"enabled": true,
31+
"actions": {
32+
"source": {
33+
"organizeImports": "on"
34+
}
35+
}
36+
}
37+
}

config_sample.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
hosts:
2+
- host1
3+
- host2
4+
terminalHeight: 300
5+
terminalMinWidth: 600

index.html

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<meta http-equiv="Content-Security-Policy"
8+
content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';">
9+
<title>Multi SSH</title>
10+
<link rel="stylesheet" href="node_modules/@xterm/xterm/css/xterm.css" />
11+
<style>
12+
body {
13+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
14+
margin: 0;
15+
padding: 0;
16+
background: #1a1a1a;
17+
color: white;
18+
overflow: hidden;
19+
height: 100vh;
20+
}
21+
22+
.app-layout {
23+
height: 100vh;
24+
display: flex;
25+
flex-direction: column;
26+
}
27+
28+
.control-panel {
29+
background: #333;
30+
padding: 5px 10px;
31+
border-bottom: 1px solid #444;
32+
font-size: 0.9em;
33+
flex-shrink: 0;
34+
}
35+
36+
.terminals-container {
37+
flex: 1;
38+
display: flex;
39+
margin: 5px;
40+
gap: 5px;
41+
flex-wrap: wrap;
42+
align-items: flex-start;
43+
align-content: flex-start;
44+
overflow: auto;
45+
padding-right: 5px; /* Account for scrollbar */
46+
}
47+
48+
.terminal-wrapper {
49+
border: 2px solid #444;
50+
border-radius: 8px;
51+
overflow: hidden;
52+
background: #1a1a1a;
53+
display: flex;
54+
flex-direction: column;
55+
height: 350px; /* Set default, will be overridden by JS */
56+
}
57+
58+
.terminal-title-bar {
59+
background: #333;
60+
color: white;
61+
padding: 5px 10px;
62+
font-size: 0.9em;
63+
font-weight: bold;
64+
border-bottom: 1px solid #444;
65+
}
66+
67+
.terminal-container {
68+
flex: 1;
69+
background: #0c0c0c;
70+
position: relative;
71+
overflow: hidden;
72+
}
73+
74+
.terminal {
75+
width: 100%;
76+
height: 100%;
77+
}
78+
</style>
79+
</head>
80+
81+
<body>
82+
<div class="app-layout">
83+
<div class="control-panel">
84+
<label>
85+
<input type="checkbox" id="send-to-all"> Send to All
86+
</label>
87+
</div>
88+
<div class="terminals-container" id="terminals-container">
89+
<!-- Terminals will be dynamically added here -->
90+
</div>
91+
</div>
92+
93+
<script src="renderer.js"></script>
94+
</body>
95+
96+
</html>

main.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
const { app, BrowserWindow, ipcMain } = require("electron");
2+
const path = require("node:path");
3+
const pty = require("node-pty");
4+
const fs = require("node:fs");
5+
const yaml = require("js-yaml");
6+
7+
// Keep a global reference of the window object
8+
let mainWindow;
9+
let ptyProcesses = []; // Array of terminal processes
10+
let config; // Will be loaded later
11+
12+
// This method will be called when Electron has finished initialization
13+
app.whenReady().then(() => {
14+
createWindow();
15+
});
16+
17+
ipcMain.on("renderer-ready", () => {
18+
initializeAllTerminals();
19+
});
20+
21+
// IPC handlers for terminal communication
22+
ipcMain.on("terminal-input", (_event, { index, data }) => {
23+
if (ptyProcesses[index]) {
24+
ptyProcesses[index].write(data);
25+
} else {
26+
// Fallback if process died
27+
mainWindow?.webContents?.send(
28+
`terminal-data-${index}`,
29+
"\r\n\x1b[91mSSH session not available\x1b[0m\r\n$ ",
30+
);
31+
}
32+
});
33+
34+
ipcMain.on("terminal-resize", (_event, { index, cols, rows }) => {
35+
if (ptyProcesses[index]) {
36+
ptyProcesses[index].resize(cols, rows);
37+
}
38+
});
39+
40+
ipcMain.on("toggle-dev-tools", () => {
41+
if (mainWindow) {
42+
if (mainWindow.webContents.isDevToolsOpened()) {
43+
mainWindow.webContents.closeDevTools();
44+
} else {
45+
mainWindow.webContents.openDevTools();
46+
}
47+
}
48+
});
49+
50+
function createWindow() {
51+
// Create the browser window
52+
mainWindow = new BrowserWindow({
53+
width: 1400,
54+
height: 1000,
55+
webPreferences: {
56+
nodeIntegration: true,
57+
contextIsolation: false,
58+
enableRemoteModule: true,
59+
},
60+
icon: path.join(__dirname, "assets/ssh.png"), // Optional: add an icon
61+
titleBarStyle: "default",
62+
show: false, // Don't show until ready
63+
});
64+
65+
// Load the index.html file
66+
mainWindow.loadFile("index.html");
67+
mainWindow.removeMenu();
68+
69+
// Show window when ready to prevent visual flash
70+
mainWindow.once("ready-to-show", () => {
71+
mainWindow.show();
72+
// Load and send config to renderer
73+
try {
74+
config = yaml.load(
75+
fs.readFileSync(path.join(__dirname, "config.yaml"), "utf8"),
76+
);
77+
mainWindow.webContents.send("config", config);
78+
} catch (error) {
79+
console.error("Failed to load config:", error);
80+
mainWindow.webContents.send("config-error", error.message);
81+
}
82+
// Wait for renderer ready before initializing PTY
83+
});
84+
85+
// Emitted when the window is closed
86+
mainWindow.on("closed", () => {
87+
// Clean up PTY processes
88+
ptyProcesses.forEach((process, _index) => {
89+
if (process && !process.killed) {
90+
process.kill();
91+
}
92+
});
93+
ptyProcesses = [];
94+
// Dereference the window object
95+
mainWindow = null;
96+
});
97+
}
98+
99+
function initializeAllTerminals() {
100+
const shell = "ssh";
101+
console.log("Using shell:", shell);
102+
103+
for (let i = 0; i < config.hosts.length; i++) {
104+
try {
105+
initializeTerminal(i, shell, config.hosts[i]);
106+
} catch (error) {
107+
console.error(`Failed to spawn SSH session ${i} process:`, error);
108+
mainWindow?.webContents?.send(
109+
`terminal-data-${i}`,
110+
`\r\n\x1b[91mFailed to start SSH session ${i + 1}: ${error.message}\x1b[0m\r\n`,
111+
);
112+
}
113+
}
114+
}
115+
116+
// Initialize PTY processes
117+
function initializeTerminal(index, shell, host) {
118+
// Use node-pty for proper pseudo-terminal functionality
119+
const ptyProcess = pty.spawn(shell, [host], {
120+
name: "xterm-256color",
121+
cols: 80,
122+
rows: 24,
123+
cwd: process.env.HOME || process.env.USERPROFILE || "/",
124+
env: process.env,
125+
});
126+
127+
// Send data from PTY to renderer
128+
ptyProcess.onData((data) => {
129+
mainWindow?.webContents?.send(`terminal-data-${index}`, data);
130+
});
131+
132+
// Handle process exit
133+
ptyProcess.onExit(({ exitCode, signal }) => {
134+
mainWindow?.webContents?.send(`terminal-exit-${index}`, {
135+
code: exitCode,
136+
signal,
137+
});
138+
});
139+
140+
ptyProcesses[index] = ptyProcess;
141+
}

0 commit comments

Comments
 (0)