Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
618f5ad010 | ||
|
|
29fef7cb33 | ||
|
|
74ce2c20be | ||
|
|
e8fefe4ebe | ||
|
|
797722c510 | ||
|
|
c589033964 | ||
|
|
0cefd5a21f | ||
|
|
d53bd1a4c1 | ||
|
|
2890844c81 |
@@ -1,51 +1,97 @@
|
|||||||
# MrTerm
|
# MrTerm
|
||||||
|
|
||||||
SSH-, SFTP- und RDP-Client im Stil von Termius – Electron + xterm.js + ssh2. Läuft auf Windows und Arch/CachyOS.
|
A modern SSH, SFTP and RDP client for Windows and Arch Linux / CachyOS. All your servers live in one place and open in tabs: terminals, file transfers and remote desktops.
|
||||||
|
|
||||||
## Funktionen
|
MrTerm is available in **English** and **German**. It follows your system language by default, and you can change it under **Settings → Language**.
|
||||||
- **Hosts**: Gruppen (verschachtelt), Tags, Farben, Suche, Quick Connect (`user@host:port`, `rdp://host`)
|
|
||||||
- **SSH-Terminal** in Tabs: Passwort, Schlüssel, SSH-Agent (Windows OpenSSH / `SSH_AUTH_SOCK`), Keyboard-Interactive/2FA, Jump-Hosts (ProxyJump-Ketten), Startbefehl, Env-Variablen, Suche, Zoom, 7 Farbschemata
|
## Download & installation
|
||||||
- **SFTP**: Dual-Pane (lokal ↔ remote), Drag & Drop, Ordner rekursiv, Umbenennen, Löschen, chmod, Fortschrittsanzeige
|
|
||||||
- **RDP als Tab** direkt in MrTerm (Windows: mstsc eingebettet, Linux: xfreerdp3 via /parent-window unter X11/XWayland), alternativ im eigenen Fenster
|
Get the latest version from the [releases page](https://git.mrblake.cc/MrBlake/MrTerm/releases).
|
||||||
- **RDP**: Windows → `mstsc` (Anmeldedaten temporär via `cmdkey`), Linux → FreeRDP (`xfreerdp3`/`sdl-freerdp3`/`wlfreerdp3`, Passwort über stdin)
|
|
||||||
- **Keychain**: Ed25519/ECDSA/RSA generieren, importieren, Public Key kopieren
|
**Windows**
|
||||||
- **Port-Forwarding**: lokal (-L), remote (-R), dynamisch/SOCKS5 (-D)
|
- `MrTerm-Setup-<version>.exe`: installer (recommended)
|
||||||
- **Snippets**, **Known Hosts** (TOFU, Warnung bei geändertem Schlüssel), **Verlauf**, `~/.ssh/config`-Import, Backup Export/Import
|
- `MrTerm-<version>-portable.exe`: portable, runs without installation
|
||||||
- **Import aus Devolutions Remote Desktop Manager** (Einstellungen → Import): Ordnerstruktur, RDP- und SSH-Einträge aus `.rdm`/XML, JSON oder CSV
|
|
||||||
- **7 App-Designs** (Midnight, Navy, Nord, Dracula, Catppuccin, Forest, Hell) + frei wählbare Akzentfarbe; Terminal-Schema optional „passend zum Design“
|
**Arch Linux / CachyOS**
|
||||||
- **Auto-Update** aus den Gitea-Releases von https://git.mrblake.cc/MrBlake/MrTerm (Setup-EXE, Portable, AppImage, pacman)
|
- `MrTerm-<version>.pacman`: install with
|
||||||
- **Vault verschlüsselt** über das OS (Windows DPAPI, Linux libsecret/KWallet)
|
```sh
|
||||||
- Befehlspalette `Strg+Shift+K`, Tab schließen `Strg+Shift+W` (Strg+W/K/T bleiben im Terminal für nano & Co.), `Strg+Tab`, `Strg+1..9`, `Strg+Shift+C/V/F`
|
sudo pacman -U MrTerm-<version>.pacman
|
||||||
|
```
|
||||||
|
- `MrTerm-<version>.AppImage`: make it executable (`chmod +x`) and run it
|
||||||
|
|
||||||
|
Optional packages on Linux:
|
||||||
|
```sh
|
||||||
|
sudo pacman -S freerdp gnome-keyring # use kwallet instead of gnome-keyring on KDE
|
||||||
|
```
|
||||||
|
`freerdp` is needed for RDP connections. `gnome-keyring` or `kwallet` lets MrTerm encrypt your saved passwords and keys.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Hosts**: nested groups, tags, colors, search and Quick Connect (`user@host:port`, `rdp://host`)
|
||||||
|
- **SSH terminal in tabs**: password, key, SSH agent, keyboard-interactive/2FA, jump hosts (ProxyJump chains), startup command, environment variables, search, zoom and several color schemes
|
||||||
|
- **SFTP**: two-pane file browser (local ↔ remote), drag & drop, recursive folders, rename, delete, chmod and progress display
|
||||||
|
- **RDP in a tab** right inside MrTerm (Windows: `mstsc`, Linux: FreeRDP), or in a separate window if you prefer
|
||||||
|
- **Keychain**: generate Ed25519/ECDSA/RSA keys, import existing ones and copy the public key
|
||||||
|
- **Port forwarding**: local (-L), remote (-R) and dynamic/SOCKS5 (-D)
|
||||||
|
- **Snippets**: save frequently used commands and send them to a terminal with one click
|
||||||
|
- **Known hosts**: MrTerm warns you if a server's host key changes
|
||||||
|
- **History** of recent connections
|
||||||
|
- **Import** from `~/.ssh/config` and from **Devolutions Remote Desktop Manager** (`.rdm`/XML, JSON or CSV)
|
||||||
|
- **Backup** export and import
|
||||||
|
- **7 app themes** (Midnight, Navy, Nord, Dracula, Catppuccin, Forest, Light) plus a custom accent color
|
||||||
|
- **Encrypted vault** using your operating system's keyring (Windows DPAPI, Linux libsecret/KWallet)
|
||||||
|
- **Automatic updates**: MrTerm checks for new versions on startup and can install them for you
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
1. Click **New host**, enter the address, username and password or key, and click **Save**.
|
||||||
|
2. Double-click the host to connect. SSH hosts open a terminal, RDP hosts open a remote desktop tab.
|
||||||
|
3. Right-click a host for more options, such as opening SFTP, duplicating it or copying its address.
|
||||||
|
|
||||||
|
For a quick one-off connection, press `Ctrl+Shift+K` and type `user@host` (or `rdp://host`).
|
||||||
|
|
||||||
|
## Keyboard shortcuts
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
|---|---|
|
||||||
|
| `Ctrl+Shift+K` / `Ctrl+Shift+T` | Quick Connect / command palette (outside the terminal also `Ctrl+K`) |
|
||||||
|
| `Ctrl+Shift+W` | Close tab |
|
||||||
|
| `Ctrl+Tab` | Next tab |
|
||||||
|
| `Ctrl+1..9` | Switch to tab |
|
||||||
|
| `Ctrl+Shift+C` / `Ctrl+Shift+V` | Copy / paste in the terminal |
|
||||||
|
| `Ctrl+Shift+F` | Search in the terminal |
|
||||||
|
| `Ctrl` + `+` / `-` / `0` | Font size |
|
||||||
|
|
||||||
|
`Ctrl+W`, `Ctrl+K` and `Ctrl+T` still reach the terminal, so editors like nano work as usual.
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
|
||||||
|
MrTerm looks for updates on startup. You can also check manually under **Settings → Updates**. When a new version is available, click **Install now** and MrTerm will download the right package for your system and restart.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **"Vault not encrypted"** (Linux): install `gnome-keyring` or `kwallet` and restart MrTerm.
|
||||||
|
- **RDP doesn't work** (Linux): install FreeRDP with `sudo pacman -S freerdp`. Under **Settings → RDP** you can see which client MrTerm uses and choose another one.
|
||||||
|
- **RDP tab stays empty**: turn off *Show RDP connections as tabs* under **Settings → RDP** to use a separate window instead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## For developers
|
||||||
|
|
||||||
## Entwicklung
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
> In VS Code-Terminals ggf. vorher `ELECTRON_RUN_AS_NODE` entfernen.
|
In VS Code terminals, unset `ELECTRON_RUN_AS_NODE` first.
|
||||||
|
|
||||||
## Pakete bauen
|
**Building packages**
|
||||||
- Windows (auf Windows): `npm run dist:win` → Installer + Portable in `dist/`
|
- Windows: `npm run dist:win` (on Linux, this needs `wine`)
|
||||||
- Arch/CachyOS (auf Linux): `npm run dist:linux` → AppImage + `.pacman`-Paket; installieren mit `sudo pacman -U dist/mrterm-*.pacman`
|
- Arch/CachyOS: `npm run dist:linux`
|
||||||
|
|
||||||
Abhängigkeiten unter Arch/CachyOS: `sudo pacman -S freerdp gnome-keyring` (oder `kwallet` unter KDE) für RDP bzw. Vault-Verschlüsselung.
|
If the build fails with `EACCES: permission denied`, some files in `node_modules` belong to root. Run `sudo chown -R $USER: node_modules` and don't run npm with `sudo`.
|
||||||
|
|
||||||
**Build bricht mit `EACCES: permission denied, unlink 'build/config.gypi'` ab** (z. B. bei `cpu-features`): Dateien in `node_modules` gehören root, meist nach einem `npm install` oder Build mit `sudo`. Besitz zurückholen und erneut bauen:
|
**Publishing a release**: `./scripts/release-all.sh` asks for the version and release notes, commits and pushes the version, builds Windows + Arch/CachyOS and uploads everything to Gitea. It reads the token from `GITEA_TOKEN` or from `.gitea-token`, which is not committed.
|
||||||
|
|
||||||
```sh
|
**Translations** live in [`src/i18n.js`](src/i18n.js). The English text in the code is the key. To add a language, add a dictionary and list it in `LANGUAGES`.
|
||||||
sudo chown -R $USER: node_modules
|
|
||||||
npm run dist:linux
|
|
||||||
```
|
|
||||||
|
|
||||||
npm und die Builds danach nicht mehr mit `sudo` ausführen – nur `pacman -U` zum Installieren braucht root.
|
To regenerate the icons after changing the logo, run `npx electron scripts/make-icons.js`.
|
||||||
|
|
||||||
## Release veröffentlichen (für Auto-Update)
|
|
||||||
1. Version in `package.json` erhöhen
|
|
||||||
2. Auf jeder Plattform bauen (`npm run dist:win` bzw. `npm run dist:linux`)
|
|
||||||
3. `GITEA_TOKEN=<token> npm run release -- --notes "Was ist neu"` – legt das Release `v<version>` an und lädt die Pakete hoch
|
|
||||||
|
|
||||||
Oder alles in einem Schritt unter Linux (Windows-Build per `wine`): `./scripts/release-all.sh` – fragt nach Version und Notizen, committet/pusht die Version, baut Windows + Arch/CachyOS und lädt hoch. Token aus `GITEA_TOKEN` oder `.gitea-token` (nicht eingecheckt).
|
|
||||||
|
|
||||||
Installierte Clients prüfen beim Start (und über Einstellungen → Updates) und installieren das passende Paket. Für ein privates Repo in den Einstellungen ein Lese-Token hinterlegen.
|
|
||||||
|
|
||||||
Icons neu erzeugen nach Logo-Änderung: `npx electron scripts/make-icons.js`
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mrterm",
|
"name": "mrterm",
|
||||||
"version": "0.2.0",
|
"version": "0.4.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mrterm",
|
"name": "mrterm",
|
||||||
"version": "0.2.0",
|
"version": "0.4.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "mrterm",
|
"name": "mrterm",
|
||||||
"productName": "MrTerm",
|
"productName": "MrTerm",
|
||||||
"version": "0.3.0",
|
"version": "0.4.3",
|
||||||
"description": "Moderner SSH-, SFTP- und RDP-Client",
|
"description": "Moderner SSH-, SFTP- und RDP-Client",
|
||||||
"main": "src/main/main.js",
|
"main": "src/main/main.js",
|
||||||
"author": "MrBlake",
|
"author": "MrBlake",
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ if [[ "$version" != "$current" ]]; then
|
|||||||
git add package.json package-lock.json
|
git add package.json package-lock.json
|
||||||
git commit -m "Version ${version}"
|
git commit -m "Version ${version}"
|
||||||
fi
|
fi
|
||||||
git push origin HEAD
|
# Push mit dem Gitea-Token (Basic-Auth, Benutzer aus GITEA_USER, Standard MrBlake)
|
||||||
|
auth="$(printf '%s:%s' "${GITEA_USER:-MrBlake}" "$GITEA_TOKEN" | base64 -w0)"
|
||||||
|
git -c http.extraHeader="Authorization: Basic ${auth}" push origin HEAD
|
||||||
|
|
||||||
rm -rf dist
|
rm -rf dist
|
||||||
npm run dist:linux
|
npm run dist:linux
|
||||||
|
|||||||
+365
@@ -0,0 +1,365 @@
|
|||||||
|
// Übersetzungen für Renderer (window.I18N) und Main-Prozess (require).
|
||||||
|
// Quellsprache ist Englisch: der Text im Code ist der Schlüssel, weitere Sprachen liefern ein Wörterbuch.
|
||||||
|
// Neue Sprache: Wörterbuch unten ergänzen und in LANGUAGES eintragen.
|
||||||
|
(function (root) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const de = {
|
||||||
|
// Allgemein
|
||||||
|
'Cancel': 'Abbrechen',
|
||||||
|
'Save': 'Speichern',
|
||||||
|
'OK': 'OK',
|
||||||
|
'Delete': 'Löschen',
|
||||||
|
'Delete?': 'Löschen?',
|
||||||
|
'Edit': 'Bearbeiten',
|
||||||
|
'Open': 'Öffnen',
|
||||||
|
'Close': 'Schließen',
|
||||||
|
'Close (Ctrl+Shift+W)': 'Schließen (Strg+Shift+W)',
|
||||||
|
'Close others': 'Andere schließen',
|
||||||
|
'Rename': 'Umbenennen',
|
||||||
|
'Duplicate': 'Duplizieren',
|
||||||
|
'Copy': 'Kopieren',
|
||||||
|
'Copied': 'Kopiert',
|
||||||
|
'Connect': 'Verbinden',
|
||||||
|
'Reconnect': 'Neu verbinden',
|
||||||
|
'Reconnecting': 'Neu verbinden',
|
||||||
|
'Remove': 'Entfernen',
|
||||||
|
'Import': 'Importieren',
|
||||||
|
'Generate': 'Generieren',
|
||||||
|
'Automatic': 'Automatisch',
|
||||||
|
'Color': 'Farbe',
|
||||||
|
'Name': 'Name',
|
||||||
|
'Type': 'Typ',
|
||||||
|
'Title': 'Titel',
|
||||||
|
'Label': 'Bezeichnung',
|
||||||
|
'Settings': 'Einstellungen',
|
||||||
|
'Menu': 'Menü',
|
||||||
|
'History': 'Verlauf',
|
||||||
|
'Minimize': 'Minimieren',
|
||||||
|
'Maximize': 'Maximieren',
|
||||||
|
'Quick Connect (Ctrl+Shift+K)': 'Quick Connect (Strg+Shift+K)',
|
||||||
|
'Collapse menu': 'Menü einklappen',
|
||||||
|
'Expand menu': 'Menü ausklappen',
|
||||||
|
'Vault encrypted': 'Vault verschlüsselt',
|
||||||
|
'Vault not encrypted': 'Vault unverschlüsselt',
|
||||||
|
'Data is encrypted with the system keyring.': 'Daten werden mit dem System-Schlüsselbund verschlüsselt.',
|
||||||
|
'No system keyring available (e.g. install gnome-keyring/kwallet).': 'Kein System-Schlüsselbund verfügbar (z. B. gnome-keyring/kwallet installieren).',
|
||||||
|
'just now': 'gerade eben',
|
||||||
|
'{n} min ago': 'vor {n} Min.',
|
||||||
|
'{n} h ago': 'vor {n} Std.',
|
||||||
|
'{n} days ago': 'vor {n} Tagen',
|
||||||
|
|
||||||
|
// Verzeichnisbaum / Hosts
|
||||||
|
'Connections': 'Verbindungen',
|
||||||
|
'Collapse all': 'Alle einklappen',
|
||||||
|
'No hosts yet': 'Noch keine Hosts',
|
||||||
|
'Add host': 'Host hinzufügen',
|
||||||
|
'Create subfolder': 'Unterordner anlegen',
|
||||||
|
'Delete group?': 'Gruppe löschen?',
|
||||||
|
'“{name}” will be deleted. Hosts are kept.': '„{name}“ wird gelöscht. Hosts bleiben erhalten.',
|
||||||
|
'Search hosts or enter user@host …': 'Hosts suchen oder user@host eingeben …',
|
||||||
|
'Group': 'Gruppe',
|
||||||
|
'Groups': 'Gruppen',
|
||||||
|
'New host': 'Neuer Host',
|
||||||
|
'All hosts': 'Alle Hosts',
|
||||||
|
'Connect to {q}?': 'Mit {q} verbinden?',
|
||||||
|
'Quick Connect without saving the host.': 'Quick Connect ohne den Host zu speichern.',
|
||||||
|
'Add your first SSH or RDP host or import ~/.ssh/config.': 'Lege deinen ersten SSH- oder RDP-Host an oder importiere ~/.ssh/config.',
|
||||||
|
'Import ~/.ssh/config': '~/.ssh/config importieren',
|
||||||
|
'No results': 'Keine Treffer',
|
||||||
|
'Adjust the search or filters.': 'Passe die Suche oder Filter an.',
|
||||||
|
'Connect in tab': 'Im Tab verbinden',
|
||||||
|
'Open in separate window': 'In eigenem Fenster öffnen',
|
||||||
|
'Open SFTP': 'SFTP öffnen',
|
||||||
|
' (copy)': ' (Kopie)',
|
||||||
|
'Copy address': 'Adresse kopieren',
|
||||||
|
'Delete host?': 'Host löschen?',
|
||||||
|
'“{name}” will be permanently removed.': '„{name}“ wird dauerhaft entfernt.',
|
||||||
|
'RDP in a tab is not possible ({reason}) – opening a separate window.': 'RDP im Tab nicht möglich ({reason}) – öffne eigenes Fenster.',
|
||||||
|
'RDP session to {name} started ({client})': 'RDP-Sitzung zu {name} gestartet ({client})',
|
||||||
|
'Invalid address. Format: user@host:port': 'Ungültige Adresse. Format: user@host:port',
|
||||||
|
'{n} host(s) imported': '{n} Host(s) importiert',
|
||||||
|
|
||||||
|
// Host-Editor
|
||||||
|
'— No group —': '— Keine Gruppe —',
|
||||||
|
'— No key —': '— Kein Schlüssel —',
|
||||||
|
'— Direct —': '— Direkt —',
|
||||||
|
'SSH key': 'SSH-Schlüssel',
|
||||||
|
'Use SSH agent': 'SSH-Agent verwenden',
|
||||||
|
'Advanced': 'Erweitert',
|
||||||
|
'Jump host (ProxyJump)': 'Jump-Host (ProxyJump)',
|
||||||
|
'Startup command': 'Startbefehl',
|
||||||
|
'e.g. tmux attach || tmux': 'z. B. tmux attach || tmux',
|
||||||
|
'Environment variables': 'Umgebungsvariablen',
|
||||||
|
'One per line, KEY=VALUE (server must allow AcceptEnv).': 'Eine pro Zeile, KEY=VALUE (Server muss AcceptEnv erlauben).',
|
||||||
|
'Domain': 'Domäne',
|
||||||
|
'optional': 'optional',
|
||||||
|
'Width': 'Breite',
|
||||||
|
'Height': 'Höhe',
|
||||||
|
'On resize': 'Bei Größenänderung',
|
||||||
|
'Adjust resolution dynamically': 'Auflösung dynamisch anpassen',
|
||||||
|
'Scale image (fixed resolution)': 'Bild skalieren (feste Auflösung)',
|
||||||
|
'Fullscreen': 'Vollbild',
|
||||||
|
'Share clipboard': 'Zwischenablage teilen',
|
||||||
|
'Play audio': 'Audio wiedergeben',
|
||||||
|
'Share local drives/home': 'Lokale Laufwerke/Home freigeben',
|
||||||
|
'Edit host': 'Host bearbeiten',
|
||||||
|
'Address': 'Adresse',
|
||||||
|
'IP or hostname': 'IP oder Hostname',
|
||||||
|
'My server': 'Mein Server',
|
||||||
|
'Authentication': 'Anmeldung',
|
||||||
|
'Username': 'Benutzername',
|
||||||
|
'Password': 'Passwort',
|
||||||
|
'stored encrypted': 'wird verschlüsselt gespeichert',
|
||||||
|
'Appearance': 'Darstellung',
|
||||||
|
'Please enter an address.': 'Bitte eine Adresse angeben.',
|
||||||
|
'Edit group': 'Gruppe bearbeiten',
|
||||||
|
'New group': 'Neue Gruppe',
|
||||||
|
'Production': 'Produktion',
|
||||||
|
'Parent group': 'Übergeordnete Gruppe',
|
||||||
|
'Name is missing.': 'Name fehlt.',
|
||||||
|
|
||||||
|
// Keychain
|
||||||
|
'Search keys …': 'Schlüssel suchen …',
|
||||||
|
'No keys': 'Keine Schlüssel',
|
||||||
|
'Generate a new Ed25519 key or import an existing one.': 'Generiere einen neuen Ed25519-Schlüssel oder importiere einen bestehenden.',
|
||||||
|
'Generate key': 'Schlüssel generieren',
|
||||||
|
'Keys': 'Schlüssel',
|
||||||
|
'Copy public key': 'Public Key kopieren',
|
||||||
|
'Public key copied': 'Public Key kopiert',
|
||||||
|
'Delete key?': 'Schlüssel löschen?',
|
||||||
|
'“{name}” will be removed from the vault.': '„{name}“ wird aus dem Vault entfernt.',
|
||||||
|
'Generate SSH key': 'SSH-Schlüssel generieren',
|
||||||
|
'recommended': 'empfohlen',
|
||||||
|
'Key generated – public key copied to clipboard': 'Schlüssel erzeugt – Public Key in Zwischenablage kopiert',
|
||||||
|
'Edit key': 'Schlüssel bearbeiten',
|
||||||
|
'Import key': 'Schlüssel importieren',
|
||||||
|
'Derived from the private key automatically if left empty.': 'Wird bei Bedarf automatisch aus dem Private Key berechnet.',
|
||||||
|
'Private key is missing.': 'Private Key fehlt.',
|
||||||
|
'File…': 'Datei…',
|
||||||
|
|
||||||
|
// Snippets
|
||||||
|
'Search snippets …': 'Snippets suchen …',
|
||||||
|
'New snippet': 'Neues Snippet',
|
||||||
|
'No snippets': 'Keine Snippets',
|
||||||
|
'Save frequently used commands and send them to a terminal with one click.': 'Speichere häufig genutzte Befehle und sende sie mit einem Klick an ein Terminal.',
|
||||||
|
'Run in active terminal': 'Im aktiven Terminal ausführen',
|
||||||
|
'Delete snippet?': 'Snippet löschen?',
|
||||||
|
'Edit snippet': 'Snippet bearbeiten',
|
||||||
|
'System update': 'Systemupdate',
|
||||||
|
'Command(s)': 'Befehl(e)',
|
||||||
|
'Send Enter automatically': 'Enter automatisch senden',
|
||||||
|
'Command is missing.': 'Befehl fehlt.',
|
||||||
|
'No open terminal': 'Kein offenes Terminal',
|
||||||
|
'No snippets yet.': 'Noch keine Snippets.',
|
||||||
|
|
||||||
|
// Port Forwarding
|
||||||
|
'Search rules …': 'Regeln suchen …',
|
||||||
|
'New rule': 'Neue Regel',
|
||||||
|
'No port forwards': 'Keine Port-Weiterleitungen',
|
||||||
|
'Local (-L), remote (-R) or dynamic as SOCKS5 proxy (-D).': 'Lokal (-L), Remote (-R) oder dynamisch als SOCKS5-Proxy (-D).',
|
||||||
|
'Local': 'Lokal',
|
||||||
|
'active': 'aktiv',
|
||||||
|
'inactive': 'inaktiv',
|
||||||
|
'Stop': 'Stoppen',
|
||||||
|
'Start': 'Starten',
|
||||||
|
'Forward active': 'Weiterleitung aktiv',
|
||||||
|
'Delete rule?': 'Regel löschen?',
|
||||||
|
'Add an SSH host first.': 'Lege zuerst einen SSH-Host an.',
|
||||||
|
'Edit rule': 'Regel bearbeiten',
|
||||||
|
'New forward': 'Neue Weiterleitung',
|
||||||
|
'Local (-L)': 'Lokal (-L)',
|
||||||
|
'Dynamic / SOCKS5 (-D)': 'Dynamisch / SOCKS5 (-D)',
|
||||||
|
'Via host': 'Über Host',
|
||||||
|
'Bind address': 'Bind-Adresse',
|
||||||
|
'Bind port': 'Bind-Port',
|
||||||
|
'Target host': 'Ziel-Host',
|
||||||
|
'Target port': 'Ziel-Port',
|
||||||
|
'Bind port is missing.': 'Bind-Port fehlt.',
|
||||||
|
'Target port is missing.': 'Ziel-Port fehlt.',
|
||||||
|
|
||||||
|
// Known Hosts & Verlauf
|
||||||
|
'Search known hosts …': 'Known Hosts suchen …',
|
||||||
|
'No known hosts': 'Keine bekannten Hosts',
|
||||||
|
'Host keys are stored here on first connection.': 'Host-Schlüssel werden bei der ersten Verbindung hier gespeichert.',
|
||||||
|
'Search history …': 'Verlauf durchsuchen …',
|
||||||
|
'No connections yet': 'Noch keine Verbindungen',
|
||||||
|
'Recently used hosts appear here.': 'Zuletzt genutzte Hosts erscheinen hier.',
|
||||||
|
|
||||||
|
// Einstellungen
|
||||||
|
'Language': 'Sprache',
|
||||||
|
'System default': 'Systemsprache',
|
||||||
|
'Accent color': 'Akzentfarbe',
|
||||||
|
'Theme default': 'Standard des Designs',
|
||||||
|
'Light': 'Hell',
|
||||||
|
'Terminal color scheme': 'Terminal-Farbschema',
|
||||||
|
'Match theme': 'Passend zum Design',
|
||||||
|
'follows the theme': 'folgt dem Design',
|
||||||
|
'Font': 'Schriftart',
|
||||||
|
'Size': 'Größe',
|
||||||
|
'Bar': 'Balken',
|
||||||
|
'Underline': 'Unterstrich',
|
||||||
|
'Scrollback lines': 'Scrollback-Zeilen',
|
||||||
|
'Blinking cursor': 'Cursor blinkt',
|
||||||
|
'Copy on select': 'Auswahl automatisch kopieren',
|
||||||
|
'SSH keep-alive (seconds, 0 = off)': 'SSH Keep-Alive (Sekunden, 0 = aus)',
|
||||||
|
'Checking …': 'Prüfe …',
|
||||||
|
'Show RDP connections as tabs in MrTerm': 'RDP-Verbindungen als Tab in MrTerm anzeigen',
|
||||||
|
'Takes effect after restarting MrTerm (X11/XWayland mode).': 'Wird nach einem Neustart von MrTerm wirksam (X11/XWayland-Modus).',
|
||||||
|
'Uses xfreerdp3 with /parent-window. For this, MrTerm runs via XWayland under Wayland.': 'Nutzt xfreerdp3 mit /parent-window. MrTerm läuft dafür unter Wayland über XWayland.',
|
||||||
|
'FreeRDP client (separate window)': 'FreeRDP-Client (eigenes Fenster)',
|
||||||
|
'Client in use: {client}': 'Verwendeter Client: {client}',
|
||||||
|
'No RDP client found. Install: {hint}': 'Kein RDP-Client gefunden. Installation: {hint}',
|
||||||
|
'Data': 'Daten',
|
||||||
|
'The vault is encrypted with the operating system keyring (Windows DPAPI / libsecret or KWallet).': 'Der Vault ist mit dem Schlüsselbund des Betriebssystems verschlüsselt (Windows DPAPI / libsecret bzw. KWallet).',
|
||||||
|
'The vault is not encrypted because no keyring is available. On Arch/CachyOS: install <code>gnome-keyring</code> or <code>kwallet</code>.': 'Der Vault ist nicht verschlüsselt, da kein Schlüsselbund verfügbar ist. Unter Arch/CachyOS: <code>gnome-keyring</code> oder <code>kwallet</code> installieren.',
|
||||||
|
'Export backup': 'Backup exportieren',
|
||||||
|
'Exported to {path}': 'Exportiert nach {path}',
|
||||||
|
'Import backup': 'Backup importieren',
|
||||||
|
'Import complete': 'Import abgeschlossen',
|
||||||
|
'Import folders and hosts (RDP & SSH) from <b>Devolutions Remote Desktop Manager</b>. In RDM: <i>File → Export → Export entries</i> as <code>.rdm</code> (XML), JSON or CSV. RDM only exports passwords unencrypted if you enable that during export.': 'Ordner und Hosts (RDP & SSH) aus <b>Devolutions Remote Desktop Manager</b> übernehmen. In RDM: <i>Datei → Exportieren → Einträge exportieren</i> als <code>.rdm</code> (XML), JSON oder CSV. Passwörter werden von RDM nur unverschlüsselt exportiert, wenn du das beim Export aktivierst.',
|
||||||
|
'Import from Remote Desktop Manager…': 'Aus Remote Desktop Manager importieren…',
|
||||||
|
'Installed version: {v}': 'Installierte Version: {v}',
|
||||||
|
'Gitea/Forgejo repository whose releases contain the installation packages.': 'Gitea/Forgejo-Repository, dessen Releases die Installationspakete enthalten.',
|
||||||
|
'Access token (private repos only)': 'Zugriffstoken (nur für private Repos)',
|
||||||
|
'Check for updates on startup': 'Beim Start automatisch nach Updates suchen',
|
||||||
|
'Include pre-releases': 'Vorabversionen (Pre-Releases) einbeziehen',
|
||||||
|
'Check for updates now': 'Jetzt nach Updates suchen',
|
||||||
|
'The repository has version {repo}, but there is no release with installation packages for it yet. Installed: {current}.': 'Im Repository liegt Version {repo}, aber dafür gibt es noch kein Release mit Installationspaketen. Installiert: {current}.',
|
||||||
|
'The repository has no releases yet.': 'Im Repository gibt es noch keine Releases.',
|
||||||
|
'MrTerm is up to date ({v}).': 'MrTerm ist aktuell ({v}).',
|
||||||
|
'Keyboard shortcuts': 'Tastenkürzel',
|
||||||
|
'Ctrl': 'Strg',
|
||||||
|
'Quick Connect / command palette (outside the terminal also {k})': 'Quick Connect / Befehlspalette (außerhalb des Terminals auch {k})',
|
||||||
|
'Close tab': 'Tab schließen',
|
||||||
|
'Next tab': 'Nächster Tab',
|
||||||
|
'Switch to tab': 'Zu Tab wechseln',
|
||||||
|
'Copy / paste in terminal': 'Kopieren / Einfügen im Terminal',
|
||||||
|
'Search in terminal': 'Im Terminal suchen',
|
||||||
|
'Font size': 'Schriftgröße',
|
||||||
|
|
||||||
|
// RDM-Import
|
||||||
|
'Import failed: {msg}': 'Import fehlgeschlagen: {msg}',
|
||||||
|
'No RDP or SSH entries found in the file.': 'Keine RDP- oder SSH-Einträge in der Datei gefunden.',
|
||||||
|
'Root level': 'Stammebene',
|
||||||
|
'Import from Remote Desktop Manager': 'Import aus Remote Desktop Manager',
|
||||||
|
'{file}: found {n} hosts ({rdp} RDP, {ssh} SSH) in {folders} folders.': '{file}: {n} Hosts ({rdp} RDP, {ssh} SSH) in {folders} Ordnern gefunden.',
|
||||||
|
'… and {n} more': '… und {n} weitere',
|
||||||
|
'{n} hosts imported': '{n} Hosts importiert',
|
||||||
|
', {n} already existed': ', {n} bereits vorhanden',
|
||||||
|
'File is not valid XML.': 'Datei ist kein gültiges XML.',
|
||||||
|
'CSV columns not recognized (expected e.g. Name, Host, ConnectionType, Group).': 'CSV-Spalten nicht erkannt (erwartet u. a. Name, Host, ConnectionType, Group).',
|
||||||
|
|
||||||
|
// Updates
|
||||||
|
'MrTerm {v} is available': 'MrTerm {v} ist verfügbar',
|
||||||
|
'Installed: {v}': 'Installiert: {v}',
|
||||||
|
'Package: {name} ({size})': 'Paket: {name} ({size})',
|
||||||
|
'Development mode: please update via <code>git pull</code>.': 'Entwicklungsmodus: Bitte per <code>git pull</code> aktualisieren.',
|
||||||
|
'The release contains no package for this system.': 'Im Release ist kein passendes Paket für dieses System.',
|
||||||
|
'Downloading …': 'Lade herunter …',
|
||||||
|
'Later': 'Später',
|
||||||
|
'Release page': 'Release-Seite',
|
||||||
|
'Install now': 'Jetzt installieren',
|
||||||
|
'Downloading update …': 'Update wird heruntergeladen …',
|
||||||
|
'Installing update, MrTerm will restart …': 'Update wird installiert, MrTerm startet neu …',
|
||||||
|
|
||||||
|
// Tabs & Sitzungen
|
||||||
|
'Rename tab': 'Tab umbenennen',
|
||||||
|
'SFTP for this host': 'SFTP für diesen Host',
|
||||||
|
'Search (Ctrl+Shift+F)': 'Suchen (Strg+Shift+F)',
|
||||||
|
'Search …': 'Suchen …',
|
||||||
|
'Connecting to {host} …': 'Verbinde mit {host} …',
|
||||||
|
'Connection closed. Press [Enter] to reconnect.': 'Verbindung beendet. [Enter] zum erneuten Verbinden.',
|
||||||
|
'Continue session in a separate window': 'Sitzung als eigenes Fenster weiterführen',
|
||||||
|
'Detach as window': 'Als Fenster lösen',
|
||||||
|
'RDP session hidden while a dialog is open.': 'RDP-Sitzung ausgeblendet, solange ein Dialog geöffnet ist.',
|
||||||
|
'In separate window': 'Im eigenen Fenster',
|
||||||
|
'Connecting to {host} … Login or certificate prompts may appear in a separate window.': 'Verbinde mit {host} … Anmelde- oder Zertifikatsabfragen erscheinen ggf. als eigenes Fenster.',
|
||||||
|
'The RDP window could not be embedded.': 'Das RDP-Fenster konnte nicht eingebettet werden.',
|
||||||
|
'RDP session ended.': 'RDP-Sitzung beendet.',
|
||||||
|
'RDP session ended (code {code}).': 'RDP-Sitzung beendet (Code {code}).',
|
||||||
|
'Session continues in a separate window': 'Sitzung läuft als eigenes Fenster weiter',
|
||||||
|
|
||||||
|
// SFTP
|
||||||
|
'Choose a host for SFTP …': 'Host für SFTP wählen …',
|
||||||
|
'No SSH hosts': 'Keine SSH-Hosts',
|
||||||
|
'SFTP requires an SSH host.': 'SFTP benötigt einen SSH-Host.',
|
||||||
|
'Open host in SFTP browser': 'Host für SFTP-Browser öffnen',
|
||||||
|
'Parent folder': 'Übergeordneter Ordner',
|
||||||
|
'Refresh': 'Aktualisieren',
|
||||||
|
'New folder': 'Neuer Ordner',
|
||||||
|
'Upload →': 'Hochladen →',
|
||||||
|
'← Download': '← Herunterladen',
|
||||||
|
'Upload': 'Hochladen',
|
||||||
|
'Download': 'Herunterladen',
|
||||||
|
'Modified': 'Geändert',
|
||||||
|
'Copy path': 'Pfad kopieren',
|
||||||
|
'Permissions (chmod)…': 'Rechte (chmod)…',
|
||||||
|
'Move to trash': 'In Papierkorb',
|
||||||
|
'New name': 'Neuer Name',
|
||||||
|
'Change permissions': 'Rechte ändern',
|
||||||
|
'Octal (e.g. 644)': 'Oktal (z. B. 644)',
|
||||||
|
'Move {n} item(s) to the trash: {list}': '{n} Element(e) in den Papierkorb verschieben: {list}',
|
||||||
|
'Permanently delete {n} item(s) from the server: {list}': '{n} Element(e) endgültig vom Server löschen: {list}',
|
||||||
|
'Nothing selected': 'Nichts ausgewählt',
|
||||||
|
'Try again': 'Erneut versuchen',
|
||||||
|
'done': 'fertig',
|
||||||
|
'Error': 'Fehler',
|
||||||
|
|
||||||
|
// Befehlspalette & Anmeldung
|
||||||
|
'Search hosts or enter user@host:port or rdp://host …': 'Host suchen, user@host:port oder rdp://host eingeben …',
|
||||||
|
'Actions': 'Aktionen',
|
||||||
|
'Log in': 'Anmelden',
|
||||||
|
|
||||||
|
// Main-Prozess
|
||||||
|
'Host key has changed!': 'Host-Schlüssel hat sich geändert!',
|
||||||
|
'Unknown host': 'Unbekannter Host',
|
||||||
|
'WARNING: The host key of {target} has changed. This may indicate a man-in-the-middle attack.': 'WARNUNG: Der Host-Schlüssel von {target} hat sich geändert. Das kann auf einen Man-in-the-Middle-Angriff hindeuten.',
|
||||||
|
'The authenticity of {target} cannot be verified.': 'Die Echtheit von {target} kann nicht bestätigt werden.',
|
||||||
|
'Previously stored:': 'Bisher gespeichert:',
|
||||||
|
'Connect anyway & replace': 'Trotzdem verbinden & ersetzen',
|
||||||
|
'Trust & connect': 'Vertrauen & verbinden',
|
||||||
|
'~/.ssh/config not found': '~/.ssh/config nicht gefunden',
|
||||||
|
'Host not found': 'Host nicht gefunden',
|
||||||
|
'Rule not found': 'Regel nicht gefunden',
|
||||||
|
'RDP exited (code {code}): {err}': 'RDP beendet (Code {code}): {err}',
|
||||||
|
'RDP launch failed: {msg}': 'RDP-Start fehlgeschlagen: {msg}',
|
||||||
|
'xfreerdp3 missing (sudo pacman -S freerdp)': 'xfreerdp3 fehlt (sudo pacman -S freerdp)',
|
||||||
|
'All files': 'Alle Dateien',
|
||||||
|
'No FreeRDP found. Install with: sudo pacman -S freerdp': 'Kein FreeRDP gefunden. Installieren mit: sudo pacman -S freerdp',
|
||||||
|
'No connection to the X server (MrTerm is not running under X11/XWayland).': 'Keine Verbindung zum X-Server (MrTerm läuft nicht unter X11/XWayland).',
|
||||||
|
'koffi not available': 'koffi nicht verfügbar',
|
||||||
|
'Platform not supported': 'Plattform nicht unterstützt',
|
||||||
|
'Connecting to {name} ({addr}) …': 'Verbinde mit {name} ({addr}) …',
|
||||||
|
'Password for {user}': 'Passwort für {user}',
|
||||||
|
'SFTP session not found': 'SFTP-Sitzung nicht gefunden',
|
||||||
|
'Invalid repository URL: {url}': 'Ungültige Repository-URL: {url}',
|
||||||
|
'Repository not found (private? Then add an access token in the settings).': 'Repository nicht gefunden (privat? Dann Zugriffstoken in den Einstellungen hinterlegen).',
|
||||||
|
'Update server responded with {status}': 'Update-Server antwortet mit {status}',
|
||||||
|
'No matching package for this system found in the release.': 'Kein passendes Paket für dieses System im Release gefunden.',
|
||||||
|
'Download failed ({status})': 'Download fehlgeschlagen ({status})',
|
||||||
|
'pacman exited with code {code}': 'pacman beendet mit Code {code}',
|
||||||
|
'Automatic installation is not possible here. The file is located at: {file}': 'Automatische Installation hier nicht möglich. Datei liegt unter: {file}',
|
||||||
|
};
|
||||||
|
|
||||||
|
const DICTS = { de };
|
||||||
|
const LANGUAGES = [['en', 'English'], ['de', 'Deutsch']];
|
||||||
|
let lang = 'en';
|
||||||
|
|
||||||
|
// pref: 'auto' oder Sprachcode; system: z. B. navigator.language / app.getLocale()
|
||||||
|
function setLanguage(pref, system) {
|
||||||
|
const code = String(pref && pref !== 'auto' ? pref : system || 'en').slice(0, 2).toLowerCase();
|
||||||
|
lang = DICTS[code] ? code : 'en';
|
||||||
|
return lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
function t(s, vars) {
|
||||||
|
let r = (DICTS[lang] && DICTS[lang][s]) || s;
|
||||||
|
if (vars) r = r.replace(/\{(\w+)\}/g, (m, k) => (k in vars ? String(vars[k]) : m));
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = { t, setLanguage, LANGUAGES, get lang() { return lang; } };
|
||||||
|
if (typeof module === 'object' && module.exports) module.exports = api;
|
||||||
|
else root.I18N = api;
|
||||||
|
})(this);
|
||||||
+16
-12
@@ -9,6 +9,8 @@ const { SshManager } = require('./ssh');
|
|||||||
const rdp = require('./rdp');
|
const rdp = require('./rdp');
|
||||||
const { Updater } = require('./updater');
|
const { Updater } = require('./updater');
|
||||||
const { createEmbed, embedSupported } = require('./rdp-embed');
|
const { createEmbed, embedSupported } = require('./rdp-embed');
|
||||||
|
const i18n = require('../i18n');
|
||||||
|
const applyLanguage = () => i18n.setLanguage(store.get().settings.language, app.getLocale());
|
||||||
|
|
||||||
let win;
|
let win;
|
||||||
const store = new Store();
|
const store = new Store();
|
||||||
@@ -34,12 +36,12 @@ async function confirmHostKey(target, fingerprint, known) {
|
|||||||
const changed = !!known;
|
const changed = !!known;
|
||||||
const r = await dialog.showMessageBox(win, {
|
const r = await dialog.showMessageBox(win, {
|
||||||
type: changed ? 'warning' : 'question',
|
type: changed ? 'warning' : 'question',
|
||||||
title: changed ? 'Host-Schlüssel hat sich geändert!' : 'Unbekannter Host',
|
title: changed ? i18n.t('Host key has changed!') : i18n.t('Unknown host'),
|
||||||
message: changed
|
message: changed
|
||||||
? `WARNUNG: Der Host-Schlüssel von ${target.host}:${target.port} hat sich geändert. Das kann auf einen Man-in-the-Middle-Angriff hindeuten.`
|
? i18n.t('WARNING: The host key of {target} has changed. This may indicate a man-in-the-middle attack.', { target: `${target.host}:${target.port}` })
|
||||||
: `Die Echtheit von ${target.host}:${target.port} kann nicht bestätigt werden.`,
|
: i18n.t('The authenticity of {target} cannot be verified.', { target: `${target.host}:${target.port}` }),
|
||||||
detail: `Fingerprint:\n${fingerprint}${changed ? `\n\nBisher gespeichert:\n${known.fingerprint}` : ''}`,
|
detail: `Fingerprint:\n${fingerprint}${changed ? `\n\n${i18n.t('Previously stored:')}\n${known.fingerprint}` : ''}`,
|
||||||
buttons: [changed ? 'Trotzdem verbinden & ersetzen' : 'Vertrauen & verbinden', 'Abbrechen'],
|
buttons: [changed ? i18n.t('Connect anyway & replace') : i18n.t('Trust & connect'), i18n.t('Cancel')],
|
||||||
defaultId: changed ? 1 : 0,
|
defaultId: changed ? 1 : 0,
|
||||||
cancelId: 1,
|
cancelId: 1,
|
||||||
});
|
});
|
||||||
@@ -104,6 +106,7 @@ handle('vault:upsert', (col, item) => store.upsert(col, item));
|
|||||||
handle('vault:remove', (col, id) => store.remove(col, id));
|
handle('vault:remove', (col, id) => store.remove(col, id));
|
||||||
handle('vault:settings', (s) => {
|
handle('vault:settings', (s) => {
|
||||||
store.setSettings(s);
|
store.setSettings(s);
|
||||||
|
if ('language' in s) applyLanguage();
|
||||||
if ('rdpEmbed' in s) fs.writeFileSync(launchFile, JSON.stringify({ ...readLaunch(), x11: s.rdpEmbed !== false }));
|
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:forgetHost', (id) => { delete store.get().knownHosts[id]; store.save(); });
|
||||||
@@ -125,7 +128,7 @@ handle('vault:import', async () => {
|
|||||||
// Import aus ~/.ssh/config
|
// Import aus ~/.ssh/config
|
||||||
handle('vault:importSshConfig', () => {
|
handle('vault:importSshConfig', () => {
|
||||||
const file = path.join(os.homedir(), '.ssh', 'config');
|
const file = path.join(os.homedir(), '.ssh', 'config');
|
||||||
if (!fs.existsSync(file)) throw new Error('~/.ssh/config nicht gefunden');
|
if (!fs.existsSync(file)) throw new Error(i18n.t('~/.ssh/config not found'));
|
||||||
const entries = [];
|
const entries = [];
|
||||||
let cur = null;
|
let cur = null;
|
||||||
for (const raw of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
|
for (const raw of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
|
||||||
@@ -188,7 +191,7 @@ handle('key:pickFile', async () => {
|
|||||||
function hostWithOverrides(hostOrId) {
|
function hostWithOverrides(hostOrId) {
|
||||||
if (typeof hostOrId === 'string') {
|
if (typeof hostOrId === 'string') {
|
||||||
const h = store.resolveHost(hostOrId);
|
const h = store.resolveHost(hostOrId);
|
||||||
if (!h) throw new Error('Host nicht gefunden');
|
if (!h) throw new Error(i18n.t('Host not found'));
|
||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
return hostOrId; // Quick-Connect: temporärer Host
|
return hostOrId; // Quick-Connect: temporärer Host
|
||||||
@@ -245,7 +248,7 @@ handle('local:op', (op, a, b) => {
|
|||||||
// ---------- Port-Forwarding ----------
|
// ---------- Port-Forwarding ----------
|
||||||
handle('fw:start', (id) => {
|
handle('fw:start', (id) => {
|
||||||
const fw = store.get().forwards.find((f) => f.id === id);
|
const fw = store.get().forwards.find((f) => f.id === id);
|
||||||
if (!fw) throw new Error('Regel nicht gefunden');
|
if (!fw) throw new Error(i18n.t('Rule not found'));
|
||||||
return ssh.startForward(fw, (ev) => send('fw:event', id, ev));
|
return ssh.startForward(fw, (ev) => send('fw:event', id, ev));
|
||||||
});
|
});
|
||||||
handle('fw:stop', (id) => ssh.stopForward(id));
|
handle('fw:stop', (id) => ssh.stopForward(id));
|
||||||
@@ -258,8 +261,8 @@ handle('rdp:launch', (hostId) => {
|
|||||||
if (host.id) store.addHistory({ hostId: host.id, at: Date.now() });
|
if (host.id) store.addHistory({ hostId: host.id, at: Date.now() });
|
||||||
const r = rdp.launch(host, store.get().settings);
|
const r = rdp.launch(host, store.get().settings);
|
||||||
if (r.process) {
|
if (r.process) {
|
||||||
r.process.on('exit', (code) => { if (code && code !== 0 && code !== 12) send('toast', `RDP beendet (Code ${code}): ${r.getErr().split('\n').filter(Boolean).slice(-1)[0] || ''}`, 'error'); });
|
r.process.on('exit', (code) => { if (code && code !== 0 && code !== 12) send('toast', i18n.t('RDP exited (code {code}): {err}', { code, err: r.getErr().split('\n').filter(Boolean).slice(-1)[0] || '' }), 'error'); });
|
||||||
r.process.on('error', (e) => send('toast', 'RDP-Start fehlgeschlagen: ' + e.message, 'error'));
|
r.process.on('error', (e) => send('toast', i18n.t('RDP launch failed: {msg}', { msg: e.message }), 'error'));
|
||||||
}
|
}
|
||||||
return r.client;
|
return r.client;
|
||||||
});
|
});
|
||||||
@@ -268,7 +271,7 @@ handle('rdp:launch', (hostId) => {
|
|||||||
const rdpSessions = new Map();
|
const rdpSessions = new Map();
|
||||||
handle('rdp:embedSupported', () => {
|
handle('rdp:embedSupported', () => {
|
||||||
const s = embedSupported();
|
const s = embedSupported();
|
||||||
if (s.ok && process.platform === 'linux' && !rdp.linuxClient(null, true)) return { ok: false, reason: 'xfreerdp3 fehlt (sudo pacman -S freerdp)' };
|
if (s.ok && process.platform === 'linux' && !rdp.linuxClient(null, true)) return { ok: false, reason: i18n.t('xfreerdp3 missing (sudo pacman -S freerdp)') };
|
||||||
return s;
|
return s;
|
||||||
});
|
});
|
||||||
handle('rdp:open', async (id, hostRef, bounds) => {
|
handle('rdp:open', async (id, hostRef, bounds) => {
|
||||||
@@ -320,7 +323,7 @@ handle('rdp:detach', (id) => {
|
|||||||
|
|
||||||
// ---------- Import aus anderen Programmen (z. B. Devolutions RDM) ----------
|
// ---------- Import aus anderen Programmen (z. B. Devolutions RDM) ----------
|
||||||
handle('import:pickFile', async (title, extensions) => {
|
handle('import:pickFile', async (title, extensions) => {
|
||||||
const r = await dialog.showOpenDialog(win, { title, filters: [{ name: title, extensions }, { name: 'Alle Dateien', extensions: ['*'] }], properties: ['openFile'] });
|
const r = await dialog.showOpenDialog(win, { title, filters: [{ name: title, extensions }, { name: i18n.t('All files'), extensions: ['*'] }], properties: ['openFile'] });
|
||||||
if (r.canceled) return null;
|
if (r.canceled) return null;
|
||||||
const buf = fs.readFileSync(r.filePaths[0]);
|
const buf = fs.readFileSync(r.filePaths[0]);
|
||||||
// UTF-16-Exporte (BOM) erkennen
|
// UTF-16-Exporte (BOM) erkennen
|
||||||
@@ -364,6 +367,7 @@ handle('shell:open', (url) => shell.openExternal(url));
|
|||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
store.load();
|
store.load();
|
||||||
|
applyLanguage();
|
||||||
createWindow();
|
createWindow();
|
||||||
if (store.get().settings.updateAutoCheck && app.isPackaged) {
|
if (store.get().settings.updateAutoCheck && app.isPackaged) {
|
||||||
setTimeout(() => updater.check().then((r) => { if (r.available) send('update:available', r); }).catch(() => {}), 6000);
|
setTimeout(() => updater.check().then((r) => { if (r.available) send('update:available', r); }).catch(() => {}), 6000);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const { spawn } = require('child_process');
|
|||||||
|
|
||||||
let koffi = null;
|
let koffi = null;
|
||||||
try { koffi = require('koffi'); } catch { /* ohne koffi nur externe Fenster */ }
|
try { koffi = require('koffi'); } catch { /* ohne koffi nur externe Fenster */ }
|
||||||
|
const i18n = require('../i18n');
|
||||||
|
|
||||||
// ------------------------------------------------------------------ Windows
|
// ------------------------------------------------------------------ Windows
|
||||||
// Chromium zeichnet per DirectComposition über alle nativen Kindfenster – ein per SetParent eingehängtes
|
// Chromium zeichnet per DirectComposition über alle nativen Kindfenster – ein per SetParent eingehängtes
|
||||||
@@ -135,7 +136,7 @@ function x11() {
|
|||||||
XSync: l.func('int XSync(void *d, int discard)'),
|
XSync: l.func('int XSync(void *d, int discard)'),
|
||||||
};
|
};
|
||||||
x11.api.display = x11.api.XOpenDisplay(null);
|
x11.api.display = x11.api.XOpenDisplay(null);
|
||||||
if (!x11.api.display) throw new Error('Keine Verbindung zum X-Server (MrTerm läuft nicht unter X11/XWayland).');
|
if (!x11.api.display) throw new Error(i18n.t('No connection to the X server (MrTerm is not running under X11/XWayland).'));
|
||||||
return x11.api;
|
return x11.api;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,12 +196,12 @@ class LinuxEmbed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function embedSupported() {
|
function embedSupported() {
|
||||||
if (!koffi) return { ok: false, reason: 'koffi nicht verfügbar' };
|
if (!koffi) return { ok: false, reason: i18n.t('koffi not available') };
|
||||||
if (process.platform === 'win32') return { ok: true };
|
if (process.platform === 'win32') return { ok: true };
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
try { x11(); return { ok: true }; } catch (e) { return { ok: false, reason: e.message }; }
|
try { x11(); return { ok: true }; } catch (e) { return { ok: false, reason: e.message }; }
|
||||||
}
|
}
|
||||||
return { ok: false, reason: 'Plattform nicht unterstützt' };
|
return { ok: false, reason: i18n.t('Platform not supported') };
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEmbed(parentHandle) {
|
function createEmbed(parentHandle) {
|
||||||
|
|||||||
+2
-1
@@ -4,6 +4,7 @@ const { spawn, execFile, execFileSync } = require('child_process');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const i18n = require('../i18n');
|
||||||
|
|
||||||
function which(bin) {
|
function which(bin) {
|
||||||
try {
|
try {
|
||||||
@@ -100,7 +101,7 @@ function launch(host, settings) {
|
|||||||
return { client: 'mstsc' };
|
return { client: 'mstsc' };
|
||||||
}
|
}
|
||||||
const client = linuxClient(settings.rdpClientLinux);
|
const client = linuxClient(settings.rdpClientLinux);
|
||||||
if (!client) throw new Error('Kein FreeRDP gefunden. Installieren mit: sudo pacman -S freerdp');
|
if (!client) throw new Error(i18n.t('No FreeRDP found. Install with: sudo pacman -S freerdp'));
|
||||||
const p = spawn(client, freerdpArgs(host), { detached: true, stdio: [host.password ? 'pipe' : 'ignore', 'ignore', 'pipe'] });
|
const p = spawn(client, freerdpArgs(host), { detached: true, stdio: [host.password ? 'pipe' : 'ignore', 'ignore', 'pipe'] });
|
||||||
if (host.password) { p.stdin.write(host.password + '\n'); p.stdin.end(); }
|
if (host.password) { p.stdin.write(host.password + '\n'); p.stdin.end(); }
|
||||||
let err = '';
|
let err = '';
|
||||||
|
|||||||
+6
-5
@@ -5,6 +5,7 @@ const fs = require('fs');
|
|||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
const i18n = require('../i18n');
|
||||||
|
|
||||||
function defaultAgent() {
|
function defaultAgent() {
|
||||||
if (process.env.SSH_AUTH_SOCK) return process.env.SSH_AUTH_SOCK;
|
if (process.env.SSH_AUTH_SOCK) return process.env.SSH_AUTH_SOCK;
|
||||||
@@ -77,7 +78,7 @@ class SshManager {
|
|||||||
let sock;
|
let sock;
|
||||||
try {
|
try {
|
||||||
for (const hop of [...chain, host]) {
|
for (const hop of [...chain, host]) {
|
||||||
onStatus(`Verbinde mit ${hop.label || hop.address} (${hop.address}:${hop.port || 22}) …`);
|
onStatus(i18n.t('Connecting to {name} ({addr}) …', { name: hop.label || hop.address, addr: `${hop.address}:${hop.port || 22}` }));
|
||||||
const conn = await this.openClient(hop, sessionId, sock);
|
const conn = await this.openClient(hop, sessionId, sock);
|
||||||
opened.push(conn);
|
opened.push(conn);
|
||||||
if (hop !== host) {
|
if (hop !== host) {
|
||||||
@@ -104,7 +105,7 @@ class SshManager {
|
|||||||
const answers = [];
|
const answers = [];
|
||||||
for (const p of prompts) {
|
for (const p of prompts) {
|
||||||
if (!p.echo && host.password && !askedPassword) { answers.push(host.password); askedPassword = true; continue; }
|
if (!p.echo && host.password && !askedPassword) { answers.push(host.password); askedPassword = true; continue; }
|
||||||
const a = await this.askSecret(sessionId, { title: name || 'Anmeldung', prompt: p.prompt, echo: p.echo, host: host.label || host.address });
|
const a = await this.askSecret(sessionId, { title: name || i18n.t('Authentication'), prompt: p.prompt, echo: p.echo, host: host.label || host.address });
|
||||||
if (a == null) return finish([]);
|
if (a == null) return finish([]);
|
||||||
answers.push(a);
|
answers.push(a);
|
||||||
}
|
}
|
||||||
@@ -114,7 +115,7 @@ class SshManager {
|
|||||||
conn.once('error', async (err) => {
|
conn.once('error', async (err) => {
|
||||||
// Kein Passwort hinterlegt und alle Methoden schlugen fehl -> nach Passwort fragen und neu versuchen
|
// Kein Passwort hinterlegt und alle Methoden schlugen fehl -> nach Passwort fragen und neu versuchen
|
||||||
if (err.level === 'client-authentication' && !host.password && !host._retried) {
|
if (err.level === 'client-authentication' && !host.password && !host._retried) {
|
||||||
const pw = await this.askSecret(sessionId, { title: 'Passwort', prompt: `Passwort für ${cfg.username}@${host.address}`, echo: false, host: host.label || host.address });
|
const pw = await this.askSecret(sessionId, { title: i18n.t('Password'), prompt: i18n.t('Password for {user}', { user: `${cfg.username}@${host.address}` }), echo: false, host: host.label || host.address });
|
||||||
if (pw != null) {
|
if (pw != null) {
|
||||||
this.openClient({ ...host, password: pw, _retried: true }, sessionId, sock).then(resolve, reject);
|
this.openClient({ ...host, password: pw, _retried: true }, sessionId, sock).then(resolve, reject);
|
||||||
return;
|
return;
|
||||||
@@ -178,7 +179,7 @@ class SshManager {
|
|||||||
|
|
||||||
sftpOf(id) {
|
sftpOf(id) {
|
||||||
const s = this.sessions.get(id);
|
const s = this.sessions.get(id);
|
||||||
if (!s?.sftp) throw new Error('SFTP-Sitzung nicht gefunden');
|
if (!s?.sftp) throw new Error(i18n.t('SFTP session not found'));
|
||||||
return s.sftp;
|
return s.sftp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +251,7 @@ class SshManager {
|
|||||||
// ---------- Port-Forwarding ----------
|
// ---------- Port-Forwarding ----------
|
||||||
async startForward(fw, onEvent) {
|
async startForward(fw, onEvent) {
|
||||||
const host = this.store.resolveHost(fw.hostId);
|
const host = this.store.resolveHost(fw.hostId);
|
||||||
if (!host) throw new Error('Host nicht gefunden');
|
if (!host) throw new Error(i18n.t('Host not found'));
|
||||||
const { conn, jumps } = await this.connect(host, 'fw-' + fw.id);
|
const { conn, jumps } = await this.connect(host, 'fw-' + fw.id);
|
||||||
const entry = { conn, jumps };
|
const entry = { conn, jumps };
|
||||||
this.forwards.set(fw.id, entry);
|
this.forwards.set(fw.id, entry);
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const DEFAULTS = {
|
|||||||
updateToken: '',
|
updateToken: '',
|
||||||
updateAutoCheck: true,
|
updateAutoCheck: true,
|
||||||
updatePrerelease: false,
|
updatePrerelease: false,
|
||||||
|
language: 'auto',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+59
-11
@@ -5,13 +5,60 @@ const { spawn } = require('child_process');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const i18n = require('../i18n');
|
||||||
|
|
||||||
|
// Startet ein Programm und liefert den Exit-Code ('ENOENT', falls es nicht existiert).
|
||||||
|
// Electron/Chromium setzt unter Linux PR_SET_NO_NEW_PRIVS, das alle Kindprozesse erben – dann
|
||||||
|
// funktionieren weder pkexec noch sudo. Deshalb über systemd-run --user starten: Elternprozess ist
|
||||||
|
// dann der systemd-User-Manager, ohne diesen Schalter.
|
||||||
|
const GUI_ENV = ['DISPLAY', 'WAYLAND_DISPLAY', 'XAUTHORITY', 'XDG_RUNTIME_DIR', 'XDG_SESSION_TYPE', 'DBUS_SESSION_BUS_ADDRESS'];
|
||||||
|
function spawnCode(cmd, args) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const p = spawn(cmd, args, { stdio: 'ignore' });
|
||||||
|
p.on('error', (e) => resolve(e.code === 'ENOENT' ? 'ENOENT' : e.message));
|
||||||
|
p.on('exit', (c) => resolve(c));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let hasSystemdRun;
|
||||||
|
async function run(cmd, args) {
|
||||||
|
if (hasSystemdRun === undefined) hasSystemdRun = (await spawnCode('systemd-run', ['--user', '--quiet', '--wait', '--collect', 'true'])) === 0;
|
||||||
|
if (!hasSystemdRun) return spawnCode(cmd, args);
|
||||||
|
const inPath = cmd.includes('/') ? fs.existsSync(cmd) : (process.env.PATH || '').split(':').some((d) => d && fs.existsSync(path.join(d, cmd)));
|
||||||
|
if (!inPath) return 'ENOENT';
|
||||||
|
const env = GUI_ENV.filter((k) => process.env[k]).flatMap((k) => ['-E', `${k}=${process.env[k]}`]);
|
||||||
|
return spawnCode('systemd-run', ['--user', '--quiet', '--wait', '--collect', ...env, '--', cmd, ...args]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback ohne Polkit-Agent: pacman -U per sudo in einem Terminalfenster; wartet, bis das Fenster geschlossen ist.
|
||||||
|
// Der Erfolg wird über eine Marker-Datei erkannt, da nicht jedes Terminal den Exit-Code durchreicht.
|
||||||
|
async function installInTerminal(file) {
|
||||||
|
const q = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
|
||||||
|
const marker = file + '.ok';
|
||||||
|
try { fs.unlinkSync(marker); } catch {}
|
||||||
|
const script = `sudo /usr/bin/pacman -U --noconfirm ${q(file)} && touch ${q(marker)} || { echo; read -rp "[Enter]"; }`;
|
||||||
|
const terms = [
|
||||||
|
['konsole', ['--nofork', '-e', 'bash', '-c', script]],
|
||||||
|
['gnome-terminal', ['--wait', '--', 'bash', '-c', script]],
|
||||||
|
['kitty', ['bash', '-c', script]],
|
||||||
|
['alacritty', ['-e', 'bash', '-c', script]],
|
||||||
|
['foot', ['bash', '-c', script]],
|
||||||
|
['xfce4-terminal', ['--disable-server', '-x', 'bash', '-c', script]],
|
||||||
|
['xterm', ['-e', 'bash', '-c', script]],
|
||||||
|
];
|
||||||
|
for (const [cmd, args] of terms) {
|
||||||
|
const c = await run(cmd, args);
|
||||||
|
if (c === 'ENOENT') continue;
|
||||||
|
return fs.existsSync(marker) ? 0 : c || 1;
|
||||||
|
}
|
||||||
|
return 'ENOENT';
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_REPO ='https://git.mrblake.cc/MrBlake/MrTerm';
|
const DEFAULT_REPO ='https://git.mrblake.cc/MrBlake/MrTerm';
|
||||||
|
|
||||||
function parseRepo(url) {
|
function parseRepo(url) {
|
||||||
const u = new URL((url || DEFAULT_REPO).replace(/\.git$/, '').replace(/\/+$/, ''));
|
const u = new URL((url || DEFAULT_REPO).replace(/\.git$/, '').replace(/\/+$/, ''));
|
||||||
const [owner, repo] = u.pathname.split('/').filter(Boolean);
|
const [owner, repo] = u.pathname.split('/').filter(Boolean);
|
||||||
if (!owner || !repo) throw new Error('Ungültige Repository-URL: ' + url);
|
if (!owner || !repo) throw new Error(i18n.t('Invalid repository URL: {url}', { url }));
|
||||||
return { base: u.origin, owner, repo };
|
return { base: u.origin, owner, repo };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,8 +106,8 @@ class Updater {
|
|||||||
async check() {
|
async check() {
|
||||||
const { base, owner, repo } = parseRepo(this.store.get().settings.updateUrl);
|
const { base, owner, repo } = parseRepo(this.store.get().settings.updateUrl);
|
||||||
const res = await fetch(`${base}/api/v1/repos/${owner}/${repo}/releases?limit=20`, { headers: this.headers() });
|
const res = await fetch(`${base}/api/v1/repos/${owner}/${repo}/releases?limit=20`, { headers: this.headers() });
|
||||||
if (res.status === 404) throw new Error('Repository nicht gefunden (privat? Dann Zugriffstoken in den Einstellungen hinterlegen).');
|
if (res.status === 404) throw new Error(i18n.t('Repository not found (private? Then add an access token in the settings).'));
|
||||||
if (!res.ok) throw new Error(`Update-Server antwortet mit ${res.status}`);
|
if (!res.ok) throw new Error(i18n.t('Update server responded with {status}', { status: res.status }));
|
||||||
const allowPre = this.store.get().settings.updatePrerelease;
|
const allowPre = this.store.get().settings.updatePrerelease;
|
||||||
const rel = (await res.json()).find((r) => !r.draft && (allowPre || !r.prerelease));
|
const rel = (await res.json()).find((r) => !r.draft && (allowPre || !r.prerelease));
|
||||||
const current = app.getVersion();
|
const current = app.getVersion();
|
||||||
@@ -90,12 +137,12 @@ class Updater {
|
|||||||
async download() {
|
async download() {
|
||||||
if (!this.latest) await this.check();
|
if (!this.latest) await this.check();
|
||||||
const { asset } = this.latest;
|
const { asset } = this.latest;
|
||||||
if (!asset) throw new Error('Kein passendes Paket für dieses System im Release gefunden.');
|
if (!asset) throw new Error(i18n.t('No matching package for this system found in the release.'));
|
||||||
const dir = path.join(os.tmpdir(), 'mrterm-update');
|
const dir = path.join(os.tmpdir(), 'mrterm-update');
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
const file = path.join(dir, asset.name);
|
const file = path.join(dir, asset.name);
|
||||||
const res = await fetch(asset.browser_download_url, { headers: { ...this.headers(), Accept: 'application/octet-stream' } });
|
const res = await fetch(asset.browser_download_url, { headers: { ...this.headers(), Accept: 'application/octet-stream' } });
|
||||||
if (!res.ok) throw new Error(`Download fehlgeschlagen (${res.status})`);
|
if (!res.ok) throw new Error(i18n.t('Download failed ({status})', { status: res.status }));
|
||||||
const total = Number(res.headers.get('content-length')) || asset.size || 0;
|
const total = Number(res.headers.get('content-length')) || asset.size || 0;
|
||||||
const out = fs.createWriteStream(file + '.part');
|
const out = fs.createWriteStream(file + '.part');
|
||||||
let done = 0, last = 0;
|
let done = 0, last = 0;
|
||||||
@@ -133,15 +180,16 @@ class Updater {
|
|||||||
app.relaunch({ execPath: target });
|
app.relaunch({ execPath: target });
|
||||||
app.exit(0);
|
app.exit(0);
|
||||||
} else if (kind === 'pacman' && /\.(pacman|pkg\.tar\.zst)$/.test(file)) {
|
} else if (kind === 'pacman' && /\.(pacman|pkg\.tar\.zst)$/.test(file)) {
|
||||||
await new Promise((resolve, reject) => {
|
// 1. pkexec (grafische Passwortabfrage über den Polkit-Agent)
|
||||||
const p = spawn('pkexec', ['pacman', '-U', '--noconfirm', file], { stdio: 'ignore' });
|
let code = await run('pkexec', ['/usr/bin/pacman', '-U', '--noconfirm', file]);
|
||||||
p.on('error', reject);
|
// 126/127 = Abfrage abgebrochen bzw. kein Polkit-Agent → Terminal mit sudo öffnen
|
||||||
p.on('exit', (c) => (c === 0 ? resolve() : reject(new Error(`pacman beendet mit Code ${c}`))));
|
if (code === 126 || code === 127 || code === 'ENOENT') code = await installInTerminal(file);
|
||||||
});
|
if (code === 'ENOENT') throw new Error(i18n.t('Automatic installation is not possible here. The file is located at: {file}', { file }));
|
||||||
|
if (code !== 0) throw new Error(i18n.t('pacman exited with code {code}', { code }));
|
||||||
app.relaunch();
|
app.relaunch();
|
||||||
app.exit(0);
|
app.exit(0);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Automatische Installation hier nicht möglich. Datei liegt unter: ' + file);
|
throw new Error(i18n.t('Automatic installation is not possible here. The file is located at: {file}', { file }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+262
-242
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:" />
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:" />
|
||||||
@@ -16,18 +16,18 @@
|
|||||||
<svg viewBox="0 0 24 24"><path d="M4 5h16v14H4z M4 9h16" /></svg><span>Vault</span>
|
<svg viewBox="0 0 24 24"><path d="M4 5h16v14H4z M4 9h16" /></svg><span>Vault</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button id="newTabBtn" class="icon-btn" title="Quick Connect (Strg+Shift+K)">+</button>
|
<button id="newTabBtn" class="icon-btn" title="Quick Connect (Ctrl+Shift+K)" data-i18n-title="Quick Connect (Ctrl+Shift+K)">+</button>
|
||||||
<div class="drag-fill"></div>
|
<div class="drag-fill"></div>
|
||||||
<div class="win-ctrls">
|
<div class="win-ctrls">
|
||||||
<button id="winMin" title="Minimieren"><svg viewBox="0 0 12 12"><path d="M2 6h8"/></svg></button>
|
<button id="winMin" title="Minimize" data-i18n-title="Minimize"><svg viewBox="0 0 12 12"><path d="M2 6h8"/></svg></button>
|
||||||
<button id="winMax" title="Maximieren"><svg viewBox="0 0 12 12"><rect x="2" y="2" width="8" height="8"/></svg></button>
|
<button id="winMax" title="Maximize" data-i18n-title="Maximize"><svg viewBox="0 0 12 12"><rect x="2" y="2" width="8" height="8"/></svg></button>
|
||||||
<button id="winClose" class="close" title="Schließen"><svg viewBox="0 0 12 12"><path d="M2 2l8 8M10 2l-8 8"/></svg></button>
|
<button id="winClose" class="close" title="Close" data-i18n-title="Close"><svg viewBox="0 0 12 12"><path d="M2 2l8 8M10 2l-8 8"/></svg></button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="body">
|
<div id="body">
|
||||||
<nav id="sidebar">
|
<nav id="sidebar">
|
||||||
<div id="navHead" class="tree-head nav-head"><span>Menü</span><button id="navToggle" title="Menü einklappen"><svg viewBox="0 0 24 24"><path d="M6 15l6-6 6 6"/></svg></button></div>
|
<div id="navHead" class="tree-head nav-head"><span data-i18n="Menu">Menu</span><button id="navToggle" title="Collapse menu"><svg viewBox="0 0 24 24"><path d="M6 15l6-6 6 6"/></svg></button></div>
|
||||||
<div id="navMenu" class="nav-menu">
|
<div id="navMenu" class="nav-menu">
|
||||||
<button class="nav active" data-view="hosts"><svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><circle cx="7" cy="7.5" r="1"/><circle cx="7" cy="16.5" r="1"/></svg><span>Hosts</span></button>
|
<button class="nav active" data-view="hosts"><svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><circle cx="7" cy="7.5" r="1"/><circle cx="7" cy="16.5" r="1"/></svg><span>Hosts</span></button>
|
||||||
<button class="nav" data-view="sftp"><svg viewBox="0 0 24 24"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg><span>SFTP</span></button>
|
<button class="nav" data-view="sftp"><svg viewBox="0 0 24 24"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg><span>SFTP</span></button>
|
||||||
@@ -35,13 +35,13 @@
|
|||||||
<button class="nav" data-view="forwards"><svg viewBox="0 0 24 24"><path d="M4 8h13l-3-3M20 16H7l3 3"/></svg><span>Port Forwarding</span></button>
|
<button class="nav" data-view="forwards"><svg viewBox="0 0 24 24"><path d="M4 8h13l-3-3M20 16H7l3 3"/></svg><span>Port Forwarding</span></button>
|
||||||
<button class="nav" data-view="snippets"><svg viewBox="0 0 24 24"><path d="M8 7l-5 5 5 5M16 7l5 5-5 5"/></svg><span>Snippets</span></button>
|
<button class="nav" data-view="snippets"><svg viewBox="0 0 24 24"><path d="M8 7l-5 5 5 5M16 7l5 5-5 5"/></svg><span>Snippets</span></button>
|
||||||
<button class="nav" data-view="known"><svg viewBox="0 0 24 24"><path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z"/><path d="M9 12l2 2 4-4"/></svg><span>Known Hosts</span></button>
|
<button class="nav" data-view="known"><svg viewBox="0 0 24 24"><path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z"/><path d="M9 12l2 2 4-4"/></svg><span>Known Hosts</span></button>
|
||||||
<button class="nav" data-view="history"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg><span>Verlauf</span></button>
|
<button class="nav" data-view="history"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg><span data-i18n="History">History</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-sep"></div>
|
<div class="nav-sep"></div>
|
||||||
<div id="tree" class="tree"></div>
|
<div id="tree" class="tree"></div>
|
||||||
<button id="updateBadge" class="nav update-badge" style="display:none"><svg viewBox="0 0 24 24"><path d="M12 4v12M6 10l6 6 6-6M4 20h16"/></svg><span>Update</span></button>
|
<button id="updateBadge" class="nav update-badge" style="display:none"><svg viewBox="0 0 24 24"><path d="M12 4v12M6 10l6 6 6-6M4 20h16"/></svg><span>Update</span></button>
|
||||||
<div id="vaultBadge" class="vault-badge"></div>
|
<div id="vaultBadge" class="vault-badge"></div>
|
||||||
<button class="nav" data-view="settings"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg><span>Einstellungen</span></button>
|
<button class="nav" data-view="settings"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg><span data-i18n="Settings">Settings</span></button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main id="main">
|
<main id="main">
|
||||||
@@ -60,6 +60,7 @@
|
|||||||
<script src="../../node_modules/@xterm/addon-fit/lib/addon-fit.js"></script>
|
<script src="../../node_modules/@xterm/addon-fit/lib/addon-fit.js"></script>
|
||||||
<script src="../../node_modules/@xterm/addon-web-links/lib/addon-web-links.js"></script>
|
<script src="../../node_modules/@xterm/addon-web-links/lib/addon-web-links.js"></script>
|
||||||
<script src="../../node_modules/@xterm/addon-search/lib/addon-search.js"></script>
|
<script src="../../node_modules/@xterm/addon-search/lib/addon-search.js"></script>
|
||||||
|
<script src="../i18n.js"></script>
|
||||||
<script src="themes.js"></script>
|
<script src="themes.js"></script>
|
||||||
<script src="rdm-import.js"></script>
|
<script src="rdm-import.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
|
|
||||||
function parseXml(text) {
|
function parseXml(text) {
|
||||||
const doc = new DOMParser().parseFromString(text, 'application/xml');
|
const doc = new DOMParser().parseFromString(text, 'application/xml');
|
||||||
if (doc.querySelector('parsererror')) throw new Error('Datei ist kein gültiges XML.');
|
if (doc.querySelector('parsererror')) throw new Error(I18N.t('File is not valid XML.'));
|
||||||
const conns = [...doc.getElementsByTagName('Connection')].filter((c) => child(c, 'ConnectionType'));
|
const conns = [...doc.getElementsByTagName('Connection')].filter((c) => child(c, 'ConnectionType'));
|
||||||
return conns.map((c) => makeEntry({
|
return conns.map((c) => makeEntry({
|
||||||
type: child(c, 'ConnectionType'),
|
type: child(c, 'ConnectionType'),
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
host: idx('host', 'hostname', 'url', 'computername', 'address'), port: idx('port', 'hostport'),
|
host: idx('host', 'hostname', 'url', 'computername', 'address'), port: idx('port', 'hostport'),
|
||||||
username: idx('username', 'user', 'login'), domain: idx('domain'), password: idx('password'), description: idx('description'),
|
username: idx('username', 'user', 'login'), domain: idx('domain'), password: idx('password'), description: idx('description'),
|
||||||
};
|
};
|
||||||
if (col.host < 0 && col.type < 0) throw new Error('CSV-Spalten nicht erkannt (erwartet u. a. Name, Host, ConnectionType, Group).');
|
if (col.host < 0 && col.type < 0) throw new Error(I18N.t('CSV columns not recognized (expected e.g. Name, Host, ConnectionType, Group).'));
|
||||||
return rows.map((r) => {
|
return rows.map((r) => {
|
||||||
const v = (k) => (col[k] >= 0 ? (r[col[k]] || '').trim() : '');
|
const v = (k) => (col[k] >= 0 ? (r[col[k]] || '').trim() : '');
|
||||||
return makeEntry({ type: v('type') || 'RDPConfigured', name: v('name'), group: v('group'), url: v('host'), port: v('port'), username: v('username'), domain: v('domain'), password: v('password'), description: v('description') });
|
return makeEntry({ type: v('type') || 'RDPConfigured', name: v('name'), group: v('group'), url: v('host'), port: v('port'), username: v('username'), domain: v('domain'), password: v('password'), description: v('description') });
|
||||||
|
|||||||
@@ -235,8 +235,10 @@ body.in-session #sidebar { display: none; }
|
|||||||
.session .sbar { height: 38px; display: flex; align-items: center; gap: 6px; padding: 0 10px; background: var(--bg-2); border-bottom: 1px solid var(--border); flex: none; }
|
.session .sbar { height: 38px; display: flex; align-items: center; gap: 6px; padding: 0 10px; background: var(--bg-2); border-bottom: 1px solid var(--border); flex: none; }
|
||||||
.session .sbar .info { color: var(--muted); font-size: 12px; margin-right: auto; display: flex; gap: 8px; align-items: center; overflow: hidden; white-space: nowrap; }
|
.session .sbar .info { color: var(--muted); font-size: 12px; margin-right: auto; display: flex; gap: 8px; align-items: center; overflow: hidden; white-space: nowrap; }
|
||||||
.session .term-wrap { flex: 1; position: relative; min-height: 0; display: flex; }
|
.session .term-wrap { flex: 1; position: relative; min-height: 0; display: flex; }
|
||||||
.session .term { flex: 1; padding: 8px 0 4px 10px; min-width: 0; }
|
.session .term { flex: 1; min-width: 0; min-height: 0; overflow: hidden; }
|
||||||
.session .term .xterm { height: 100%; }
|
/* Innenabstand am .xterm-Element: FitAddon zieht nur dessen Padding ab (am Elternelement würde es mitgezählt) */
|
||||||
|
.session .term .xterm { height: 100%; padding: 8px 0 4px 10px; }
|
||||||
|
.session .term .xterm-viewport { background-color: transparent !important; } /* sonst schwarzer Rand im Innenabstand */
|
||||||
.session .overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 16px; background: var(--overlay); z-index: 3; }
|
.session .overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 16px; background: var(--overlay); z-index: 3; }
|
||||||
.session .overlay .spinner { width: 36px; height: 36px; border: 3px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.9s linear infinite; }
|
.session .overlay .spinner { width: 36px; height: 36px; border: 3px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.9s linear infinite; }
|
||||||
.session .overlay .msg { color: var(--muted); max-width: 520px; text-align: center; }
|
.session .overlay .msg { color: var(--muted); max-width: 520px; text-align: center; }
|
||||||
|
|||||||
@@ -37,5 +37,5 @@ window.APP_THEMES = {
|
|||||||
dracula: { name: 'Dracula', bg: '#282a36', side: '#21222c', card: '#343746', accent: '#bd93f9', text: '#f8f8f2', term: 'dracula' },
|
dracula: { name: 'Dracula', bg: '#282a36', side: '#21222c', card: '#343746', accent: '#bd93f9', text: '#f8f8f2', term: 'dracula' },
|
||||||
mocha: { name: 'Catppuccin', bg: '#1e1e2e', side: '#181825', card: '#2a2a3d', accent: '#cba6f7', text: '#cdd6f4', term: 'mocha' },
|
mocha: { name: 'Catppuccin', bg: '#1e1e2e', side: '#181825', card: '#2a2a3d', accent: '#cba6f7', text: '#cdd6f4', term: 'mocha' },
|
||||||
forest: { name: 'Forest', bg: '#141a17', side: '#0f1411', card: '#1f2a24', accent: '#4cc38a', text: '#e3ece6', term: 'forest' },
|
forest: { name: 'Forest', bg: '#141a17', side: '#0f1411', card: '#1f2a24', accent: '#4cc38a', text: '#e3ece6', term: 'forest' },
|
||||||
light: { name: 'Hell', bg: '#f6f7fb', side: '#e9ecf4', card: '#ffffff', accent: '#5b67f1', text: '#1f2330', term: 'light' },
|
light: { name: 'Light', bg: '#f6f7fb', side: '#e9ecf4', card: '#ffffff', accent: '#5b67f1', text: '#1f2330', term: 'light' },
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user