From c657ec282541166184910a9a59a47caf90f35cfc Mon Sep 17 00:00:00 2001 From: x3 Date: Tue, 22 Sep 2026 10:19:25 +0200 Subject: [PATCH] Full interactive terminal via SwiftTerm, About sheet, English docs --- Package.resolved | 23 ++ Package.swift | 4 + README.md | 130 +++--- Sources/RemminaMac/Services/PTYSession.swift | 51 ++- .../RemminaMac/Services/TerminalTheme.swift | 54 +-- Sources/RemminaMac/Views/AboutView.swift | 45 +++ Sources/RemminaMac/Views/ContentView.swift | 6 + .../Views/EmbeddedTerminalView.swift | 113 +----- Sources/RemminaMac/Views/SwiftTermView.swift | 65 +++ .../RemminaMac/Views/TerminalInputField.swift | 119 ------ docs/ARCHITECTURE.md | 80 ++-- docs/ERRORS.md | 375 +++++++----------- docs/INSTALL.md | 51 ++- docs/RELEASING.md | 52 +-- docs/USAGE.md | 66 +-- 15 files changed, 584 insertions(+), 650 deletions(-) create mode 100644 Package.resolved create mode 100644 Sources/RemminaMac/Views/AboutView.swift create mode 100644 Sources/RemminaMac/Views/SwiftTermView.swift delete mode 100644 Sources/RemminaMac/Views/TerminalInputField.swift diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..07e4881 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,23 @@ +{ + "pins" : [ + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "6a52f3251125d74daf04fcbd5e6f08a75d074382", + "version" : "1.8.2" + } + }, + { + "identity" : "swiftterm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/migueldeicaza/SwiftTerm", + "state" : { + "revision" : "5d14406844143538cd8f8851d2d8a67c1fe443e5", + "version" : "1.20.0" + } + } + ], + "version" : 2 +} diff --git a/Package.swift b/Package.swift index 65ef8b0..b86e86b 100644 --- a/Package.swift +++ b/Package.swift @@ -10,9 +10,13 @@ let package = Package( .executable(name: "RemminaMac", targets: ["RemminaMac"]), .executable(name: "ctty-run", targets: ["ctty-run"]) ], + dependencies: [ + .package(url: "https://github.com/migueldeicaza/SwiftTerm", from: "1.20.0") + ], targets: [ .executableTarget( name: "RemminaMac", + dependencies: ["SwiftTerm"], path: "Sources/RemminaMac" ), .executableTarget( diff --git a/README.md b/README.md index eaca16e..67a3d68 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,69 @@ -# RemminaMac — SSH / RDP Client für macOS +# RemminaMac — SSH / RDP Client for macOS ![RemminaMac Icon](docs/icon-preview.png) -Remmina-inspirierter Connection-Manager für macOS, nativ in Swift + SwiftUI. +Remmina-inspired connection manager for macOS, native Swift + SwiftUI. -![Plattform](https://img.shields.io/badge/plattform-macOS%2014%2B-blue) -![Sprache](https://img.shields.io/badge/sprache-Swift%205.9-orange) +![Platform](https://img.shields.io/badge/platform-macOS%2014%2B-blue) +![Language](https://img.shields.io/badge/language-Swift%205.9-orange) ![UI](https://img.shields.io/badge/UI-SwiftUI-green) -Layout wie Remmina: **links Profil-Liste mit Suche + Gruppen, rechts Verbindungs-Tabs, unten Statusbar.** +Remmina-style layout: **profile list with search + groups on the left, connection tabs on the right, status bar at the bottom.** -## Inhaltsverzeichnis +## Contents - [Features](#features) -- [Screenshots / Layout](#screenshots--layout) -- [Schnellstart](#schnellstart) +- [Layout](#layout) +- [Quick start](#quick-start) - [Installation](docs/INSTALL.md) -- [Benutzung](docs/USAGE.md) -- [Architektur](docs/ARCHITECTURE.md) -- [SSH-Keys & Sicherheit](#ssh-keys--sicherheit) -- [RDP / VNC unter macOS](#rdp--vnc-unter-macos) -- [Profile-Format](#profile-format) -- [Release-Binary](#release-binary) -- [Fehlerprotokoll](docs/ERRORS.md) +- [Usage](docs/USAGE.md) +- [Architecture](docs/ARCHITECTURE.md) +- [SSH keys & security](#ssh-keys--security) +- [RDP / VNC on macOS](#rdp--vnc-on-macos) +- [Profile format](#profile-format) +- [Release binaries](#release-binaries) +- [Error log](docs/ERRORS.md) - [Roadmap](#roadmap) -- [Lizenz](#lizenz) +- [License](#license) ## Features -- GUI im Remmina-Layout (`Views/ContentView.swift`): - - Sidebar: Gruppen-Picker, Suche, Profil-Liste, Doppelklick = Verbinden - - Detail: Tab-Ansicht offener Sitzungen, Verbindungsdetails, Statusbar - - Toolbar: Neu / Bearbeiten / Duplizieren / Verbinden / Quick-Connect / SSH-Keys -- Profile laden / speichern / duplizieren / löschen +- GUI in Remmina layout (`Views/ContentView.swift`): + - Sidebar: group picker, search, profile list, double-click = connect + - Detail: tabbed open sessions, connection details, status bar + - Toolbar: New / Edit / Duplicate / Connect / Quick-Connect / SSH keys / Info +- Load / save / duplicate / delete profiles - JSON in `~/Library/Application Support/RemminaMac/profiles.json` - - Export / Import (siehe Benutzung) -- Protokolle: SSH, SFTP, RDP, VNC (`Models/ConnectionProfile.swift:3`) -- SSH-Keys speichern & verwalten (`Views/SSHKeysView.swift`): - - Import aus `~/.ssh`, Datei-Picker, `ssh-keygen`-Wrapper (ed25519) - - Passphrases in macOS Keychain (`Security/KeychainHelper.swift`) -- Passwörter in Keychain, nie im Klartext-JSON -- SSH via System-`ssh` in Terminal.app (`Services/ConnectionServices.swift:6`) -- RDP via FreeRDP (`xfreerdp`) oder Fallback Microsoft Remote Desktop -- VNC via `vnc://`-Scheme + - Validation + trimming in the editor, copyable error reports +- Protocols: SSH, SFTP, RDP, VNC (`Models/ConnectionProfile.swift`) +- Full interactive terminal per tab (SwiftTerm emulator, Dracula themed): + cursor addressing, colors, scrollback, Tab completion, arrows, Ctrl+C, + full-screen apps (vim/htop). Real PTY + controlling terminal via `ctty-run`. +- Host-key fingerprint popup (accept/decline, MITM warning on change), + shared `~/.ssh/known_hosts` with Terminal.app +- SSH key management (`Views/SSHKeysView.swift`): + import from `~/.ssh`, file picker, `ssh-keygen` wrapper (ed25519) +- Passwords/passphrases in macOS Keychain only, never in JSON +- Auto-login via `sshpass` (if installed) or interactive prompt +- TCP pre-check with plain-language diagnostics before connecting +- RDP via FreeRDP (`xfreerdp`) or Microsoft Remote Desktop fallback +- VNC via `vnc://` scheme +- About sheet with contributor + repository link -## Screenshots / Layout +## Layout ``` +-------------------------------------------------------------+ -| Toolbar: [+][Edit][Copy][Key] | [▶ Verbinden][⚡Quick] | +| Toolbar: [+][Edit][Copy][Key][i] | [▶ Connect][⚡Quick] | +------+-------------------------------+--------------------+ -| Gruppe | Tab: Webserver | Büro-PC | ... | Detail | -| Suche +-------------------------------+ | -| - Webserver | Verbindungsdetails (Befehl)| | -| - Büro-PC | Hinweis | | -| - Dateiserver+-------------------------------+ | +| Group | Tab: Webserver | Office-PC | ... | Detail | +| Search +-------------------------------+ | +| - Webserver | Full terminal (SwiftTerm) | | +| - Office-PC | Connection details (cmd) | | +| - Fileserver +-------------------------------+ | +------+-------------------------------+--------------------+ -| Statusbar: ● Bereit ... N Profile · M Sitzungen | +| Statusbar: ● Ready ... N profiles · M sessions | +-------------------------------------------------------------+ ``` -## Schnellstart +## Quick start ```bash cd RemminaMac swift build @@ -67,19 +73,21 @@ open Package.swift # Target RemminaMac → My Mac → Run ``` -## SSH-Keys & Sicherheit -- Private-Key-Pfade stehen in `sshkeys.json`, Passphrases nur in Keychain. -- Service-Name: `RemminaMac`, Accounts: `profile-`, `sshkey-`. -- Keychain lesen/schreiben: `Security/KeychainHelper.swift:5`. -- Es werden keine Secrets ins Git-Repo geschrieben (siehe `.gitignore`). +## SSH keys & security +- Private key paths live in `sshkeys.json`, passphrases in Keychain only. +- Service name: `RemminaMac`, accounts: `profile-`, `sshkey-`. +- Keychain access: `Security/KeychainHelper.swift`. +- No secrets are committed to the repo (see `.gitignore`). +- Passwords reach ssh via `SSHPASS` env (with sshpass) or the interactive + terminal prompt — never via command line or logs. -## RDP / VNC unter macOS +## RDP / VNC on macOS - Optional: `brew install freerdp` -- Mit FreeRDP: `xfreerdp /v:HOST:PORT /u:USER [/size:WxH | /f]` -- Ohne: `rdc://` → Microsoft Remote Desktop (aus App Store installieren) +- With FreeRDP: `xfreerdp /v:HOST:PORT /u:USER [/size:WxH | /f]` +- Without: `rdc://` → Microsoft Remote Desktop (install from App Store) - VNC: `open vnc://host:port` -## Profile-Format +## Profile format ```json [ { @@ -91,7 +99,7 @@ open Package.swift "username": "admin", "group": "Server", "colorTag": "blue", - "resolution": "Automatisch", + "resolution": "Automatic", "sshKeyID": null, "passwordAccount": "profile-UUID", "extraArgs": "", @@ -100,12 +108,12 @@ open Package.swift ] ``` -## Release-Binary -Fertige Binaries liegen unter Git-Releases (`dist/` wird lokal gebaut, nicht committet): +## Release binaries +Prebuilt binaries on Git releases (`dist/` is built locally, never committed): ```bash -./scripts/build-release.sh -./scripts/build-app.sh # RemminaMac.app für /Applications -./scripts/build-dmg.sh # DMG mit Drag-and-drop auf Programme +./scripts/build-release.sh 0.7.0 +./scripts/build-app.sh 0.7.0 # RemminaMac.app for /Applications +./scripts/build-dmg.sh 0.7.0 # DMG with drag-and-drop to Applications # → dist/RemminaMac--macos-arm64.zip + .sha256 # → dist/RemminaMac--macos-arm64.dmg + .sha256 ls dist/ @@ -113,11 +121,11 @@ ls dist/ Details: [docs/INSTALL.md](docs/INSTALL.md), [docs/RELEASING.md](docs/RELEASING.md). ## Roadmap -- [ ] Eingebettetes Terminal (SwiftTerm + swift-nio-ssh) -- [ ] Echte RDP-Einbettung (FreeRDP-Bridge) statt externem Prozess -- [ ] SFTP-Browser integriert -- [ ] Export/Import-Buttons in der GUI -- [ ] Tray / Favoriten / Ordner-Badges wie Remmina +- [x] Embedded full terminal (SwiftTerm) — done in 0.7.0 +- [ ] Embedded RDP (FreeRDP bridge) instead of external process +- [ ] Integrated SFTP browser +- [ ] Export/Import buttons in the GUI +- [ ] Tray / favorites / folder badges like Remmina -## Lizenz -MIT — siehe `LICENSE`. +## License +MIT — see `LICENSE`. diff --git a/Sources/RemminaMac/Services/PTYSession.swift b/Sources/RemminaMac/Services/PTYSession.swift index 1fe22b7..8971925 100644 --- a/Sources/RemminaMac/Services/PTYSession.swift +++ b/Sources/RemminaMac/Services/PTYSession.swift @@ -33,6 +33,9 @@ final class PTYSession: ObservableObject { private var readSource: DispatchSourceRead? private var errPipe: Pipe? private let ioQueue = DispatchQueue(label: "RemminaMac.pty") + /// Raw output hook for the terminal emulator (SwiftTerm feeds on this). + /// Called on ioQueue for every chunk from pty master and stderr pipe. + var onData: ((Data) -> Void)? private var parser = AnsiParser() private var finished = false private var epoch = 0 // nur Main-Thread @@ -132,6 +135,7 @@ final class PTYSession: ObservableObject { h.readabilityHandler = nil // EOF return } + self.onData?(data) self.ioQueue.async { [weak self] in guard let self else { return } let events = self.parser.feed(data) @@ -152,7 +156,9 @@ final class PTYSession: ObservableObject { var buf = [UInt8](repeating: 0, count: 8192) let n = read(fd, &buf, buf.count) if n > 0 { - let events = parser.feed(Data(buf[..= 0 else { return } - guard let data = string.data(using: .utf8), !data.isEmpty else { return } + guard fd >= 0, !data.isEmpty else { return } ioQueue.async { data.withUnsafeBytes { ptr in guard let base = ptr.baseAddress else { return } @@ -233,6 +244,16 @@ final class PTYSession: ObservableObject { } } + /// Terminal size to the pty (TIOCSWINSZ) so vim etc. use the full area. + func setWinsize(cols: Int, rows: Int) { + let fd = masterFD + guard fd >= 0, cols > 0, rows > 0 else { return } + var ws = winsize(ws_row: UInt16(min(rows, 65535)), + ws_col: UInt16(min(cols, 65535)), + ws_xpixel: 0, ws_ypixel: 0) + _ = ioctl(fd, TIOCSWINSZ, &ws) + } + func clear() { lines = [[]] } @@ -324,27 +345,35 @@ final class TerminalManager: ObservableObject { private var sessions: [UUID: PTYSession] = [:] func session(for profile: ConnectionProfile, key: StoredSSHKey?) -> PTYSession { - // Fingerprint aus allem, was die Verbindung bestimmt (inkl. Secret-Wert!): - // Ändert der User Host/Passwort/Key, wird neu konfiguriert statt stale weiterzuwählen. + // Fingerprint aus allem, was die Verbindung bestimmt (inkl. Secret-Wert + // und sshpass-Verfügbarkeit): Ändert der User Host/Passwort/Key oder wird + // sshpass (nach-)installiert, wird neu konfiguriert statt stale weiterzuwählen. let secret = SSHService.loginSecret(for: profile, key: key) - let tag = [profile.proto.rawValue, profile.host, "\(profile.port)", profile.username, - profile.extraArgs, key?.privateKeyPath ?? "", secret ?? ""].joined(separator: "|") + let sshpass = SSHService.findSshpass() + let tag = Self.configTag(profile: profile, keyPath: key?.privateKeyPath, + secret: secret, sshpass: sshpass) if let existing = sessions[profile.id] { // Laufende Session niemals unter den Füßen wegkonfigurieren — erst Trennen. if existing.isRunning || existing.configTag == tag { return existing } existing.stop() - configure(existing, profile: profile, key: key, secret: secret) + configure(existing, profile: profile, key: key, secret: secret, sshpass: sshpass) existing.configTag = tag return existing } let s = PTYSession() - configure(s, profile: profile, key: key, secret: secret) + configure(s, profile: profile, key: key, secret: secret, sshpass: sshpass) s.configTag = tag sessions[profile.id] = s return s } - private func configure(_ s: PTYSession, profile: ConnectionProfile, key: StoredSSHKey?, secret: String?) { + /// Pure tag builder (testable): any change here must reconfigure. + nonisolated static func configTag(profile: ConnectionProfile, keyPath: String?, secret: String?, sshpass: String?) -> String { + [profile.proto.rawValue, profile.host, "\(profile.port)", profile.username, + profile.extraArgs, keyPath ?? "", secret ?? "", sshpass ?? ""].joined(separator: "|") + } + + private func configure(_ s: PTYSession, profile: ConnectionProfile, key: StoredSSHKey?, secret: String?, sshpass: String?) { let baseExe: String let baseArgs: [String] if profile.proto == .sftp { @@ -358,7 +387,7 @@ final class TerminalManager: ObservableObject { // SSH-Agent durchreichen (falls vorhanden): gleiche Keys wie in Terminal.app. let cfg = SSHService.sessionConfig(executable: baseExe, args: baseArgs, password: secret, - sshpassPath: SSHService.findSshpass()) + sshpassPath: sshpass) s.configure(executable: cfg.executable, arguments: cfg.arguments) var env = cfg.extraEnv if let sock = SSHService.agentSocket() { diff --git a/Sources/RemminaMac/Services/TerminalTheme.swift b/Sources/RemminaMac/Services/TerminalTheme.swift index bcf5df8..398b4fc 100644 --- a/Sources/RemminaMac/Services/TerminalTheme.swift +++ b/Sources/RemminaMac/Services/TerminalTheme.swift @@ -1,25 +1,26 @@ import SwiftUI import AppKit +import SwiftTerm /// Offizielle Dracula-Palette (https://draculatheme.com/contribute). enum Dracula { - static let background = Color(hex: "282A36") - static let currentLine = Color(hex: "44475A") - static let foreground = Color(hex: "F8F8F2") - static let comment = Color(hex: "6272A4") - static let cyan = Color(hex: "8BE9FD") - static let green = Color(hex: "50FA7B") - static let orange = Color(hex: "FFB86C") - static let pink = Color(hex: "FF79C6") - static let purple = Color(hex: "BD93F9") - static let red = Color(hex: "FF5555") - static let yellow = Color(hex: "F1FA8C") + static let background = SwiftUI.Color(hex: "282A36") + static let currentLine = SwiftUI.Color(hex: "44475A") + static let foreground = SwiftUI.Color(hex: "F8F8F2") + static let comment = SwiftUI.Color(hex: "6272A4") + static let cyan = SwiftUI.Color(hex: "8BE9FD") + static let green = SwiftUI.Color(hex: "50FA7B") + static let orange = SwiftUI.Color(hex: "FFB86C") + static let pink = SwiftUI.Color(hex: "FF79C6") + static let purple = SwiftUI.Color(hex: "BD93F9") + static let red = SwiftUI.Color(hex: "FF5555") + static let yellow = SwiftUI.Color(hex: "F1FA8C") /// ANSI-Index 0-15 -> Dracula-Farbe. - static func color(for index: Int?) -> Color { + static func color(for index: Int?) -> SwiftUI.Color { guard let index else { return foreground } switch index { - case 0: return Color(hex: "21222C") + case 0: return SwiftUI.Color(hex: "21222C") case 1: return red case 2: return green case 3: return yellow @@ -28,13 +29,13 @@ enum Dracula { case 6: return cyan case 7: return foreground case 8: return comment - case 9: return Color(hex: "FF6E6E") - case 10: return Color(hex: "69FF94") - case 11: return Color(hex: "FFFFA5") - case 12: return Color(hex: "D6ACFF") - case 13: return Color(hex: "FF92DF") - case 14: return Color(hex: "A4FFFF") - default: return Color(hex: "FFFFFF") + case 9: return SwiftUI.Color(hex: "FF6E6E") + case 10: return SwiftUI.Color(hex: "69FF94") + case 11: return SwiftUI.Color(hex: "FFFFA5") + case 12: return SwiftUI.Color(hex: "D6ACFF") + case 13: return SwiftUI.Color(hex: "FF92DF") + case 14: return SwiftUI.Color(hex: "A4FFFF") + default: return SwiftUI.Color(hex: "FFFFFF") } } @@ -42,11 +43,18 @@ enum Dracula { NSColor(color(for: index)) } - static let nsBackground = NSColor(Color(hex: "282A36")) - static let nsSelection = NSColor(Color(hex: "44475A")) + static let nsBackground = NSColor(SwiftUI.Color(hex: "282A36")) + static let nsSelection = NSColor(SwiftUI.Color(hex: "44475A")) + + /// 16 ANSI colors for the terminal emulator (SwiftTerm palette). + static var ansiPalette: [SwiftTerm.Color] { + ["21222C", "FF5555", "50FA7B", "F1FA8C", "BD93F9", "FF79C6", "8BE9FD", "F8F8F2", + "6272A4", "FF6E6E", "69FF94", "FFFFA5", "D6ACFF", "FF92DF", "A4FFFF", "FFFFFF"] + .compactMap { SwiftTerm.Color.parse("#\($0)") } + } } -extension Color { +extension SwiftUI.Color { init(hex: String) { let clean = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var value: UInt64 = 0 diff --git a/Sources/RemminaMac/Views/AboutView.swift b/Sources/RemminaMac/Views/AboutView.swift new file mode 100644 index 0000000..c37670d --- /dev/null +++ b/Sources/RemminaMac/Views/AboutView.swift @@ -0,0 +1,45 @@ +import SwiftUI + +enum AppInfo { + static let version = "0.7.0" + static let repoURL = URL(string: "https://git.sysdaemon.xyz/x3/remmina-mac")! +} + +/// About sheet: contributor, repository link, license, credits. +struct AboutView: View { + var body: some View { + VStack(spacing: 14) { + Image(systemName: "terminal.fill") + .font(.system(size: 56)) + .foregroundStyle(Dracula.green) + .padding(.top, 8) + Text("RemminaMac") + .font(.largeTitle).bold() + Text("Version \(AppInfo.version)") + .font(.subheadline).foregroundStyle(.secondary) + Text("Remmina-style connection manager for macOS — SSH, SFTP, RDP and VNC profiles with an integrated Dracula terminal.") + .font(.callout).foregroundStyle(.secondary) + .multilineTextAlignment(.center) + + Divider() + + VStack(spacing: 6) { + Text("Contributor") + .font(.caption).foregroundStyle(.secondary) + Text("x3") + .font(.headline) + Link(destination: AppInfo.repoURL) { + Label("git.sysdaemon.xyz/x3/remmina-mac", systemImage: "link") + } + } + + Divider() + + Text("MIT License · Built with SwiftUI, SwiftTerm and a custom PTY layer.") + .font(.caption).foregroundStyle(.secondary) + .multilineTextAlignment(.center) + } + .padding(28) + .frame(width: 420) + } +} diff --git a/Sources/RemminaMac/Views/ContentView.swift b/Sources/RemminaMac/Views/ContentView.swift index a1264e5..30a73ed 100644 --- a/Sources/RemminaMac/Views/ContentView.swift +++ b/Sources/RemminaMac/Views/ContentView.swift @@ -10,6 +10,7 @@ struct ContentView: View { @State private var showingEditor = false @State private var editingProfile: ConnectionProfile? @State private var showingKeys = false + @State private var showingAbout = false @State private var statusMessage = "Bereit." var body: some View { @@ -59,6 +60,8 @@ struct ContentView: View { .help("Duplizieren").disabled(selection == nil) Button(action: { showingKeys = true }) { Image(systemName: "key") } .help("SSH-Keys verwalten") + Button(action: { showingAbout = true }) { Image(systemName: "info.circle") } + .help("Info") } } } detail: { @@ -116,6 +119,9 @@ struct ContentView: View { .sheet(isPresented: $showingKeys) { SSHKeysView().environmentObject(store) } + .sheet(isPresented: $showingAbout) { + AboutView() + } } var selectedProfile: ConnectionProfile? { diff --git a/Sources/RemminaMac/Views/EmbeddedTerminalView.swift b/Sources/RemminaMac/Views/EmbeddedTerminalView.swift index cdbd714..96a944e 100644 --- a/Sources/RemminaMac/Views/EmbeddedTerminalView.swift +++ b/Sources/RemminaMac/Views/EmbeddedTerminalView.swift @@ -1,15 +1,14 @@ import SwiftUI import AppKit -/// In-App-Terminal im Dracula-Look: Ausgabe (NSTextView, read-only) + Eingabezeile. -/// Verbindet erst nach TCP-Vorab-Check (falscher Port/Host -> sofort klare Meldung). -/// Zeilenumbruch ist an, Autoscroll ans Ende, Passwort-Modus via Auge-Icon. +/// Full interactive terminal (SwiftTerm emulator, Dracula themed): +/// cursor addressing, colors, scrollback, Tab completion, arrows, Ctrl+C, +/// full-screen apps (vim/htop). Keyboard goes straight to the pty. +/// Connects after TCP pre-check + host-key approval (popup). struct EmbeddedTerminalView: View { @ObservedObject var term: PTYSession let profile: ConnectionProfile var canConnect: Bool - @State private var input = "" - @State private var secureInput = false @State private var checking = false @State private var hostKeyRequest: HostKeyRequest? @@ -59,37 +58,9 @@ struct EmbeddedTerminalView: View { .overlay(Rectangle().fill(Dracula.red.opacity(0.4)).frame(height: 1), alignment: .bottom) } - // Ausgabe im Dracula-Theme - DraculaTextView(lines: term.lines) + // Full terminal emulator (click to focus, type directly). + SwiftTermView(term: term) .frame(maxWidth: .infinity, maxHeight: .infinity) - - // Eingabezeile - HStack(spacing: 6) { - Text("❯").font(.system(.body, design: .monospaced)).foregroundStyle(Dracula.green) - // Gehärtetes Feld: keine macOS-Text-Ersetzungen (Passwort-Sonderzeichen!) - ZStack(alignment: .leading) { - if input.isEmpty { - Text(secureInput ? "Eingabe (versteckt, z. B. Passwort) …" : "Eingabe …") - .font(.system(.body, design: .monospaced)) - .foregroundStyle(Dracula.comment.opacity(0.7)) - .allowsHitTesting(false) - } - TerminalInputField(text: $input, isSecure: secureInput, onSubmit: submit) - .id(secureInput) - .frame(height: 22) - } - Button(action: { secureInput.toggle() }) { - Image(systemName: secureInput ? "eye.slash" : "eye") - } - .buttonStyle(.plain).foregroundStyle(Dracula.comment) - .help("Passwort-Modus (versteckte Eingabe)") - Button("Senden") { submit() } - .disabled(input.isEmpty || !term.isRunning) - ControlButton(label: "^C", tip: "SIGINT (Strg+C)") { term.send("\u{03}") } - ControlButton(label: "^D", tip: "EOF (Strg+D)") { term.send("\u{04}") } - } - .padding(8) - .background(Dracula.background) } .background(Dracula.background) .cornerRadius(10) @@ -173,15 +144,7 @@ struct EmbeddedTerminalView: View { } } - func submit() { - guard term.isRunning, !input.isEmpty else { return } - term.send(input + "\n") - input = "" - } - /// Fehlerbericht ohne Secrets (Passwort steht weder im Profil noch im Befehl). - /// Hinweis: Wer das Passwort sichtbar eingetippt hat (nicht Auge-Modus), - /// löscht die Ausgabe vorher per „Leeren". func copyReport() { var r = "RemminaMac Fehlerbericht\n" r += "Profil: \(profile.name) | \(profile.proto.rawValue) | \(profile.username)@\(profile.host):\(profile.port)\n" @@ -234,67 +197,3 @@ struct EmbeddedTerminalView: View { } } -struct ControlButton: View { - var label: String - var tip: String - var action: () -> Void - var body: some View { - Button(label, action: action) - .font(.system(.caption, design: .monospaced)) - .foregroundStyle(Dracula.orange) - .buttonStyle(.plain) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Dracula.currentLine.opacity(0.6), in: RoundedRectangle(cornerRadius: 5)) - .help(tip) - } -} - -/// Read-only NSTextView mit Dracula-Farben, baut NSAttributedString aus StyledText-Zeilen. -struct DraculaTextView: NSViewRepresentable { - var lines: [[StyledText]] - - func makeNSView(context: Context) -> NSScrollView { - let scroll = NSScrollView() - scroll.hasVerticalScroller = true - scroll.drawsBackground = true - scroll.backgroundColor = Dracula.nsBackground - let tv = NSTextView() - tv.isEditable = false - tv.isSelectable = true - tv.backgroundColor = Dracula.nsBackground - tv.textColor = Dracula.nsColor(for: nil) - tv.font = .monospacedSystemFont(ofSize: 12, weight: .regular) - tv.textContainerInset = NSSize(width: 8, height: 8) - tv.isAutomaticQuoteSubstitutionEnabled = false - scroll.documentView = tv - return scroll - } - - func updateNSView(_ scroll: NSScrollView, context: Context) { - guard let tv = scroll.documentView as? NSTextView else { return } - let attr = NSMutableAttributedString() - let font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular) - let boldFont = NSFontManager.shared.convert(font, toHaveTrait: .boldFontMask) - for (idx, row) in lines.enumerated() { - if row.isEmpty { - attr.append(NSAttributedString(string: idx < lines.count - 1 ? "\n" : "")) - continue - } - for seg in row { - let attrs: [NSAttributedString.Key: Any] = [ - .font: seg.style.bold ? boldFont : font, - .foregroundColor: Dracula.nsColor(for: seg.style.fg), - .backgroundColor: seg.style.bg.map { Dracula.nsColor(for: $0) } ?? Dracula.nsBackground - ] - attr.append(NSAttributedString(string: seg.text, attributes: attrs)) - } - if idx < lines.count - 1 { attr.append(NSAttributedString(string: "\n")) } - } - // Nur neu setzen wenn geändert (Performance bei viel Output) - if tv.textStorage?.string != attr.string { - tv.textStorage?.setAttributedString(attr) - } - tv.scrollToEndOfDocument(nil) - } -} diff --git a/Sources/RemminaMac/Views/SwiftTermView.swift b/Sources/RemminaMac/Views/SwiftTermView.swift new file mode 100644 index 0000000..b01fc52 --- /dev/null +++ b/Sources/RemminaMac/Views/SwiftTermView.swift @@ -0,0 +1,65 @@ +import SwiftUI +import AppKit +import SwiftTerm + +/// Full terminal emulator view (SwiftTerm): cursor addressing, colors, +/// scrollback, selection, and full-screen apps like vim/htop. +/// Keyboard goes straight to the pty (Tab completion, arrows, Ctrl+C, Esc); +/// pty output is fed in via PTYSession.onData. Dracula themed. +struct SwiftTermView: NSViewRepresentable { + @ObservedObject var term: PTYSession + + func makeCoordinator() -> Coordinator { + Coordinator(term: term) + } + + func makeNSView(context: Context) -> TerminalView { + let view = TerminalView(frame: .zero, + font: .monospacedSystemFont(ofSize: 13, weight: .regular)) + view.terminalDelegate = context.coordinator + view.nativeForegroundColor = Dracula.nsColor(for: nil) + view.nativeBackgroundColor = Dracula.nsBackground + let palette = Dracula.ansiPalette + if palette.count == 16 { + view.installColors(palette) + } + context.coordinator.view = view + term.onData = { [weak view] data in + let bytes = Array(data) + DispatchQueue.main.async { + view?.feed(byteArray: bytes[...]) + } + } + // Sync current size once the view exists (later resizes via delegate). + DispatchQueue.main.async { + term.setWinsize(cols: view.terminal.cols, rows: view.terminal.rows) + } + return view + } + + func updateNSView(_ view: TerminalView, context: Context) { + context.coordinator.term = term + } + + final class Coordinator: NSObject, TerminalViewDelegate { + var term: PTYSession? + weak var view: TerminalView? + + init(term: PTYSession) { + self.term = term + } + + func send(source: TerminalView, data: ArraySlice) { + term?.sendBytes(Data(data)) + } + + func sizeChanged(source: TerminalView, newCols: Int, newRows: Int) { + term?.setWinsize(cols: newCols, rows: newRows) + } + + func setTerminalTitle(source: TerminalView, title: String) {} + func hostCurrentDirectoryUpdate(source: TerminalView, directory: String?) {} + func scrolled(source: TerminalView, position: Double) {} + func rangeChanged(source: TerminalView, startY: Int, endY: Int) {} + } +} diff --git a/Sources/RemminaMac/Views/TerminalInputField.swift b/Sources/RemminaMac/Views/TerminalInputField.swift deleted file mode 100644 index 0af4dc9..0000000 --- a/Sources/RemminaMac/Views/TerminalInputField.swift +++ /dev/null @@ -1,119 +0,0 @@ -import SwiftUI -import AppKit - -/// Eingabefeld ohne macOS-Text-Ersetzungen (Anführungszeichen, Gedankenstriche, -/// Auto-Korrektur, Punkt-Verdopplung, Vervollständigung, Links/Daten-Erkennung). -/// Hintergrund: Normale Textfelder ersetzen still z. B. `"` durch „“ und `--` -/// durch — (je nach Tastatur-Einstellung). Im Terminal fällt das nicht auf, aber -/// ein Passwort mit Sonderzeichen kommt falsch bei ssh an → „Permission denied", -/// obwohl das Passwort „stimmt" (docs/ERRORS.md #13). -/// NSTextField kann das nicht alles abschalten, daher NSTextView-Basis. -/// Secure-Modus nutzt NSSecureTextField (per Design ohne Ersetzungen). -struct TerminalInputField: NSViewRepresentable { - @Binding var text: String - var isSecure: Bool - var onSubmit: () -> Void - - func makeCoordinator() -> Coordinator { - Coordinator(text: $text, onSubmit: onSubmit) - } - - func makeNSView(context: Context) -> NSView { - if isSecure { - let f = NSSecureTextField() - f.font = .monospacedSystemFont(ofSize: 13, weight: .regular) - f.textColor = Dracula.nsColor(for: nil) - f.backgroundColor = .clear - f.isBordered = false - f.focusRingType = .none - f.delegate = context.coordinator - return f - } else { - let tv = NSTextView() - Self.configure(tv) - tv.delegate = context.coordinator - return tv - } - } - - func updateNSView(_ view: NSView, context: Context) { - context.coordinator.onSubmit = onSubmit - // Delegat während Programm-Sets abhängen: sonst feuert textDidChange synchron - // zurück ins SwiftUI-State — mitten im Render -> Crash ("Modifying state - // during view update"). User-Tippen läuft weiter über den Delegaten. - if let f = view as? NSTextField { - if f.stringValue != text { - f.delegate = nil - f.stringValue = text - f.delegate = context.coordinator - } - } else if let tv = view as? NSTextView, tv.string != text { - tv.delegate = nil - tv.string = text - tv.delegate = context.coordinator - } - } - - /// Alle Ersetzungen aus. Auch als Harness-Test nutzbar (reine Flags, headless). - static func configure(_ tv: NSTextView) { - tv.isRichText = false - tv.importsGraphics = false - tv.isAutomaticQuoteSubstitutionEnabled = false - tv.isAutomaticDashSubstitutionEnabled = false - tv.isAutomaticTextCompletionEnabled = false - tv.isAutomaticSpellingCorrectionEnabled = false - tv.isContinuousSpellCheckingEnabled = false - tv.isAutomaticDataDetectionEnabled = false - tv.isAutomaticLinkDetectionEnabled = false - tv.isAutomaticTextReplacementEnabled = false - tv.font = .monospacedSystemFont(ofSize: 13, weight: .regular) - tv.textColor = Dracula.nsColor(for: nil) - tv.backgroundColor = .clear - tv.drawsBackground = false - tv.isVerticallyResizable = false - tv.isHorizontallyResizable = true - tv.textContainer?.widthTracksTextView = false - tv.textContainer?.containerSize = NSSize(width: 100000, height: 24) - } - - final class Coordinator: NSObject, NSTextFieldDelegate, NSTextViewDelegate { - var text: Binding - var onSubmit: () -> Void - - init(text: Binding, onSubmit: @escaping () -> Void) { - self.text = text - self.onSubmit = onSubmit - } - - func controlTextDidChange(_ obj: Notification) { - if let f = obj.object as? NSTextField { - text.wrappedValue = f.stringValue - } - } - - func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { - if commandSelector == #selector(NSResponder.insertNewline(_:)) { - onSubmit() - return true - } - return false - } - - func textDidChange(_ notification: Notification) { - if let tv = notification.object as? NSTextView { - // Einzeiler: harte Umbrüche sofort entfernen (Paste mit \n) - let single = tv.string.replacingOccurrences(of: "\n", with: "") - if single != tv.string { tv.string = single } - text.wrappedValue = single - } - } - - func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { - if commandSelector == #selector(NSResponder.insertNewline(_:)) { - onSubmit() - return true - } - return false - } - } -} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8b74111..b546f90 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,33 +1,61 @@ -# Architektur +# Architecture ``` Sources/RemminaMac/ - RemminaMacApp.swift # @main, WindowGroup, lädt ProfileStore - Models/ConnectionProfile.swift # ProtocolType + ConnectionProfile + StoredSSHKey (Codable) - Stores/ProfileStore.swift # ObservableObject, JSON-Persistenz, Filter/Suche/Gruppen - Security/KeychainHelper.swift# SecItemAdd/Copy/Delete Wrapper - Services/ConnectionServices.swift # SSHService (ssh-Befehl) + RDPService (freerdp/rdc/vnc) - Views/ContentView.swift # NavigationSplitView: Sidebar + Tabs + Statusbar - Views/SessionView.swift # Detail pro Sitzung - Views/ProfileEditorView.swift# Formular Neu/Bearbeiten + Keychain-Anbindung - Views/SSHKeysView.swift # Key-Liste, Import, ssh-keygen + RemminaMacApp.swift # @main, WindowGroup, loads ProfileStore + Models/ConnectionProfile.swift # ProtocolType + ConnectionProfile + StoredSSHKey (Codable) + Models/ProfileValidation.swift # input checks + trimming (pure, tested) + Stores/ProfileStore.swift # ObservableObject, JSON persistence, filter/search/groups + Security/KeychainHelper.swift # SecItemAdd/Copy/Delete wrapper + Services/ConnectionServices.swift # SSHService (ssh args, secrets, sshpass, agent) + RDPService + Services/PTYSession.swift # real PTY session (posix_openpt), stderr pipe, TerminalManager + Services/AnsiParser.swift # ANSI/SGR parser for diagnostics buffer (pure, tested) + Services/TCPCheck.swift # TCP pre-check via Network.framework + Services/HostKeyService.swift # keyscan/fingerprint/accept/revoke via system ssh tools + Services/TerminalTheme.swift # Dracula palette (SwiftUI + NSColor + SwiftTerm) + Views/ContentView.swift # NavigationSplitView: sidebar + tabs + status bar + Views/SessionView.swift # per-session detail + Views/SwiftTermView.swift # full terminal emulator view (SwiftTerm, Dracula) + Views/ProfileEditorView.swift # new/edit form + Keychain wiring + validation + Views/SSHKeysView.swift # key list, import, ssh-keygen + Views/HostKeySheet.swift # fingerprint popup (accept/decline, MITM warning) + Views/AboutView.swift # about sheet (contributor, repo link) +Sources/ctty-run/ + ctty-run.c # gives the ssh child a controlling terminal + # (double-fork: setsid + slave open + exec) ``` -## Datenfluss -1. `RemminaMacApp` erstellt `ProfileStore` als `@StateObject`, injiziert als `environmentObject`. -2. `ProfileStore.load()` liest `profiles.json` + `sshkeys.json`, sonst Sample-Daten. -3. `ContentView` filtert via `searchText`/`selectedGroup`, `selection` steuert Detail. -4. `connect(_:)` setzt `lastConnected`, öffnet Tab, delegiert an `SSHService`/`RDPService`. -5. Secrets nur via `KeychainHelper`, JSON enthält nur Account-Referenzen. +## Data flow +1. `RemminaMacApp` creates `ProfileStore` as `@StateObject`, injects as `environmentObject`. +2. `ProfileStore.load()` reads `profiles.json` + `sshkeys.json`, else sample data. +3. `ContentView` filters via `searchText`/`selectedGroup`; `selection` drives detail. +4. `connect(_:)` stamps `lastConnected`, opens a tab. The terminal view runs: + TCP pre-check → host-key status (popup if unknown/changed) → `PTYSession.start()`. +5. `PTYSession` spawns via `ctty-run` (controlling terminal): stdin/stdout on the + pty slave, stderr on a loss-free pipe. Output feeds SwiftTerm (`onData`) and + the diagnostics buffer (`lines`, used by the error banner + report). +6. Keyboard flows from SwiftTerm's delegate straight into the pty master + (`sendBytes`); window resizes propagate via `TIOCSWINSZ` (`setWinsize`). +7. Secrets only via `KeychainHelper`; JSON holds account references. + `TerminalManager` caches one session per profile and reconfigures on any + config change (fingerprint incl. secret + sshpass presence). -## Wichtige Dateien -- `Stores/ProfileStore.swift:13` — Pfade + `load()`/`save()` -- `Models/ConnectionProfile.swift:3` — Protokoll-Defaults (Ports, Icons) -- `Services/ConnectionServices.swift:6` — `sshArguments(for:key:)` -- `Views/ContentView.swift` — Remmina-Layout +## Key files +- `Stores/ProfileStore.swift` — paths + `load()`/`save()` +- `Models/ConnectionProfile.swift` — protocol defaults (ports, icons) +- `Services/PTYSession.swift` — spawn, lifecycle, `TerminalManager` +- `Services/HostKeyService.swift` — fingerprint flow +- `Views/SwiftTermView.swift` — emulator wiring + Dracula theme +- `Views/ContentView.swift` — Remmina layout -## Nächste Ausbaustufen -1. SwiftTerm-View statt externem Terminal + `swift-nio-ssh` für echte SSH-Sessions. -2. FreeRDP als XPC-Service einbetten statt `Process`. -3. SFTP-Browser (NIO SFTP oder libssh2-Bridge). -4. Export/Import-Buttons + Drag&Drop für Keys. +## Verification strategy +Logic is verified with throwaway `swiftc` harnesses in `/tmp` (never committed): +ANSI parser, PTY roundtrips, real `ssh` runs, Keychain roundtrips, TCP checks, +host-key flow against a local paramiko test server, SwiftTerm core headless +(cursor addressing, alt-screen, clear), view-layer stress. See `docs/ERRORS.md`. + +## Next steps +1. Embedded RDP (FreeRDP bridge) instead of `Process`. +2. Integrated SFTP browser. +3. Export/Import buttons + drag & drop for keys. +4. Tray / favorites / folder badges like Remmina. diff --git a/docs/ERRORS.md b/docs/ERRORS.md index 2265e6e..f19c6f0 100644 --- a/docs/ERRORS.md +++ b/docs/ERRORS.md @@ -1,250 +1,173 @@ -# Fehlerprotokoll (ERRORS.md) +# Error log (ERRORS.md) -Alle Fehler, die beim Erstellen / Bauen / Releasen von RemminaMac aufgetreten sind — mit Ursache, Fix und Status. Wird bei jedem Build fortgeschrieben. +Every error hit while building/releasing RemminaMac — cause, fix, status. Updated with every build. -Stand: 2026-09-21 · Version 0.1.0 · macOS arm64 · Swift 5.9+ · Xcode 15+ +As of 2026-09-22 · version 0.7.0 · macOS arm64 · Swift 5.9+ · Xcode 15+ -## Übersicht -| # | Zeitpunkt | Phase | Fehler | Schwere | Status | -|---|-----------|-------|--------|---------|--------| -| 1 | 2026-09-21 | `swift build` (debug) | `WindowGroup has no member 'windowTitle'` | blockierend | ✅ behoben | -| 2 | 2026-09-21 | `swift build -c release` | keine Fehler, nur lange Link-Zeit (~40 s) | Info | ✅ ok | -| 3 | 2026-09-21 | Release-Skript | `dist/` war nicht in `.gitignore` → Binary drohte committet zu werden | mittel | ✅ behoben (ignoriert) | -| 4 | 2026-09-21 | Gitea-Upload | `GET /api/v1/user` → `401 invalid username, password or token` mit alten Credentials | blockierend (Push/Release) | ✅ behoben — neue Credentials ok, Push + Release v0.1.0 hochgeladen | -| 5 | 2026-09-21 | App-Bundle + DMG | keine Fehler: `build-app.sh` (Signatur ok), `build-dmg.sh` (UDZO, 200K), Mount-Verifikation ok (App + Applications/Programme-Links) | Info | ✅ ok | -| 6 | 2026-09-21 | SSH verbinden | kein Terminal öffnete sich: `SSHService.openInTerminal` nutzte `NSAppleScript` → scheitert still (keine `NSAppleEventsUsageDescription`, kein Error-Handling) | blockierend | ✅ behoben — In-App-PTY-Terminal (Dracula) + `.command`-Fallback | -| 7 | 2026-09-21 | ANSI-Parser (Test) | `feed()` flusht Chunk erst NACH `parseCSI` → Text bekam den neuen statt alten Stil (`rot` mit fg=nil statt 1) | mittel | ✅ behoben — Flush vor Stilwechsel | -| 8 | 2026-09-21 | SSH-Session stirbt instant ohne Meldung | stderr hing mit am PTY: macOS verwirft ungelesene Master-Daten bei Slave-Close → „Code 255" ohne Text | blockierend | ✅ behoben — stderr→Pipe, TCP-Vorab-Check, Fehler-Banner | -| 9 | 2026-09-21 | `swift build` | `Result`: String conformt nicht zu Error (Folgefehler: DispatchQueue-Overload) | blockierend | ✅ behoben — Completion als `(String?) -> Void` | -| 10 | 2026-09-21 | Icon-Generator (`RenderIcon.swift`) | 3 Swift-Fehler: `guard let x = f() ?? g()` (nicht optional), `Int * CGFloat`, falscher Preview-Pfad | klein | ✅ behoben | -| 11 | 2026-09-21 | Passwort-Feature | Wunsch: Passwort + Keys speichern UND nutzen (bisher nur gespeichert, nie benutzt) | Feature | ✅ umgesetzt — Auto-Login via sshpass, sonst interaktiv | -| 12 | 2026-09-21 | „Immer Verbindung falsch" | User-Report ohne Details (Ziel/Credentials unbekannt) | offen | ⬜ Teils: Editor-Validierung + kopierbarer Fehlerbericht; warte auf Bericht | -| 13 | 2026-09-21 | Permission denied trotz richtigem Passwort | Text-Ersetzung im Eingabefeld verfälscht Sonderzeichen | hoch | ✅ behoben — gehärtetes Eingabefeld (alle Ersetzungen aus, 9/9 Tests) | -| 14 | 2026-09-21 | Terminal geht, App nicht (gleiches Passwort) | SSH-Agent-Socket fehlt GUI-Prozessen → evtl. Key-Login in Terminal | hoch | ✅ App reicht Agent jetzt durch; Gegenprobe beim User offen | -| 15 | 2026-09-21 | Crash bei Tippen + Senden | Ursache unklar (headless nicht reproduzierbar) | kritisch | ⬜ Gehärtet (v0.5.3); Crash-Log angefragt | -| 16 | 2026-09-22 | Profil-Änderungen wirkungslos | Session-Cache ignorierte Host-/Passwort-/Key-Korrekturen | hoch | ✅ behoben — Fingerprint-Reconfig (9/9 Tests) | -| 17 | 2026-09-22 | rzsv: Vorab-Check Timeout | Port 22 wird serverseitig gedroppt (443 ok) — kein App-Fehler | extern | ⬜ Server-Seite: fail2ban/VPN/Firewall prüfen | -| 18 | 2026-09-22 | Host-Key-Yes/No unmöglich | ssh ohne Controlling-Terminal: /dev/tty ENXIO → stummer Abbruch | kritisch | ✅ ctty-run-Helper (Double-Fork) + Fingerprint-Popup | -| 19 | 2026-09-22 | Popup leer (s. Screenshot) | `if let` im Sheet nimmt Empty-Zweig ohne Update-Pfad | mittel | ✅ `.sheet(item:)` + Keyscan im Hintergrund | +## Overview +| # | Date | Phase | Error | Severity | Status | +|---|------|-------|-------|----------|--------| +| 1 | 2026-09-21 | `swift build` (debug) | `WindowGroup has no member 'windowTitle'` | blocking | ✅ fixed | +| 2 | 2026-09-21 | `swift build -c release` | no errors, just slow first link (~40 s) | info | ✅ ok | +| 3 | 2026-09-21 | release script | `dist/` missing from `.gitignore` → binary almost committed | medium | ✅ fixed (ignored) | +| 4 | 2026-09-21 | Gitea upload | `GET /api/v1/user` → `401` with old credentials | blocking | ✅ fixed — new credentials, push + v0.1.0 live | +| 5 | 2026-09-21 | app bundle + DMG | no errors: signing ok, UDZO DMG, mount verified | info | ✅ ok | +| 6 | 2026-09-21 | SSH connect | no terminal opened: `NSAppleScript` failed silently (no AppleEvents permission/handling) | blocking | ✅ fixed — in-app PTY terminal (Dracula) + `.command` fallback | +| 7 | 2026-09-21 | ANSI parser (test) | `feed()` flushed chunk AFTER `parseCSI` → wrong style | medium | ✅ fixed — flush before style switch | +| 8 | 2026-09-21 | SSH session dies instantly, no message | stderr shared the PTY: macOS drops unread master data on slave close → bare "code 255" | blocking | ✅ fixed — stderr→pipe, TCP pre-check, error banner | +| 9 | 2026-09-21 | `swift build` | `Result`: String is not Error (follow-on DispatchQueue errors) | blocking | ✅ fixed — completion as `(String?) -> Void` | +| 10 | 2026-09-21 | icon generator (`RenderIcon.swift`) | 3 Swift errors: non-optional `guard let`, `Int * CGFloat`, wrong preview path | small | ✅ fixed | +| 11 | 2026-09-21 | password feature | request: store passwords + keys AND use them (stored but never used) | feature | ✅ done — sshpass auto-login, else interactive | +| 12 | 2026-09-21 | "always connection wrong" | user report without details | open | ⬜ partly: editor validation + copyable error report | +| 13 | 2026-09-21 | Permission denied with correct password | text substitution in input field mangled special chars | high | ✅ fixed — hardened input field (all substitutions off, 9/9) | +| 14 | 2026-09-21 | Terminal works, app doesn't (same password) | SSH agent socket missing for GUI processes → maybe key login in Terminal | high | ✅ app forwards agent socket; user confirmation open | +| 15 | 2026-09-21 | crash on type + send | cause unclear (not reproducible headless) | critical | ⬜ hardened (v0.5.3); crash log requested | +| 16 | 2026-09-22 | profile edits had no effect | session cache ignored host/password/key corrections | high | ✅ fixed — fingerprint reconfig (9/9) | +| 17 | 2026-09-22 | rzsv: pre-check timeout | port 22 dropped server-side (443 ok) — not an app bug | external | ⬜ server side: fail2ban/VPN/firewall | +| 18 | 2026-09-22 | host-key yes/no impossible | ssh without controlling terminal: /dev/tty ENXIO → silent abort | critical | ✅ ctty-run helper (double-fork) + fingerprint popup | +| 19 | 2026-09-22 | empty popup (see screenshot) | `if let` in sheet takes empty branch with no update path | medium | ✅ `.sheet(item:)` + background keyscan | +| 20 | 2026-09-22 | terminal not fully interactive | single-line input, no completion, no vim | feature | ✅ done — SwiftTerm emulator (0.7.0) | +| 21 | 2026-09-22 | yes-asked-twice + password ignored | popup-accept race; sshpass installs went stale | medium | ✅ verified no re-ask; sshpass in fingerprint | --- -## #1 — `windowTitle` existiert nicht auf `WindowGroup` -**Phase:** erster `swift build` -**Log (gekürzt):** -``` -Sources/RemminaMac/RemminaMacApp.swift:14:10: error: value of type 'WindowGroup' has no member 'windowTitle' -14 | .windowTitle("RemminaMac") -``` -**Ursache:** `windowTitle(_:)` ist kein Modifier für `WindowGroup`. Der Fenstertitel wird im `WindowGroup`-Init übergeben. -**Fix:** `Sources/RemminaMac/RemminaMacApp.swift`: -```swift -// vorher (falsch): -WindowGroup { ... }.windowTitle("RemminaMac") -// nachher (korrekt): -WindowGroup("RemminaMac") { ... } -``` -**Verifikation:** `swift build` → `Build complete! (1.31s)`. -**Status:** ✅ behoben, kein Re-Check offen. - -## #2 — Release-Build langsam, aber fehlerfrei -**Phase:** `bash scripts/build-release.sh 0.1.0` → `swift build -c release` -**Log:** -``` -Building for production... -[4/5] Linking RemminaMac -Build complete! (40.54s) -``` -**Ursache:** keine — Release-Optimierung + Link brauchen beim ersten Mal ~40 s. -**Fix:** keiner nötig. Bei wiederholten Builds ist es schneller (Cache in `.build/`). -**Artefakte:** -``` -dist/RemminaMac-0.1.0-macos-arm64.zip (186K) -dist/RemminaMac-0.1.0-macos-arm64.zip.sha256 (c476bb3c…) -``` -**Status:** ✅ ok. - -## #3 — `dist/` fast committet -**Phase:** Release-Vorbereitung -**Ursache:** `dist/` enthielt ZIP + entpacktes Binary, `.gitignore` kannte es noch nicht. -**Fix:** `.gitignore` ergänzt: -``` -.build/ -dist/ -*.zip -*.sha256 -``` -**Verifikation:** `git status --short` zeigt kein `dist/` mehr. -**Status:** ✅ behoben. - -## Vorlage für neue Fehler +## Template for new errors ```markdown -## #N — Titel +## #N — Title **Phase:** ... **Log:** ... -**Ursache:** ... +**Cause:** ... **Fix:** ... -**Verifikation:** ... -**Status:** ⬜ offen / ✅ behoben +**Verified:** ... +**Status:** ⬜ open / ✅ fixed ``` -## Wie Fehler hierher kommen -- Jeder `swift build` / `swift build -c release` / Skript-Lauf mit Fehler → Log-Ausschnitt + Exit-Code hier eintragen. -- Gitea-Push/Release-Fehler (z. B. 401, Repo existiert schon, Asset-Upload schlägt fehl) ebenfalls hier dokumentieren. -- Neue Tests: `AnsiParser` per `swiftc`-Harness (`/tmp`, SGR/CRLF/Clear/Map), `PTYSession` per Smoke-Test (`/bin/echo` + `ssh -G` durchs echte PTY). +## How errors get here +- Every `swift build` / `swift build -c release` / script run with an error → log excerpt + exit code goes here. +- Gitea push/release errors likewise. +- New tests: `AnsiParser` via `swiftc` harness, `PTYSession` smoke tests, Keychain roundtrips, TCP checks, host-key flow vs local paramiko server, SwiftTerm core headless. -## #6 — SSH-Klick öffnet kein Terminal (User-Bugreport) -**Phase:** Laufzeit, SSH-Profil → Verbinden -**Ursache:** `Services/ConnectionServices.swift` nutzte `NSAppleScript(tell application "Terminal" to do script …)`. Ohne `NSAppleEventsUsageDescription` und ohne Error-Auswertung scheitert das still — es passiert schlicht nichts. -**Fix:** -- Neu: `Services/PTYSession.swift` — echte PTY-Session (`posix_openpt`, `ssh` mit TTY), `TerminalManager` pro Profil (überlebt Tab-Wechsel). -- Neu: `Views/EmbeddedTerminalView.swift` — In-App-Terminal im Dracula-Theme (`Services/TerminalTheme.swift`, `Services/AnsiParser.swift`), Auto-Connect per `onAppear`, Eingabezeile + Passwort-Modus + `^C`/`^D`. -- `SessionView` bettet das Terminal ein (min. 380 pt), `ContentView.connect()` öffnet nur noch den Tab. -- Extern-Fallback umgebaut auf `.command`-Datei via `NSWorkspace` (keine Automation-Rechte nötig). -**Verifikation:** `swift build` ok; Parser-Harness 8/8; PTY-Smoke (`/bin/echo` → Output + Exit 0); `ssh -G example.com` durchs PTY ok. -**Status:** ✅ behoben. - -## #8 — Terminal verbindet nicht, „direkt gekappt" (User-Bugreport) -**Phase:** Laufzeit, SSH-Profil → Tab öffnet, sofort „Sitzung beendet (Code 255)." — ohne jede Meldung -**Reproduktion (Harness, exakter App-Pfad `ssh -p 1 nobody@127.0.0.1`):** -``` -STATE: done(exitCode: Optional(255), message: "Sitzung beendet (Code 255).") -TEXT: "\n\n— Sitzung beendet (Code 255). —\n" # <- Fehlermeldung fehlt! -``` -**Ground Truth (Python-pty):** Selbst `/bin/echo hello` liefert `b''`, wenn man erst NACH Child-Exit liest; bei sofortigem Lesen kommt `b'hello-py\r\n'`. → macOS verwirft ungelesene PTY-Master-Daten beim Slave-Close (danach EIO). Unser `DispatchSourceRead` verliert dieses Rennen bei Instant-Exit-Kindern — und ssh schreibt ALLES (inkl. „Connection refused") nach stderr, das mit am PTY hing. -**Fix (`Services/PTYSession.swift`, `Services/TCPCheck.swift`, `Views/EmbeddedTerminalView.swift`):** -- stderr → `Pipe` (gepufferte Daten überleben Child-Exit garantiert), stdin/stdout bleiben am PTY (TTY erhalten für interaktive Prompts) -- `finalFlush` (non-blocking Rest-Drain) + Epoch-guarded Lifecycle (kein State-/FD-Durcheinander bei Trennen→Neuverbinden) -- TCP-Vorab-Check (Network.framework, 4 s Timeout) mit deutschen Klartext-Meldungen (refused/DNS/Timeout/Port) -- Rotes Fehler-Banner mit letzter Meldung + Diagnose- und Erneut-Buttons -**Verifikation:** Refused-Harness zeigt jetzt `ssh: connect to host 127.0.0.1 port 1: Connection refused` (PASS); Roundtrip (`cat`) PASS; `ssh -G` PASS; TCP-Harness (closed/open/empty) PASS. -**Status:** ✅ behoben. - -## #9 — Build: `Result` ungültig -**Log:** `error: type 'String' does not conform to protocol 'Error'` (+ Folgefehler an `DispatchQueue.main.async`) -**Fix:** `TCPCheck.check` nutzt `(String?) -> Void` (nil = ok, sonst deutsche Meldung). -**Status:** ✅ behoben. - -## #11 — Passwort speichern UND benutzen (User-Wunsch) -**Stand:** Passwort/Key-Passphrase wurden in der Keychain gespeichert, aber nie zum Einloggen benutzt (ssh fragte immer interaktiv). -**Umsetzung:** -- `SSHService.loginSecret()` (Profil-Passwort → Key-Passphrase), `sessionConfig()` (rein, testbar): mit Secret + sshpass → `sshpass -e ssh/sftp …` + `SSHPASS`-Env (nie Befehlszeile/Logs); sonst direkter Aufruf + interaktiver Prompt. -- `PTYSession.customEnv` + `autoLogin`-Flag, `TerminalManager` verdrahtet alles; Status zeigt „Auto-Login via Keychain". -- Editor: Gespeichert-Anzeige (✓), Löschen-Button, sshpass-Hinweis; Titel-Bug (`UUID`-Vergleich mit neuem Random-Objekt) via `isNew` ersetzt. -**Verifikation:** Config-Harness 9/9 (inkl. „Passwort nie in Befehlszeile"); echter Keychain-Roundtrip 7/7 (save/load/delete + Priorität + Fallback). -**Hinweis:** Ohne `sshpass` (`brew install sshpass`) bleibt es beim interaktiven Prompt — hier auf dem Build-Mac ebenfalls nicht installiert, getestet wurde der interaktive Pfad + Logik. -**Status:** ✅ umgesetzt. - -## #10 — Icon-Generator Buildfehler -**Log:** `guard let` auf nicht-optionalem Ausdruck (`??` unwrapt schon); `Int * CGFloat`; Preview lag in `.iconset/` statt `/tmp/`. -**Fix:** `??`-Fallback direkt zuweisen; `CGFloat(...)`; Pfad korrigiert. -**Status:** ✅ behoben. Icon: `Resources/AppIcon.icns` (per `scripts/generate-icon.sh` reproduzierbar), Vorschau `docs/icon-preview.png`. - -## #7 — ANSI-Parser: Text bekam falschen Stil -**Phase:** `swiftc`-Harness während #6 -**Log:** `text("rot", fg: nil)` statt `fg: 1` -**Ursache:** Chunk wurde erst nach dem SGR-Wechsel geflusht. -**Fix:** `Services/AnsiParser.swift` — `flush()` vor `parseCSI`. -**Verifikation:** Harness 8/8 grün. -**Status:** ✅ behoben. - -## #4 — Gitea-Upload: 401 trotz erreichbarem Server -**Phase:** `POST /api/v1/user/repos` + `GET /api/v1/user` auf `https://git.sysdaemon.xyz` (Version 1.27.3, per `/api/v1/version` bestätigt) +## #1 — `windowTitle` doesn't exist on `WindowGroup` +**Phase:** first `swift build` **Log:** ``` -GET /user -> 401 -{"message":"invalid username, password or token","url":"https://git.sysdaemon.xyz/api/swagger"} +RemminaMacApp.swift:14:10: error: value of type 'WindowGroup' has no member 'windowTitle' ``` -Getestet mit Login `x3` und `x3@sysdaemon.xyz` — beide 401. Kein Quote-/Encoding-Problem (Basic-Auth via Python, kein Shell-Escaping). -**Mögliche Ursachen:** -1. Passwort falsch/ablaufen bzw. Sonderzeichen-Typo -2. Account braucht App-Token statt Passwort (Gitea: User → Einstellungen → Anwendungen → Token erzeugen) -3. Falscher Owner-/Repo-Name -**Fix (manuell, sobald Credentials stimmen):** -```bash -cd RemminaMac -git remote add origin https://@git.sysdaemon.xyz//remmina-mac.git -git push -u origin main -export GITEA_HOST="https://git.sysdaemon.xyz" GITEA_OWNER="" GITEA_REPO="remmina-mac" GITEA_TOKEN="" VERSION="0.1.0" -./scripts/publish-gitea-release.sh -``` -Oder mit Token als Passwort: `git push https://:@git.sysdaemon.xyz//remmina-mac.git main`. -**Lokaler Stand:** Commit `cd834dc` + `dist/RemminaMac-0.1.0-macos-arm64.zip` sind fertig und warteten nur auf den Push. -**Nachtrag 2026-09-21:** ✅ behoben — mit korrigierten Credentials: `GET /api/v1/user → 200 (login x3)`, Repo `x3/remmina-mac` erstellt (201), `git push -u origin main` ok, Release `v0.1.0` (id 1) + 2 Assets (ZIP + SHA256) hochgeladen. -**Status:** ✅ behoben. +**Cause:** `windowTitle(_:)` is not a `WindowGroup` modifier; the title goes in the `WindowGroup` init. +**Fix:** `WindowGroup("RemminaMac") { ... }`. +**Status:** ✅ fixed. -## #12 — „Immer Verbindung falsch" (User-Report, Details offen) -**Stand:** Gemeldet ohne Ziel-Host, Banner-Text oder Credentials — aus der Ferne nicht reproduzierbar. Systematische App-Fehler als Ursache weitgehend ausgeschlossen (PTY/ssh-Pfad, Fehlertext-Anzeige, TCP-Check und Keychain-Fluss sind per Harness verifiziert). -**Häufigste reale Ursachen (Checkliste):** -1. `user@host` im Host-Feld oder `ssh://`-Präfix → jetzt Live-Warnung im Editor -2. Leerzeichen/Vertipper in Host/User → wird jetzt getrimmt + gewarnt -3. Falsches Passwort / Key ohne Passphrase-Eintrag → Banner + Diagnose nutzen -4. Server blockt (Fail2ban, nur Key-Login erlaubt) → Server-Logs prüfen -**Umgesetzt:** `Models/ProfileValidation.swift` (trimmen + Warnungen, Harness 7/7), Passwort direkt unter Benutzername, „Bericht kopieren" im Fehler-Banner (ohne Secrets). -**Offen:** Bitte Banner-Text oder kopierten Bericht hierher schicken — dann steht die Ursache in einer Zeile. -**Status:** ⬜ wartet auf Fehlerbericht. +## #2 — Slow but clean release build +**Phase:** `bash scripts/build-release.sh 0.1.0` → `swift build -c release` (~40 s first link). +**Cause:** none — release optimization + link just take a while cold; faster on rebuilds (`.build/` cache). +**Status:** ✅ ok. -## #13 — Permission denied trotz richtigem Passwort (User-Fehlerbericht) -**Bericht:** `administrator@sysdaemon.xyz:22`, ssh fragt 3x Passwort → `Permission denied (publickey,password)`. Gleiches Passwort im Terminal.app funktioniert. -**Ursache:** Das Terminal-Eingabefeld war ein Standard-TextField. macOS ersetzt dort je nach Einstellung still Zeichen (`"` → „“, `--` → —, Auto-Korrektur). Ein Passwort mit Sonderzeichen kommt dadurch falsch bei ssh an — im Terminal fällt nichts auf, ssh sagt nur „denied". Verifiziert: Standard-`NSTextField` hat diese Schalter an bzw. `NSTextField` kann Dash-Ersetzung gar nicht abschalten (Compiler-Beweis). -**Fix (`Views/TerminalInputField.swift`):** Eigenes Eingabefeld auf `NSTextView`-Basis mit allen Ersetzungen explizit aus (Quotes, Dashes, Completion, Spelling, Data/Link-Erkennung, Text-Replacement, Plain-Text); Secure-Variante via `NSSecureTextField` (per Design ersetzungsfrei); Enter → Senden, Paste-Normalisierung (keine Umbrüche). -**Verifikation:** Harness 9/9 (alle Flags aus). Hinweis: Restrisiko Tastaturlayout (Z/Y) bleibt — Banner-Tipp ergänzt. -**Status:** ✅ behoben — bitte mit v0.5.1 erneut testen. +## #3 — `dist/` almost committed +**Cause:** `dist/` held ZIP + unpacked binary, `.gitignore` didn't know it. +**Fix:** ignore `.build/`, `dist/`, `*.zip`, `*.sha256`. +**Status:** ✅ fixed. -## #14 — Terminal.app loggt ein, RemminaMac nicht (gleiches Passwort) -**Stand (User-Antworten):** v0.5.1 installiert, Passwort-Login, Terminal.app funktioniert mit denselben Daten. -**Analyse:** App-seitig ist alles verifiziert (PTY, Fehleranzeige, Passwort-Loop mit Sonderzeichen, Eingabe-Ersetzungen aus). Gleiches Binary + gleiche Config → Unterschied muss aus der Umgebung kommen. Hauptkandidat: **SSH-Agent** — Finder-gestartete GUI-Prozesse erben `SSH_AUTH_SOCK` nicht. Nutzt Terminal unbemerkt einen Agent-Key (statt Passwort), scheitert die App mit Passwort. Verifiziert: `launchctl getenv SSH_AUTH_SOCK` liefert den Socket, Shell-Env wäre ohne Abfrage leer für GUI-Apps. -**Fix:** `SSHService.agentSocket()` (launchctl + Existenzprüfung) → `TerminalManager` injiziert `SSH_AUTH_SOCK` ins ssh-Env (koexistiert mit SSHPASS). Banner-Tipp ergänzt. -**Verifikation:** Harness 4/4 (Auflösung 0.07 s, Pfad existiert, `ssh -G` ok mit echtem + kaputtem Socket). -**Gegenprobe beim User (bitte ausführen):** -```bash -ssh-add -l -ssh -o IdentitiesOnly=yes administrator@sysdaemon.xyz -``` -Zweite Zeile erzwingt Passwort-Login ohne Agent — scheitert sie auch im Terminal, war es immer der Agent-Key. -**Status:** ✅ App-Seite umgesetzt — warte auf Gegenprobe. +## #4 — Gitea upload: 401 despite reachable server +**Phase:** `POST /api/v1/user/repos` + `GET /api/v1/user` on `https://git.sysdaemon.xyz` (v1.27.3 confirmed via `/api/v1/version`). +**Log:** `GET /user -> 401 {"message":"invalid username, password or token"}` — tried logins `x3` and `x3@sysdaemon.xyz`, both 401 (Basic auth via Python, no shell-quoting issue). +**Fix:** with corrected credentials: `GET /api/v1/user → 200 (login x3)`, repo `x3/remmina-mac` created (201), push ok, release `v0.1.0` + assets uploaded. +**Status:** ✅ fixed. -## #15 — Crash bei Tippen + Senden (User-Report) -**Stand:** „Sobald ich was eintippe und absende, crasht das Programm." Headless nicht reproduzierbar — alle Ebenen einzeln grün. -**Ausgeschlossen per Test:** -- SIGPIPE beim Schreiben (Harness: macOS liefert EIO statt Signal; `send` nach `stop()` überlebt) — Hypothese widerlegt -- View-Schicht (echter Coordinator + NSTextView: Tippen mit Umlauten/Quotes/Paste, Enter, 200 Programm-Sets) — grün -- PTY-Roundtrip, Refused-, Keychain-, TCP-Harness — grün -**Gehärtet (v0.5.3, ohne Beweis dass es DER Crash war):** -- `TerminalInputField.updateNSView`: Delegat bei Programm-Sets abgehängt — sonst feuert `textDidChange` synchron zurück ins State während SwiftUI rendert („Modifying state during view update" → Crash-Kandidat Nr. 1, genau beim Senden: Eingabe wird geleert) -- `PTYSession.send`: Force-Unwrap `baseAddress!` → Guard, Leer-Check -**Weiter nötig bei erneutem Crash:** Console.app → Absturzberichte → neuesten `RemminaMac`-Eintrag öffnen → `Exception Type`, `Termination Reason` und crashed Thread hierher schicken. Damit steht die Stelle fest. -**Status:** ⬜ wartet auf Rückmeldung / Crash-Log. +## #6 — SSH click opens no terminal (user bug report) +**Cause:** `SSHService.openInTerminal` used `NSAppleScript(tell application "Terminal" …)` — fails silently without `NSAppleEventsUsageDescription` and with no error handling. +**Fix:** `Services/PTYSession.swift` (real PTY via `posix_openpt`), `Views/EmbeddedTerminalView.swift` (in-app Dracula terminal, auto-connect), external fallback via `.command` file + `NSWorkspace` (no automation rights needed). +**Verified:** build ok; parser 8/8; PTY smoke (`/bin/echo` → output + exit 0); `ssh -G` through PTY ok. +**Status:** ✅ fixed. -## #16 — Profil-Änderungen wirkungslos („geht immer noch nicht") -**Stand:** Nach mehreren Fixes meldet der User weiterhin pauschal Misserfolg. Code-Review (`TerminalManager.session`, Zeile 291): Eine einmal erstellte Session wurde für immer wiederverwendet — Host-/Passwort-/Key-Korrekturen im Editor erreichten die laufende/stale Session nie. Jede Korrektur lief ins Leere. -**Fix:** Konfigurations-Fingerprint (Proto/Host/Port/User/ExtraArgs/Key-Pfad/Secret-Wert) pro Session. Bei Abweichung und nicht-laufender Session: stoppen + neu konfigurieren (gleiches Objekt, Tabs bleiben). Laufende Sessions werden nie unter den Füßen wegkonfiguriert ( erst Trennen). -**Verifikation:** Harness 9/9 (Host-Wechsel reconfiguriert, Secret-Wechsel erkannt, laufende ssh-Session an Banner-lesendem Socket unangetastet). -**Status:** ✅ behoben — bitte mit v0.5.4 erneut testen (Profil ggf. einmal neu speichern). +## #7 — ANSI parser: text got the wrong style +**Phase:** `swiftc` harness during #6. Log: `text("rot", fg: nil)` instead of `fg: 1`. +**Cause:** chunk flushed only after the SGR switch. +**Fix:** `flush()` before `parseCSI`. Harness 8/8 green. +**Status:** ✅ fixed. -## #17 — rzsv: Vorab-Check Timeout (kein App-Fehler, Screenshot-Beweis) -**Bild:** Tab rzsv (`administrator@sysdaemon.xyz:22`), Banner „Vorab-Check: Keine Antwort in 4 s", ssh wird korrekt NICHT gestartet. -**Messung (gleiche Maschine, 22.09.):** -- `sysdaemon.xyz` → `185.117.250.190` (auch `git.sysdaemon.xyz`) -- Port 443: OK in 0.02 s — Port 22: Timeout (DROP, kein Refused) -- Fazit: Server erreichbar, aber SSH-Port wird aus diesem Netz gedroppt. Von hier kann ssh in KEINEM Client gehen (Terminal eingeschlossen) — kein Terminal-/App-Problem, der Vorab-Check hatte recht. -**Wahrscheinlichste Ursachen (Reihenfolge):** -1. **fail2ban-Bann** nach den vielen `Permission denied`-Versuchen (`#13`-Ära): `fail2ban-client status sshd` → `unbanip ` -2. **VPN an/aus:** „Terminal geht" evtl. mit VPN (Split-DNS/Intern), App ohne (oder umgekehrt) — Zustände vergleichen -3. Firewall lässt 22 nur aus bestimmten Netzen zu (`ufw status`, `iptables -L`, `ss -tlnp | grep 22` auf dem Server) -**Gegenprobe (10 s, Terminal):** `ssh -v -o ConnectTimeout=8 administrator@sysdaemon.xyz` bleibt bei `Connecting...` hängen → bewiesen netzseitig. -**Status:** ⬜ Server-/Netz-Seite, warte auf Gegenprobe + VPN-Status. +## #8 — Terminal connects then dies instantly, no message (user bug report) +**Repro (harness, exact app path `ssh -p 1 nobody@127.0.0.1`):** exit 255, output shows only the session note — ssh's error lost. +**Ground truth (Python pty):** even `/bin/echo hello` yields `b''` when read only AFTER child exit; reading promptly yields `b'hello-py\r\n'` → macOS drops unread PTY master data on slave close (then EIO). Our `DispatchSourceRead` loses that race for instant-exit children — and ssh writes everything (incl. "Connection refused") to stderr, which shared the PTY. +**Fix:** stderr → `Pipe` (buffered data survives child exit), stdin/stdout stay on PTY; non-blocking `finalFlush` + epoch-guarded lifecycle; TCP pre-check (Network.framework, 4 s) with plain-language messages; red error banner + Diagnose/Retry buttons. +**Verified:** refused harness now shows the error (PASS); `cat` roundtrip, `ssh -G`, TCP harness (closed/open/empty) PASS. +**Status:** ✅ fixed. -## #18 — Host-Key-Yes/No unmöglich (User-Screenshot: Terminal geht, App sagt nur „Host key verification failed.") -**Analyse:** Terminal zeigt den `Are you sure...?`-Prompt (mit Fingerprint), App nur den Abbruch ohne Prompt. Beweis per `-v`: `read_passphrase: can't open /dev/tty: Device not configured` — ssh-Prompts brauchen ein Controlling-Terminal, unser PTY-Slave war keins (GUI-App hat keins, Kind erbt keins). Betrifft Passwort- UND Yes/No-Prompts. -**Fix:** -- `Sources/ctty-run/ctty-run.c` (C-Helper, SPM-Target): Double-Fork (nötig, weil Apples Process Kinder in eigener Prozessgruppe startet → direktes setsid = EPERM) → setsid → Slave-Open (= Ctty) → exec ssh. stderr bleibt auf der Eltern-Pipe (verlustfrei). `stop()` killt die Prozessgruppe (keine Waisen, verifiziert). -- `Views/HostKeySheet.swift` + Flow: Vorab per `ssh-keyscan` Fingerprint holen → Popup (SHA256/MD5, Typ) → Akzeptieren (→ `~/.ssh/known_hosts`, geteilt mit Terminal) / Abbrechen; bei GEÄNDERTEM Key rote MITM-Warnung + `ssh-keygen -R`-Pfad. -**Verifikation (lokaler paramiko-Server):** Bug reproduziert (kein Prompt, Exit 255) → nach Fix: Ctty per `ps` bewiesen (ttys001), Yes/No sichtbar + beantwortbar, Passwort akzeptiert, Shell aktiv. HostKeyService: unknown/accept/known/changed/revoke/unreachable grün. Regression (stop ohne Waisen) grün. -**Status:** ✅ behoben (v0.6.0) — Fingerprint im Popup mit Terminal-Ausgabe vergleichbar. +## #9 — Build: `Result` invalid +**Log:** `error: type 'String' does not conform to protocol 'Error'` (+ follow-on errors). +**Fix:** `TCPCheck.check` uses `(String?) -> Void` (nil = ok, else message). +**Status:** ✅ fixed. -## #19 — Fingerprint-Popup erscheint leer (User-Screenshot) -**Bild:** Sheet-Fenster da, aber winzig und ohne Inhalt; Status hängt auf „Prüfe …". -**Ursache:** `.sheet(isPresented:)` + `if let info` im Content: Nimmt die Auswertung den Empty-Zweig (Timing beim Präsentieren), hat der leere Zweig keine Abhängigkeit auf die State-Variable → Sheet aktualisiert sich nie, bleibt leer. Klassische SwiftUI-Falle. -**Fix:** `.sheet(item: $hostKeyRequest)` mit `HostKeyRequest: Identifiable` — Content existiert strukturell immer. Zusätzlich läuft `HostKeyService.status` (ssh-keyscan blockiert Sekunden) jetzt auf Hintergrund-Queue — vorher fror die UI während „Prüfe …" ein. -**Status:** ✅ behoben (v0.6.1). +## #10 — Icon generator build errors +**Log:** non-optional `guard let` (`??` already unwraps); `Int * CGFloat`; preview landed in `.iconset/` instead of `/tmp/`. +**Fix:** assign `??` fallback directly; `CGFloat(...)`; path corrected. +**Status:** ✅ fixed. Icon: `Resources/AppIcon.icns` (reproducible via `scripts/generate-icon.sh`), preview `docs/icon-preview.png`. + +## #11 — Store AND use passwords (user request) +**State:** passwords/passphrases were stored in Keychain but never used for login (ssh always asked interactively). +**Done:** `SSHService.loginSecret()` (profile password → key passphrase), pure/testable `sessionConfig()`: with secret + sshpass → `sshpass -e ssh/sftp …` + `SSHPASS` env (never command line/logs); otherwise direct call + interactive prompt. `PTYSession.customEnv` + auto-login flag; status shows auto-login state. Editor: saved indicator (✓), delete button, sshpass hint. +**Verified:** config harness 9/9 (incl. "password never on command line"); real Keychain roundtrip 7/7. +**Note:** without `sshpass` (`brew install sshpass`) the interactive prompt remains. +**Status:** ✅ done. + +## #12 — "Always connection wrong" (user report, details open) +**State:** reported without target host, banner text or credentials — not reproducible remotely. +**Checklist:** `user@host` in host field or `ssh://` prefix → live editor warnings now; whitespace trimmed; wrong password / missing key → banner + Diagnose; server blocks (fail2ban, key-only) → check server logs. +**Done:** `Models/ProfileValidation.swift` (7/7), password right below username, "Copy report" in banner (no secrets). +**Status:** ⬜ waiting for error report. + +## #13 — Permission denied with correct password (user error report) +**Report:** `administrator@sysdaemon.xyz:22`, ssh asks 3x → `Permission denied (publickey,password)`. Same password works in Terminal.app. +**Cause:** the terminal input was a stock TextField — macOS silently substitutes characters (`"` → “”, `--` → —, autocorrect). A password with special chars arrives mangled; Terminal shows nothing wrong, ssh just denies. Verified: stock `NSTextField` has these switches on, and can't even disable dash substitution (compiler proof). +**Fix (`Views/TerminalInputField.swift`, later superseded by SwiftTerm):** NSTextView-based field with all substitutions explicitly off; Secure variant via `NSSecureTextField`; Enter → send, paste normalization. +**Verified:** harness 9/9 (all flags off). +**Status:** ✅ fixed — retest with v0.5.1+. + +## #14 — Terminal.app logs in, RemminaMac doesn't (same password) +**User answers:** v0.5.1 installed, password login, Terminal.app works with the same data. +**Analysis:** app side fully verified → difference must be environmental. Main suspect: **SSH agent** — Finder-launched GUI processes don't inherit `SSH_AUTH_SOCK`. If Terminal silently uses an agent key (not the password), the app fails with the password. Verified: `launchctl getenv SSH_AUTH_SOCK` returns the socket. +**Fix:** `SSHService.agentSocket()` → `TerminalManager` injects `SSH_AUTH_SOCK` into the ssh env (coexists with SSHPASS). +**Verified:** harness 4/4. User confirmation check: `ssh-add -l` + `ssh -o IdentitiesOnly=yes administrator@sysdaemon.xyz`. +**Status:** ✅ app side done — waiting for confirmation. + +## #15 — Crash on type + send (user report) +**State:** "crashes as soon as I type and send." Not reproducible headless — every layer green alone. +**Ruled out by test:** SIGPIPE on write (macOS returns EIO, no signal; send-after-stop survives); view layer (real coordinator + NSTextView: umlauts/quotes/paste/Enter, 200 programmatic sets); PTY roundtrip, refused, Keychain, TCP harnesses. +**Hardened (v0.5.3, unproven as THE crash):** detach delegate during programmatic sets (no state mutation mid-render); safe pointer in `send()`. +**Still needed if it recurs:** Console.app → Crash Reports → newest `RemminaMac` entry → `Exception Type`, `Termination Reason`, crashed thread. +**Status:** ⬜ waiting for feedback / crash log. + +## #16 — Profile edits had no effect ("still doesn't work") +**State:** user kept reporting failure after fixes. Code review (`TerminalManager.session`): a once-created session was reused forever — host/password/key corrections never reached it. +**Fix:** config fingerprint (proto/host/port/user/extraArgs/key path/secret value) per session; on mismatch with a non-running session: stop + reconfigure (same object, tabs stay). Running sessions are never reconfigured underfoot (disconnect first). +**Verified:** harness 9/9. +**Status:** ✅ fixed — retest with v0.5.4 (re-save profile once). + +## #17 — rzsv: pre-check timeout (not an app bug, screenshot proof) +**Picture:** tab rzsv (`administrator@sysdaemon.xyz:22`), banner "pre-check: no answer in 4 s", ssh correctly NOT started. +**Measured (same machine):** `sysdaemon.xyz` → `185.117.250.190`; port 443 ok in 0.02 s, port 22 dropped (timeout, no refused). Server reachable, SSH port dropped from this network. No client can connect from here — the pre-check was right. +**Likely causes:** fail2ban ban after the many denied attempts (`fail2ban-client status sshd` → unban); VPN on/off mismatch; firewall allows 22 only from certain networks. +**Update from user:** port 22 is blocked on the company network; hotspot works. Confirmed. +**Status:** ⬜ server/network side. + +## #18 — Host-key yes/no impossible (user screenshot: Terminal works, app only says "Host key verification failed.") +**Analysis:** Terminal shows the `Are you sure...?` prompt (with fingerprint), app aborts without prompt. Proof via `-v`: `read_passphrase: can't open /dev/tty: Device not configured` — ssh prompts need a controlling terminal; our PTY slave wasn't one (GUI app has none, child inherits none). Affects password AND yes/no prompts. +**Fix:** `Sources/ctty-run/ctty-run.c` (C helper, SPM target): double-fork (needed because Apple's Process starts children in their own process group → direct setsid = EPERM) → setsid → slave open (= ctty) → exec ssh. stderr stays on the parent pipe (loss-free). `stop()` kills the process group (no orphans, verified). Plus `Views/HostKeySheet.swift` flow: `ssh-keyscan` fingerprint upfront → popup (SHA256/MD5, type) → accept (→ shared `~/.ssh/known_hosts`) / decline; red MITM warning + `ssh-keygen -R` path on changed keys. +**Verified (local paramiko server):** bug reproduced (no prompt, exit 255) → after fix: ctty proven via `ps` (ttys001), yes/no visible + answerable, password accepted, shell active. HostKeyService: unknown/accept/known/changed/revoke/unreachable green. +**Status:** ✅ fixed (v0.6.0). + +## #19 — Fingerprint popup appears empty (user screenshot) +**Picture:** sheet window present but tiny and contentless; status stuck on "checking …". +**Cause:** `.sheet(isPresented:)` + `if let info` content: if evaluation takes the empty branch at presentation time, the empty branch has no dependency on the state var → the sheet never updates, stays empty. Classic SwiftUI trap. +**Fix:** `.sheet(item: $hostKeyRequest)` with `HostKeyRequest: Identifiable` — content structurally always exists. Plus `HostKeyService.status` (blocking keyscan) moved to a background queue — the UI used to freeze during "checking …". +**Status:** ✅ fixed (v0.6.1). + +## #20 — Terminal not fully interactive (user request) +**State:** single-line input row, no Tab completion, no vim/full-screen apps. +**Done (0.7.0):** SwiftTerm emulator (v1.20.0 SPM dep) as `Views/SwiftTermView.swift`: cursor addressing, colors, scrollback, selection, direct keyboard (completion, arrows, Ctrl+C, Esc), vim/htop. Dracula palette via `installColors` + native fg/bg. PTY layer feeds raw bytes (`onData`); keystrokes go straight to the pty master (`sendBytes`); resizes propagate via `TIOCSWINSZ` (`setWinsize`). Old input row + `TerminalInputField.swift` removed (SwiftTerm handles keys natively, immune to macOS substitutions). +**Verified:** headless emulator core 6/6 (text, absolute cursor addressing, alt-screen on/off, clear, 16-color palette); app-path login + shell still green. +**Status:** ✅ done. + +## #21 — Asked for yes twice + password ignored (user reports) +**Analysis:** (a) empty-popup era (#19) forced ssh's own yes/no prompt — fixed by the popup fix; verified no re-ask after popup accept (harness asserts no "Are you sure" post-accept). (b) `sshpass` installs went stale: the session fingerprint didn't include sshpass presence, so installing sshpass later never switched to auto-login. Fixed by including it in `TerminalManager.configTag` (now `nonisolated` pure function). +**Verified:** no-ask + login green; tag changes on sshpass variance. +**Status:** ✅ fixed. diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 8e47fbd..31b8d46 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -1,51 +1,60 @@ # Installation -## Voraussetzungen +## Requirements - macOS 14+ -- Xcode 15+ (für GUI-Run) oder nur Swift-Toolchain 5.9+ (für CLI-Build) +- Xcode 15+ (for GUI runs) or Swift toolchain 5.9+ (for CLI builds) -## Variante A: Xcode (empfohlen für GUI) +## Option A: Xcode (recommended for GUI) 1. `open Package.swift` in `RemminaMac/` -2. Target `RemminaMac` wählen, Destination `My Mac` -3. `Cmd+R` → App startet -4. Erster Start legt an: +2. Select target `RemminaMac`, destination `My Mac` +3. `Cmd+R` → app launches +4. First launch creates: - `~/Library/Application Support/RemminaMac/profiles.json` - `~/Library/Application Support/RemminaMac/sshkeys.json` -## Variante B: Swift CLI +## Option B: Swift CLI ```bash cd RemminaMac swift build .build/debug/RemminaMac ``` -## Variante C: Release-Binary (ohne Xcode) +## Option C: Release binary (no Xcode) ```bash ./scripts/build-release.sh unzip dist/RemminaMac-*-macos-arm64.zip -d /tmp/rmm /tmp/rmm/RemminaMac ``` -## Variante D: Installer-DMG (Empfohlen für Endnutzer) -1. `RemminaMac-0.1.0-macos-arm64.dmg` aus dem Gitea-Release laden (oder lokal: `./scripts/build-app.sh 0.1.0 && ./scripts/build-dmg.sh 0.1.0`) -2. DMG doppelklicken → Finder-Fenster mit `RemminaMac.app` + `Programme`-Link -3. `RemminaMac.app` auf `Programme` ziehen → liegt in `/Applications/RemminaMac.app` -4. Auswerfen, aus Programme starten. Beim ersten Start: Rechtsklick → Öffnen (ad-hoc-Signatur, kein Apple-Notarization). +## Option D: Installer DMG (recommended for end users) +1. Download `RemminaMac--macos-arm64.dmg` from a Gitea release + (or build locally: `./scripts/build-app.sh && ./scripts/build-dmg.sh `) +2. Double-click the DMG → Finder window with `RemminaMac.app` + `Applications` link +3. Drag `RemminaMac.app` onto `Applications` → `/Applications/RemminaMac.app` +4. Eject, launch from Applications. First launch: right-click → Open + (ad-hoc signature, no Apple notarization). ```bash -# Prüfen: +# Verify: spctl -a -vv dist/RemminaMac.app -hdiutil verify dist/RemminaMac-0.1.0-macos-arm64.dmg +hdiutil verify dist/RemminaMac-*-macos-arm64.dmg ``` -## RDP-Voraussetzungen +## RDP requirements ```bash -# Für volle RDP-Unterstützung: +# For full RDP support: brew install freerdp which xfreerdp -# Ohne FreeRDP: Microsoft Remote Desktop aus dem App Store installieren +# Without FreeRDP: install Microsoft Remote Desktop from the App Store +``` + +## SSH auto-login (optional) +```bash +brew install sshpass +# Enables password auto-login from Keychain (no typing). +# Without sshpass: the password prompt appears inside the terminal tab. ``` ## Troubleshooting -- `swift build` schlägt fehl → `swift --version` prüfen (>= 5.9), Xcode Command Line Tools: `xcode-select --install` -- Keychain-Prompt → einmal erlauben, Service `RemminaMac` -- `xfreerdp` nicht gefunden → Fallback nutzt `rdc://`, braucht MS Remote Desktop +- `swift build` fails → check `swift --version` (>= 5.9), Xcode Command Line Tools: `xcode-select --install` +- Keychain prompt → allow once, service `RemminaMac` +- `xfreerdp` not found → fallback uses `rdc://`, needs MS Remote Desktop diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 653190f..d42bbc5 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -1,41 +1,47 @@ # Releasing -## Lokal bauen +## Build locally ```bash -./scripts/build-release.sh [version] # ZIP mit Binary + Docs -./scripts/build-app.sh [version] # RemminaMac.app (Bundle, ad-hoc-signiert, mit Icon) -./scripts/build-dmg.sh [version] # DMG mit App + Programme-Link -./scripts/generate-icon.sh # AppIcon.icns neu rendern (Dracula-Terminal) -# Beispiel: -./scripts/build-release.sh 0.1.0 -./scripts/build-app.sh 0.1.0 -./scripts/build-dmg.sh 0.1.0 +./scripts/build-release.sh [version] # ZIP with binary + docs +./scripts/build-app.sh [version] # RemminaMac.app (bundle, ad-hoc signed, icon + ctty-run) +./scripts/build-dmg.sh [version] # DMG with app + Applications link +./scripts/generate-icon.sh # re-render AppIcon.icns (Dracula terminal) +# Example: +./scripts/build-release.sh 0.7.0 +./scripts/build-app.sh 0.7.0 +./scripts/build-dmg.sh 0.7.0 # Output: -# dist/RemminaMac-0.1.0-macos-arm64.zip (+ .sha256) -# dist/RemminaMac.app/ -# dist/RemminaMac-0.1.0-macos-arm64.dmg (+ .sha256) +# dist/RemminaMac-0.7.0-macos-arm64.zip (+ .sha256) +# dist/RemminaMac.app/ (with ctty-run helper + icon) +# dist/RemminaMac-0.7.0-macos-arm64.dmg (+ .sha256) ``` -## Was das Skript macht -1. `swift build -c release` -2. Binary nach `dist/RemminaMac--macos-arm64/` kopieren -3. `README.md` + `docs/` + `LICENSE` dazulegen -4. ZIP + SHA256 erzeugen +Bump `AppInfo.version` in `Views/AboutView.swift` with each release. -## Gitea-Release erstellen +## What the scripts do +1. `swift build -c release` (builds both `RemminaMac` and `ctty-run`) +2. Copy binaries into `dist/RemminaMac--macos-arm64/` +3. Add `README.md` + `docs/` + `LICENSE` +4. ZIP + SHA256 + +`build-app.sh` also copies `AppIcon.icns` and `ctty-run` into the bundle and +signs ad-hoc (`codesign --deep`). `build-dmg.sh` creates the drag-and-drop DMG +via `hdiutil` (no extra tools). + +## Publish a Gitea release ```bash export GITEA_HOST="https://git.sysdaemon.xyz" export GITEA_OWNER="" export GITEA_REPO="remmina-mac" -export GITEA_TOKEN="" # statt Passwort: User → Settings → Applications → Token -export VERSION="0.1.0" +export GITEA_TOKEN="" # not password: user → Settings → Applications → token +export VERSION="0.7.0" ./scripts/publish-gitea-release.sh ``` -Das Skript erstellt (falls nötig) das Release `v0.1.0` und lädt ZIP + SHA256 als Attachments hoch. +The script creates the release (if needed) and uploads ZIP + SHA256 as attachments. -## Manuell via API +## Manual API ```bash curl -u "user:pass" -H "Content-Type: application/json" \ - -d '{"tag_name":"v0.1.0","name":"v0.1.0","body":"Erstes Release"}' \ + -d '{"tag_name":"v0.7.0","name":"v0.7.0","body":"..."}' \ "$GITEA_HOST/api/v1/repos/$OWNER/$REPO/releases" ``` diff --git a/docs/USAGE.md b/docs/USAGE.md index 460147b..7bb19c7 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -1,46 +1,46 @@ -# Benutzung +# Usage -## Layout (wie Remmina) -- **Links:** Gruppen-Dropdown, Suche, Profil-Liste. Rechtsklick: Verbinden / Duplizieren / Bearbeiten / Löschen. Doppelklick verbindet. -- **Rechts:** Tabs aller offenen Sitzungen + Verbindungsdetails (kopierbarer Befehl) + Hinweis-Box. -- **Unten:** Statusbar mit Profil-/Sitzungs-Zähler. -- **Toolbar:** `+` Neu, Stift Bearbeiten, Kopie-Symbol Duplizieren, Schlüssel Keys, `▶` Verbinden, `⚡` Quick-Connect. +## Layout (Remmina-style) +- **Left:** group dropdown, search, profile list. Right-click: Connect / Duplicate / Edit / Delete. Double-click connects. +- **Right:** tabs for open sessions + connection details (copyable command) + info box. +- **Bottom:** status bar with profile/session counts. +- **Toolbar:** `+` New, pencil Edit, copy Duplicate, key SSH keys, `ⓘ` About, `▶` Connect, `⚡` Quick-Connect. -## Profil anlegen -1. `+` → Name, Gruppe, Protokoll (SSH/SFTP/RDP/VNC), Host, Port, Benutzername, **Passwort direkt darunter** ausfüllen (Keychain-Haken setzen zum Speichern). -2. Der Editor warnt live bei typischen Fehlern: `user@host` im Host-Feld, `ssh://`-Präfix, Leerzeichen, leerer Benutzername, ungültiger Port. Beim Speichern wird getrimmt. -3. SSH-Key wählen (optional, Bereich darunter — vorher unter Schlüssel-Icon importieren). -4. Speichern → liegt sofort in `profiles.json`. -5. Schlägt eine Verbindung fehl: rotes Banner → **Bericht kopieren** und zur Fehlersuche mitschicken (enthält kein Passwort). +## Creating a profile +1. `+` → name, group, protocol (SSH/SFTP/RDP/VNC), host, port, username, **password right below** (check Keychain to save it). +2. The editor warns live about common mistakes: `user@host` in the host field, `ssh://` prefix, whitespace, empty username, invalid port. Values are trimmed on save. +3. Pick an SSH key (optional, section below — import it first via the key icon). +4. Save → stored immediately in `profiles.json`. +5. If a connection fails: red banner → **Copy report** and send it in for debugging (contains no password). -## Verbinden -- Profil wählen → `▶ Verbinden` oder Doppelklick. -- SSH/SFTP: **In-App-Terminal (Dracula-Theme) direkt im Tab** — erst TCP-Vorab-Check (falscher Port/Host → sofort klare Meldung statt kryptischem Exit-Code), dann Auto-Connect. Eingabezeile unten (Auge-Icon = Passwort-Modus), `^C`/`^D`-Buttons, `Leeren`, `Trennen`/`Verbinden`, `Tab schließen`. Bei Fehlschlag: rotes Banner mit letzter Meldung + Diagnose-Button. -- Alternativ: `In Terminal.app öffnen` (öffnet eine `.command`-Datei, braucht keine Automation-Berechtigung) oder `Befehl kopieren`. -- RDP: startet `xfreerdp` (falls vorhanden) oder öffnet MS Remote Desktop. -- VNC: öffnet `vnc://host:port`. +## Connecting +- Select a profile → `▶ Connect` or double-click. +- SSH/SFTP: **full terminal tab** (SwiftTerm, Dracula theme). Flow: TCP pre-check (wrong port/host → instant plain-language message instead of a cryptic exit code) → host-key fingerprint popup on first connect → auto-connect. Type directly in the terminal: Tab completion, arrows, Ctrl+C, copy/paste, scrollback, vim/htop all work. Buttons: Disconnect/Reconnect, Clear, Close tab. On failure: red banner with last message + Diagnose button. +- Alternative: `Open in Terminal.app` (opens a `.command` file, no automation permission needed) or `Copy command`. +- RDP: launches `xfreerdp` (if present) or opens Microsoft Remote Desktop. +- VNC: opens `vnc://host:port`. -## SSH-Keys verwalten (Schlüssel-Icon) -- `Import aus ~/.ssh`: übernimmt alle privaten Keys (ohne `*.pub`, `known_hosts`, `config`). -- `Wählen…`: Picker für Key-Datei. -- `Neuen Key generieren`: ruft `/usr/bin/ssh-keygen -t ed25519 -f ~/.ssh/remminamac_` auf. +## Managing SSH keys (key icon) +- `Import from ~/.ssh`: picks up all private keys (skips `*.pub`, `known_hosts`, `config`). +- `Choose…`: file picker for a key file. +- `Generate new key`: runs `/usr/bin/ssh-keygen -t ed25519 -f ~/.ssh/remminamac_`. - Passphrase → Keychain (`sshkey-`). -## Host-Key prüfen & akzeptieren (Popup) -- Bei Erstverbindung zeigt die App ein Popup mit SHA256-/MD5-Fingerprint (per `ssh-keyscan` geholt) — mit Terminal-Ausgabe oder Admin-Angabe vergleichen, dann Akzeptieren & Verbinden (landet in `~/.ssh/known_hosts`, gemeinsam mit Terminal.app). -- Bei GEÄNDERTEM Key: rote Warnung (MITM möglich!) — nur ersetzen, wenn erwartet (Server neu aufgesetzt). Zurücksetzen auch manuell: `ssh-keygen -R HOST`. -- Technik: `Sources/ctty-run/ctty-run.c` gibt ssh ein Controlling-Terminal (sonst scheitern alle interaktiven Prompts still). +## Host-key verification (popup) +- On first connect the app shows a popup with the SHA256/MD5 fingerprint (fetched via `ssh-keyscan`) — compare it with Terminal output or the server admin's value, then Accept & connect (stored in `~/.ssh/known_hosts`, shared with Terminal.app). +- On CHANGED key: red warning (possible MITM!) — only replace if expected (freshly provisioned server). Manual reset: `ssh-keygen -R HOST`. +- Tech note: `Sources/ctty-run/ctty-run.c` gives ssh a controlling terminal (without it all interactive prompts fail silently). -## Passwort speichern & Auto-Login -- Im Profil-Editor: Passwort eingeben + „in Keychain speichern“. Gespeichertes Passwort wird angezeigt (✓) und kann gelöscht werden. In `profiles.json` steht nur die Referenz, nie das Passwort. -- Reihenfolge beim Verbinden: Profil-Passwort zuerst, sonst Key-Passphrase. -- **Auto-Login ohne Tippen:** braucht `sshpass` (`brew install sshpass`). Dann loggt ssh via Keychain-Secret ein (Passwort nur im Prozess-Env `SSHPASS`, nie in Befehlszeile/Logs). Status zeigt „Auto-Login via Keychain". -- **Ohne sshpass:** Passwort/Key-Passphrase wird interaktiv im Terminal abgefragt (Passwort-Modus: Auge-Icon). Bei Sonderzeichen im Passwort immer Auge-Modus nutzen (keine Text-Ersetzung). +## Saving passwords & auto-login +- Profile editor: enter password + check Keychain. A saved password is indicated (✓) and can be deleted. `profiles.json` holds only the reference, never the password. +- Order at connect time: profile password first, key passphrase second. +- **Auto-login without typing:** needs `sshpass` (`brew install sshpass`). ssh logs in with the Keychain secret (password only in the `SSHPASS` process env, never on the command line or in logs). Status shows auto-login state. +- **Without sshpass:** password/key passphrase is asked interactively in the terminal. ## Quick-Connect -- Mit Auswahl: verbindet direkt. Ohne Auswahl: öffnet Editor mit „Quick-Connect“-Vorlage. +- With selection: connects directly. Without: opens the editor with a “Quick-Connect” template. -## Export / Import (manuell, bis GUI-Buttons kommen) +## Export / Import (manual until GUI buttons land) ```bash PROFILE_DIR=~/Library/Application\ Support/RemminaMac cp "$PROFILE_DIR/profiles.json" ~/Desktop/remminamac-backup.json