RDP-Verbindungen als Tabs (mstsc/FreeRDP eingebettet); App-Kürzel im Terminal auf Strg+Shift, damit Strg+W/K/T bei nano & Co. ankommen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 15:20:04 +02:00
co-authored by Claude Opus 5.5
parent b1c6141264
commit aa31e753c2
11 changed files with 830 additions and 60 deletions
+65 -1
View File
@@ -8,9 +8,16 @@ const { Store } = require('./store');
const { SshManager } = require('./ssh');
const rdp = require('./rdp');
const { Updater } = require('./updater');
const { createEmbed, embedSupported } = require('./rdp-embed');
let win;
const store = new Store();
// Unter Linux muss MrTerm für eingebettete RDP-Tabs als X11-Client laufen (unter Wayland via XWayland).
// Die Wahl wird unverschlüsselt in launch.json gespeichert, weil sie vor dem Start des Fensters gebraucht wird.
const launchFile = path.join(app.getPath('userData'), 'launch.json');
function readLaunch() { try { return JSON.parse(fs.readFileSync(launchFile, 'utf8')); } catch { return {}; } }
if (process.platform === 'linux' && readLaunch().x11 !== false) app.commandLine.appendSwitch('ozone-platform', 'x11');
const pendingSecrets = new Map();
function send(channel, ...args) {
@@ -85,7 +92,10 @@ ipcMain.on('win:close', () => win.close());
handle('vault:get', () => ({ ...store.get(), encrypted: store.encrypted, platform: process.platform }));
handle('vault:upsert', (col, item) => store.upsert(col, item));
handle('vault:remove', (col, id) => store.remove(col, id));
handle('vault:settings', (s) => store.setSettings(s));
handle('vault:settings', (s) => {
store.setSettings(s);
if ('rdpEmbed' in s) fs.writeFileSync(launchFile, JSON.stringify({ ...readLaunch(), x11: s.rdpEmbed !== false }));
});
handle('vault:forgetHost', (id) => { delete store.get().knownHosts[id]; store.save(); });
handle('vault:export', async () => {
const r = await dialog.showSaveDialog(win, { defaultPath: 'mrterm-backup.json', filters: [{ name: 'JSON', extensions: ['json'] }] });
@@ -244,6 +254,60 @@ handle('rdp:launch', (hostId) => {
return r.client;
});
// Eingebettete RDP-Sitzungen (Anzeige im Tab)
const rdpSessions = new Map();
handle('rdp:embedSupported', () => {
const s = embedSupported();
if (s.ok && process.platform === 'linux' && !rdp.linuxClient(null, true)) return { ok: false, reason: 'xfreerdp3 fehlt (sudo pacman -S freerdp)' };
return s;
});
handle('rdp:open', async (id, hostRef, bounds) => {
const host = hostWithOverrides(hostRef);
if (host.id) store.addHistory({ hostId: host.id, at: Date.now() });
const emb = createEmbed(win.getNativeWindowHandle());
const sess = { emb, cancelled: false };
rdpSessions.set(id, sess);
const size = { width: bounds.width, height: bounds.height };
let proc;
if (process.platform === 'win32') {
const file = rdp.writeRdpFile(host, size);
proc = await new Promise((resolve) => rdp.withWinCredentials(host, () => resolve(emb.start({ rdpFile: file }))));
} else {
proc = emb.start({ client: rdp.linuxClient(null, true), args: rdp.freerdpArgs(host, size), password: host.password });
}
proc.on('error', (e) => send('rdp:event', id, 'exit', { code: -1, message: e.message }));
proc.on('exit', (code) => {
const err = (emb.err || '').split('\n').filter((l) => /ERROR|error/.test(l)).slice(-1)[0] || '';
if (rdpSessions.get(id) === sess) send('rdp:event', id, 'exit', { code, message: err });
rdpSessions.delete(id);
});
const ok = await emb.attach(bounds, () => sess.cancelled);
if (ok) {
if (sess.hidden) emb.hide();
send('rdp:event', id, 'attached');
}
return ok;
});
ipcMain.on('rdp:bounds', (_e, id, b) => { try { rdpSessions.get(id)?.emb.setBounds(b); } catch {} });
ipcMain.on('rdp:show', (_e, id) => { const s = rdpSessions.get(id); if (s) { s.hidden = false; try { s.emb.show(); s.emb.focus(); } catch {} } });
ipcMain.on('rdp:hide', (_e, id) => { const s = rdpSessions.get(id); if (s) { s.hidden = true; try { s.emb.hide(); } catch {} } });
ipcMain.on('rdp:focus', (_e, id) => { try { rdpSessions.get(id)?.emb.focus(); } catch {} });
ipcMain.on('rdp:close', (_e, id) => {
const s = rdpSessions.get(id);
if (!s) return;
s.cancelled = true;
rdpSessions.delete(id);
s.emb.kill();
});
handle('rdp:detach', (id) => {
const s = rdpSessions.get(id);
if (!s) return false;
if (process.platform !== 'win32') return false;
s.emb.detach();
rdpSessions.delete(id); // läuft als eigenständiges Fenster weiter
return true;
});
// ---------- Import aus anderen Programmen (z. B. Devolutions RDM) ----------
handle('import:pickFile', async (title, extensions) => {
const r = await dialog.showOpenDialog(win, { title, filters: [{ name: title, extensions }, { name: 'Alle Dateien', extensions: ['*'] }], properties: ['openFile'] });