diff --git a/package-lock.json b/package-lock.json index 6b6e28a..4f918da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mrterm", - "version": "0.13.3", + "version": "0.14.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mrterm", - "version": "0.13.3", + "version": "0.14.0", "license": "MIT", "dependencies": { "@xterm/addon-fit": "^0.11.0", diff --git a/package.json b/package.json index f1943da..e192d69 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mrterm", "productName": "MrTerm", - "version": "0.13.3", + "version": "0.14.0", "description": "Moderner SSH-, SFTP- und RDP-Client", "main": "src/main/main.js", "author": "MrBlake", diff --git a/src/i18n.js b/src/i18n.js index 02142f5..300346b 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -351,6 +351,22 @@ 'Touch your security key to unlock MrTerm.': 'Berühre deinen Sicherheitsschlüssel, um MrTerm zu entsperren.', 'Security key prompt was cancelled or timed out.': 'Die Abfrage des Sicherheitsschlüssels wurde abgebrochen oder ist abgelaufen.', 'This security key does not support the hmac-secret/PRF extension.': 'Dieser Sicherheitsschlüssel unterstützt die hmac-secret/PRF-Erweiterung nicht.', + 'Templates': 'Vorlagen', + 'Run': 'Ausführen', + 'Authorize SSH key for a user': 'SSH-Key für Benutzer freischalten', + 'Adds a public key from the keychain to ~/.ssh/authorized_keys of the user.': 'Trägt einen öffentlichen Schlüssel aus dem Schlüsselbund in ~/.ssh/authorized_keys des Benutzers ein.', + 'Key': 'Schlüssel', + 'Public key is missing.': 'Öffentlicher Schlüssel fehlt.', + 'User is missing.': 'Benutzer fehlt.', + 'Install QEMU guest agent': 'QEMU Guest Agent installieren', + 'Installs and starts qemu-guest-agent (apt, dnf, yum, pacman, zypper or apk).': 'Installiert und startet qemu-guest-agent (apt, dnf, yum, pacman, zypper oder apk).', + 'Use APT cache server': 'APT-Cache-Server verwenden', + 'Routes APT downloads through a cache proxy such as apt-cacher-ng (/etc/apt/apt.conf.d/00proxy).': 'Leitet APT-Downloads über einen Cache-Proxy wie apt-cacher-ng (/etc/apt/apt.conf.d/00proxy).', + 'Proxy URL': 'Proxy-URL', + 'Invalid URL.': 'Ungültige URL.', + 'Remove APT cache server': 'APT-Cache-Server entfernen', + 'Removes the APT proxy setting again.': 'Entfernt die APT-Proxy-Einstellung wieder.', + 'Updates all packages with the system package manager.': 'Aktualisiert alle Pakete mit dem Paketmanager des Systems.', 'Connecting to security key …': 'Verbinde mit dem Sicherheitsschlüssel …', 'Enter the PIN of your security key.': 'Gib die PIN deines Sicherheitsschlüssels ein.', 'Wrong PIN.': 'Falsche PIN.', diff --git a/src/renderer/app.js b/src/renderer/app.js index 2d57532..f9ae55d 100644 --- a/src/renderer/app.js +++ b/src/renderer/app.js @@ -658,7 +658,7 @@ function viewSnippets(page) { page.append(toolbar(T('Search snippets …'), [{ label: T('New snippet'), icon: 'plus', cls: 'primary', run: () => editSnippet() }])); const c = h('
'); page.append(c); const list = S.vault.snippets.filter((s) => matches(s.label, s.command)); - if (!list.length) return c.append(emptyState('code', T('No snippets'), T('Save frequently used commands and send them to a terminal with one click.'), T('New snippet'), () => editSnippet())); + if (!list.length) { c.append(emptyState('code', T('No snippets'), T('Save frequently used commands and send them to a terminal with one click.'), T('New snippet'), () => editSnippet())); c.append(templateList()); return; } const l = h('
'); for (const s of list) { const card = h(`
${ICONS.code}
${esc(s.label)}
${esc(s.command.split('\n')[0])}${s.command.includes('\n') ? ' …' : ''}
`); @@ -671,7 +671,7 @@ function viewSnippets(page) { }; l.append(card); } - c.append(l); + c.append(l, templateList()); } function editSnippet(s = {}) { const form = openDrawer(s.id ? T('Edit snippet') : T('New snippet'), [ @@ -685,6 +685,86 @@ function editSnippet(s = {}) { reload(); }); } +// Eingebaute Vorlagen (nicht im Vault). Parameter werden vor dem Ausführen abgefragt; build() liefert eine Zeile. +// $S = sudo, außer man ist bereits root. +const shq = (v) => `'${String(v).replace(/'/g, `'\\''`)}'`; +const SUDO = 'S=$([ "$(id -u)" = 0 ] || echo sudo);'; +const SNIPPET_TEMPLATES = [ + { + label: () => T('Authorize SSH key for a user'), + desc: () => T('Adds a public key from the keychain to ~/.ssh/authorized_keys of the user.'), + params: () => [ + S.vault.keys.some((k) => k.publicKey) + ? field(T('Key'), 'key', '', { type: 'select', options: S.vault.keys.filter((k) => k.publicKey).map((k) => [k.id, k.label]) }) + : field('Public Key', 'pub', '', { type: 'textarea', placeholder: 'ssh-ed25519 AAAA…' }), + field(T('User'), 'user', 'root'), + ], + build: (v) => { + const pub = (v.key ? S.vault.keys.find((k) => k.id === v.key)?.publicKey : v.pub || '').trim(); + if (!pub) throw new Error(T('Public key is missing.')); + if (!v.user.trim()) throw new Error(T('User is missing.')); + return `${SUDO} u=${shq(v.user.trim())}; k=${shq(pub)}; d="$(getent passwd "$u" | cut -d: -f6)/.ssh"; ` + + `$S install -d -m 700 -o "$u" -g "$(id -gn "$u")" "$d" && { $S grep -qxF "$k" "$d/authorized_keys" 2>/dev/null || echo "$k" | $S tee -a "$d/authorized_keys" >/dev/null; } ` + + `&& $S chown "$u": "$d/authorized_keys" && $S chmod 600 "$d/authorized_keys" && echo "OK: $d/authorized_keys"`; + }, + }, + { + label: () => T('Install QEMU guest agent'), + desc: () => T('Installs and starts qemu-guest-agent (apt, dnf, yum, pacman, zypper or apk).'), + build: () => `${SUDO} if command -v apt-get >/dev/null; then $S apt-get update && $S apt-get install -y qemu-guest-agent; ` + + 'elif command -v dnf >/dev/null; then $S dnf install -y qemu-guest-agent; elif command -v yum >/dev/null; then $S yum install -y qemu-guest-agent; ' + + 'elif command -v pacman >/dev/null; then $S pacman -S --needed --noconfirm qemu-guest-agent; elif command -v zypper >/dev/null; then $S zypper -n install qemu-guest-agent; ' + + 'elif command -v apk >/dev/null; then $S apk add qemu-guest-agent && $S rc-update add qemu-guest-agent && $S rc-service qemu-guest-agent start; fi; ' + + 'command -v systemctl >/dev/null && { $S systemctl enable --now qemu-guest-agent 2>/dev/null || $S systemctl start qemu-guest-agent; }; ' + + 'command -v systemctl >/dev/null && systemctl is-active qemu-guest-agent', + }, + { + label: () => T('Use APT cache server'), + desc: () => T('Routes APT downloads through a cache proxy such as apt-cacher-ng (/etc/apt/apt.conf.d/00proxy).'), + params: () => [field(T('Proxy URL'), 'url', 'http://', { placeholder: 'http://192.168.1.10:3142' })], + build: (v) => { + const url = v.url.trim(); + if (!/^https?:\/\/[^\s/]+/.test(url)) throw new Error(T('Invalid URL.')); + return `${SUDO} echo ${shq(`Acquire::http::Proxy "${url}";`)} | $S tee /etc/apt/apt.conf.d/00proxy && $S apt-get update`; + }, + }, + { + label: () => T('Remove APT cache server'), + desc: () => T('Removes the APT proxy setting again.'), + build: () => `${SUDO} $S rm -f /etc/apt/apt.conf.d/00proxy && $S apt-get update`, + }, + { + label: () => T('System update'), + desc: () => T('Updates all packages with the system package manager.'), + build: () => `${SUDO} if command -v apt-get >/dev/null; then $S apt-get update && $S apt-get -y full-upgrade; elif command -v dnf >/dev/null; then $S dnf -y upgrade; ` + + 'elif command -v pacman >/dev/null; then $S pacman -Syu --noconfirm; elif command -v zypper >/dev/null; then $S zypper -n update; elif command -v apk >/dev/null; then $S apk upgrade --update; fi', + }, +]; + +// Fragt die Parameter einer Vorlage ab und schickt den Befehl ins Terminal +async function runTemplate(t, session) { + const params = t.params ? t.params() : []; + let v = {}; + if (params.length) { + const body = `

${esc(t.desc())}

${params.map((el) => el.outerHTML).join('')}`; + v = await modal({ title: t.label(), body, buttons: [{ label: T('Cancel'), value: null, cls: 'ghost' }, { label: T('Run'), value: 'form', cls: 'primary' }] }); + if (!v) return; + } + let command; + try { command = t.build(v); } catch (e) { return toast(e.message, 'error'); } + runSnippet({ command, autoRun: true }, session); +} + +function templateList(session) { + const d = h(`
${T('Templates')}
`); + for (const t of SNIPPET_TEMPLATES) { + const it = h(`
${esc(t.label())}
${esc(t.desc())}
`); + it.onclick = () => runTemplate(t, session); + $('.tpl-items', d).append(it); + } + return d; +} + function runSnippet(s, session) { const t = session || S.tabs.find((x) => x.id === S.active && x.kind === 'ssh') || [...S.tabs].reverse().find((x) => x.kind === 'ssh'); if (!t) return toast(T('No open terminal'), 'error'); @@ -1504,6 +1584,7 @@ class TerminalSession { it.onclick = () => runSnippet(s, this); box.append(it); }); + box.append(templateList(this)); } toggleFind(open) { diff --git a/src/renderer/styles.css b/src/renderer/styles.css index ef5179b..55e4113 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -191,6 +191,12 @@ body.in-session #sidebar { display: none; } .snip-panel .items { overflow: auto; padding: 8px; display: flex; flex-direction: column; gap: 6px; } .snip-panel .it { background: var(--card); border-radius: 8px; padding: 9px 10px; cursor: pointer; } .snip-panel .it:hover { background: var(--card-hover); } +.snip-tpl { margin-top: 10px; } +.snip-tpl summary { cursor: pointer; color: var(--muted); font-weight: 600; padding: 6px 2px; user-select: none; } +.snip-tpl .tpl-items { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; } +.snip-tpl .it { background: var(--card); border-radius: 8px; padding: 9px 10px; cursor: pointer; } +.snip-tpl .it:hover { background: var(--card-hover); } +.snip-tpl .hint { color: var(--muted); font-size: 12px; margin-top: 3px; } .snip-panel .it .mono { color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 3px; } .findbar { position: absolute; top: 8px; right: 24px; z-index: 4; display: none; gap: 4px; align-items: center; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 4px; box-shadow: 0 8px 24px #0008; } .findbar.open { display: flex; }