2 Commits
16 changed files with 624 additions and 679 deletions
+23
View File
@@ -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
}
+4
View File
@@ -10,9 +10,13 @@ let package = Package(
.executable(name: "RemminaMac", targets: ["RemminaMac"]), .executable(name: "RemminaMac", targets: ["RemminaMac"]),
.executable(name: "ctty-run", targets: ["ctty-run"]) .executable(name: "ctty-run", targets: ["ctty-run"])
], ],
dependencies: [
.package(url: "https://github.com/migueldeicaza/SwiftTerm", from: "1.20.0")
],
targets: [ targets: [
.executableTarget( .executableTarget(
name: "RemminaMac", name: "RemminaMac",
dependencies: ["SwiftTerm"],
path: "Sources/RemminaMac" path: "Sources/RemminaMac"
), ),
.executableTarget( .executableTarget(
+69 -61
View File
@@ -1,63 +1,69 @@
# RemminaMac — SSH / RDP Client für macOS # RemminaMac — SSH / RDP Client for macOS
![RemminaMac Icon](docs/icon-preview.png) ![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) ![Platform](https://img.shields.io/badge/platform-macOS%2014%2B-blue)
![Sprache](https://img.shields.io/badge/sprache-Swift%205.9-orange) ![Language](https://img.shields.io/badge/language-Swift%205.9-orange)
![UI](https://img.shields.io/badge/UI-SwiftUI-green) ![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) - [Features](#features)
- [Screenshots / Layout](#screenshots--layout) - [Layout](#layout)
- [Schnellstart](#schnellstart) - [Quick start](#quick-start)
- [Installation](docs/INSTALL.md) - [Installation](docs/INSTALL.md)
- [Benutzung](docs/USAGE.md) - [Usage](docs/USAGE.md)
- [Architektur](docs/ARCHITECTURE.md) - [Architecture](docs/ARCHITECTURE.md)
- [SSH-Keys & Sicherheit](#ssh-keys--sicherheit) - [SSH keys & security](#ssh-keys--security)
- [RDP / VNC unter macOS](#rdp--vnc-unter-macos) - [RDP / VNC on macOS](#rdp--vnc-on-macos)
- [Profile-Format](#profile-format) - [Profile format](#profile-format)
- [Release-Binary](#release-binary) - [Release binaries](#release-binaries)
- [Fehlerprotokoll](docs/ERRORS.md) - [Error log](docs/ERRORS.md)
- [Roadmap](#roadmap) - [Roadmap](#roadmap)
- [Lizenz](#lizenz) - [License](#license)
## Features ## Features
- GUI im Remmina-Layout (`Views/ContentView.swift`): - GUI in Remmina layout (`Views/ContentView.swift`):
- Sidebar: Gruppen-Picker, Suche, Profil-Liste, Doppelklick = Verbinden - Sidebar: group picker, search, profile list, double-click = connect
- Detail: Tab-Ansicht offener Sitzungen, Verbindungsdetails, Statusbar - Detail: tabbed open sessions, connection details, status bar
- Toolbar: Neu / Bearbeiten / Duplizieren / Verbinden / Quick-Connect / SSH-Keys - Toolbar: New / Edit / Duplicate / Connect / Quick-Connect / SSH keys / Info
- Profile laden / speichern / duplizieren / löschen - Load / save / duplicate / delete profiles
- JSON in `~/Library/Application Support/RemminaMac/profiles.json` - JSON in `~/Library/Application Support/RemminaMac/profiles.json`
- Export / Import (siehe Benutzung) - Validation + trimming in the editor, copyable error reports
- Protokolle: SSH, SFTP, RDP, VNC (`Models/ConnectionProfile.swift:3`) - Protocols: SSH, SFTP, RDP, VNC (`Models/ConnectionProfile.swift`)
- SSH-Keys speichern & verwalten (`Views/SSHKeysView.swift`): - Full interactive terminal per tab (SwiftTerm emulator, Dracula themed):
- Import aus `~/.ssh`, Datei-Picker, `ssh-keygen`-Wrapper (ed25519) cursor addressing, colors, scrollback, Tab completion, arrows, Ctrl+C,
- Passphrases in macOS Keychain (`Security/KeychainHelper.swift`) full-screen apps (vim/htop). Real PTY + controlling terminal via `ctty-run`.
- Passwörter in Keychain, nie im Klartext-JSON - Host-key fingerprint popup (accept/decline, MITM warning on change),
- SSH via System-`ssh` in Terminal.app (`Services/ConnectionServices.swift:6`) shared `~/.ssh/known_hosts` with Terminal.app
- RDP via FreeRDP (`xfreerdp`) oder Fallback Microsoft Remote Desktop - SSH key management (`Views/SSHKeysView.swift`):
- VNC via `vnc://`-Scheme 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 | | Group | Tab: Webserver | Office-PC | ... | Detail |
| Suche +-------------------------------+ | | Search +-------------------------------+ |
| - Webserver | Verbindungsdetails (Befehl)| | | - Webserver | Full terminal (SwiftTerm) | |
| - Büro-PC | Hinweis | | | - Office-PC | Connection details (cmd) | |
| - Dateiserver+-------------------------------+ | | - Fileserver +-------------------------------+ |
+------+-------------------------------+--------------------+ +------+-------------------------------+--------------------+
| Statusbar: ● Bereit ... N Profile · M Sitzungen | | Statusbar: ● Ready ... N profiles · M sessions |
+-------------------------------------------------------------+ +-------------------------------------------------------------+
``` ```
## Schnellstart ## Quick start
```bash ```bash
cd RemminaMac cd RemminaMac
swift build swift build
@@ -67,19 +73,21 @@ open Package.swift
# Target RemminaMac → My Mac → Run # Target RemminaMac → My Mac → Run
``` ```
## SSH-Keys & Sicherheit ## SSH keys & security
- Private-Key-Pfade stehen in `sshkeys.json`, Passphrases nur in Keychain. - Private key paths live in `sshkeys.json`, passphrases in Keychain only.
- Service-Name: `RemminaMac`, Accounts: `profile-<UUID>`, `sshkey-<UUID>`. - Service name: `RemminaMac`, accounts: `profile-<UUID>`, `sshkey-<UUID>`.
- Keychain lesen/schreiben: `Security/KeychainHelper.swift:5`. - Keychain access: `Security/KeychainHelper.swift`.
- Es werden keine Secrets ins Git-Repo geschrieben (siehe `.gitignore`). - 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` - Optional: `brew install freerdp`
- Mit FreeRDP: `xfreerdp /v:HOST:PORT /u:USER [/size:WxH | /f]` - With FreeRDP: `xfreerdp /v:HOST:PORT /u:USER [/size:WxH | /f]`
- Ohne: `rdc://` → Microsoft Remote Desktop (aus App Store installieren) - Without: `rdc://` → Microsoft Remote Desktop (install from App Store)
- VNC: `open vnc://host:port` - VNC: `open vnc://host:port`
## Profile-Format ## Profile format
```json ```json
[ [
{ {
@@ -91,7 +99,7 @@ open Package.swift
"username": "admin", "username": "admin",
"group": "Server", "group": "Server",
"colorTag": "blue", "colorTag": "blue",
"resolution": "Automatisch", "resolution": "Automatic",
"sshKeyID": null, "sshKeyID": null,
"passwordAccount": "profile-UUID", "passwordAccount": "profile-UUID",
"extraArgs": "", "extraArgs": "",
@@ -100,12 +108,12 @@ open Package.swift
] ]
``` ```
## Release-Binary ## Release binaries
Fertige Binaries liegen unter Git-Releases (`dist/` wird lokal gebaut, nicht committet): Prebuilt binaries on Git releases (`dist/` is built locally, never committed):
```bash ```bash
./scripts/build-release.sh ./scripts/build-release.sh 0.7.0
./scripts/build-app.sh # RemminaMac.app für /Applications ./scripts/build-app.sh 0.7.0 # RemminaMac.app for /Applications
./scripts/build-dmg.sh # DMG mit Drag-and-drop auf Programme ./scripts/build-dmg.sh 0.7.0 # DMG with drag-and-drop to Applications
# → dist/RemminaMac-<version>-macos-arm64.zip + .sha256 # → dist/RemminaMac-<version>-macos-arm64.zip + .sha256
# → dist/RemminaMac-<version>-macos-arm64.dmg + .sha256 # → dist/RemminaMac-<version>-macos-arm64.dmg + .sha256
ls dist/ ls dist/
@@ -113,11 +121,11 @@ ls dist/
Details: [docs/INSTALL.md](docs/INSTALL.md), [docs/RELEASING.md](docs/RELEASING.md). Details: [docs/INSTALL.md](docs/INSTALL.md), [docs/RELEASING.md](docs/RELEASING.md).
## Roadmap ## Roadmap
- [ ] Eingebettetes Terminal (SwiftTerm + swift-nio-ssh) - [x] Embedded full terminal (SwiftTerm) — done in 0.7.0
- [ ] Echte RDP-Einbettung (FreeRDP-Bridge) statt externem Prozess - [ ] Embedded RDP (FreeRDP bridge) instead of external process
- [ ] SFTP-Browser integriert - [ ] Integrated SFTP browser
- [ ] Export/Import-Buttons in der GUI - [ ] Export/Import buttons in the GUI
- [ ] Tray / Favoriten / Ordner-Badges wie Remmina - [ ] Tray / favorites / folder badges like Remmina
## Lizenz ## License
MIT — siehe `LICENSE`. MIT — see `LICENSE`.
+40 -11
View File
@@ -33,6 +33,9 @@ final class PTYSession: ObservableObject {
private var readSource: DispatchSourceRead? private var readSource: DispatchSourceRead?
private var errPipe: Pipe? private var errPipe: Pipe?
private let ioQueue = DispatchQueue(label: "RemminaMac.pty") 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 parser = AnsiParser()
private var finished = false private var finished = false
private var epoch = 0 // nur Main-Thread private var epoch = 0 // nur Main-Thread
@@ -132,6 +135,7 @@ final class PTYSession: ObservableObject {
h.readabilityHandler = nil // EOF h.readabilityHandler = nil // EOF
return return
} }
self.onData?(data)
self.ioQueue.async { [weak self] in self.ioQueue.async { [weak self] in
guard let self else { return } guard let self else { return }
let events = self.parser.feed(data) let events = self.parser.feed(data)
@@ -152,7 +156,9 @@ final class PTYSession: ObservableObject {
var buf = [UInt8](repeating: 0, count: 8192) var buf = [UInt8](repeating: 0, count: 8192)
let n = read(fd, &buf, buf.count) let n = read(fd, &buf, buf.count)
if n > 0 { if n > 0 {
let events = parser.feed(Data(buf[..<n])) let data = Data(buf[..<n])
onData?(data)
let events = parser.feed(data)
DispatchQueue.main.async { [weak self] in self?.apply(events) } DispatchQueue.main.async { [weak self] in self?.apply(events) }
} else { } else {
readSource?.cancel() // EOF/EIO — Rest holt finalFlush bzw. ist verworfen readSource?.cancel() // EOF/EIO — Rest holt finalFlush bzw. ist verworfen
@@ -217,9 +223,14 @@ final class PTYSession: ObservableObject {
} }
func send(_ string: String) { func send(_ string: String) {
guard let data = string.data(using: .utf8) else { return }
sendBytes(data)
}
/// Raw bytes from the terminal emulator (keyboard) to the pty master.
func sendBytes(_ data: Data) {
let fd = masterFD let fd = masterFD
guard fd >= 0 else { return } guard fd >= 0, !data.isEmpty else { return }
guard let data = string.data(using: .utf8), !data.isEmpty else { return }
ioQueue.async { ioQueue.async {
data.withUnsafeBytes { ptr in data.withUnsafeBytes { ptr in
guard let base = ptr.baseAddress else { return } 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() { func clear() {
lines = [[]] lines = [[]]
} }
@@ -324,27 +345,35 @@ final class TerminalManager: ObservableObject {
private var sessions: [UUID: PTYSession] = [:] private var sessions: [UUID: PTYSession] = [:]
func session(for profile: ConnectionProfile, key: StoredSSHKey?) -> PTYSession { func session(for profile: ConnectionProfile, key: StoredSSHKey?) -> PTYSession {
// Fingerprint aus allem, was die Verbindung bestimmt (inkl. Secret-Wert!): // Fingerprint aus allem, was die Verbindung bestimmt (inkl. Secret-Wert
// Ändert der User Host/Passwort/Key, wird neu konfiguriert statt stale weiterzuwählen. // 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 secret = SSHService.loginSecret(for: profile, key: key)
let tag = [profile.proto.rawValue, profile.host, "\(profile.port)", profile.username, let sshpass = SSHService.findSshpass()
profile.extraArgs, key?.privateKeyPath ?? "", secret ?? ""].joined(separator: "|") let tag = Self.configTag(profile: profile, keyPath: key?.privateKeyPath,
secret: secret, sshpass: sshpass)
if let existing = sessions[profile.id] { if let existing = sessions[profile.id] {
// Laufende Session niemals unter den Füßen wegkonfigurieren — erst Trennen. // Laufende Session niemals unter den Füßen wegkonfigurieren — erst Trennen.
if existing.isRunning || existing.configTag == tag { return existing } if existing.isRunning || existing.configTag == tag { return existing }
existing.stop() existing.stop()
configure(existing, profile: profile, key: key, secret: secret) configure(existing, profile: profile, key: key, secret: secret, sshpass: sshpass)
existing.configTag = tag existing.configTag = tag
return existing return existing
} }
let s = PTYSession() let s = PTYSession()
configure(s, profile: profile, key: key, secret: secret) configure(s, profile: profile, key: key, secret: secret, sshpass: sshpass)
s.configTag = tag s.configTag = tag
sessions[profile.id] = s sessions[profile.id] = s
return 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 baseExe: String
let baseArgs: [String] let baseArgs: [String]
if profile.proto == .sftp { if profile.proto == .sftp {
@@ -358,7 +387,7 @@ final class TerminalManager: ObservableObject {
// SSH-Agent durchreichen (falls vorhanden): gleiche Keys wie in Terminal.app. // SSH-Agent durchreichen (falls vorhanden): gleiche Keys wie in Terminal.app.
let cfg = SSHService.sessionConfig(executable: baseExe, args: baseArgs, let cfg = SSHService.sessionConfig(executable: baseExe, args: baseArgs,
password: secret, password: secret,
sshpassPath: SSHService.findSshpass()) sshpassPath: sshpass)
s.configure(executable: cfg.executable, arguments: cfg.arguments) s.configure(executable: cfg.executable, arguments: cfg.arguments)
var env = cfg.extraEnv var env = cfg.extraEnv
if let sock = SSHService.agentSocket() { if let sock = SSHService.agentSocket() {
+31 -23
View File
@@ -1,25 +1,26 @@
import SwiftUI import SwiftUI
import AppKit import AppKit
import SwiftTerm
/// Offizielle Dracula-Palette (https://draculatheme.com/contribute). /// Offizielle Dracula-Palette (https://draculatheme.com/contribute).
enum Dracula { enum Dracula {
static let background = Color(hex: "282A36") static let background = SwiftUI.Color(hex: "282A36")
static let currentLine = Color(hex: "44475A") static let currentLine = SwiftUI.Color(hex: "44475A")
static let foreground = Color(hex: "F8F8F2") static let foreground = SwiftUI.Color(hex: "F8F8F2")
static let comment = Color(hex: "6272A4") static let comment = SwiftUI.Color(hex: "6272A4")
static let cyan = Color(hex: "8BE9FD") static let cyan = SwiftUI.Color(hex: "8BE9FD")
static let green = Color(hex: "50FA7B") static let green = SwiftUI.Color(hex: "50FA7B")
static let orange = Color(hex: "FFB86C") static let orange = SwiftUI.Color(hex: "FFB86C")
static let pink = Color(hex: "FF79C6") static let pink = SwiftUI.Color(hex: "FF79C6")
static let purple = Color(hex: "BD93F9") static let purple = SwiftUI.Color(hex: "BD93F9")
static let red = Color(hex: "FF5555") static let red = SwiftUI.Color(hex: "FF5555")
static let yellow = Color(hex: "F1FA8C") static let yellow = SwiftUI.Color(hex: "F1FA8C")
/// ANSI-Index 0-15 -> Dracula-Farbe. /// 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 } guard let index else { return foreground }
switch index { switch index {
case 0: return Color(hex: "21222C") case 0: return SwiftUI.Color(hex: "21222C")
case 1: return red case 1: return red
case 2: return green case 2: return green
case 3: return yellow case 3: return yellow
@@ -28,13 +29,13 @@ enum Dracula {
case 6: return cyan case 6: return cyan
case 7: return foreground case 7: return foreground
case 8: return comment case 8: return comment
case 9: return Color(hex: "FF6E6E") case 9: return SwiftUI.Color(hex: "FF6E6E")
case 10: return Color(hex: "69FF94") case 10: return SwiftUI.Color(hex: "69FF94")
case 11: return Color(hex: "FFFFA5") case 11: return SwiftUI.Color(hex: "FFFFA5")
case 12: return Color(hex: "D6ACFF") case 12: return SwiftUI.Color(hex: "D6ACFF")
case 13: return Color(hex: "FF92DF") case 13: return SwiftUI.Color(hex: "FF92DF")
case 14: return Color(hex: "A4FFFF") case 14: return SwiftUI.Color(hex: "A4FFFF")
default: return Color(hex: "FFFFFF") default: return SwiftUI.Color(hex: "FFFFFF")
} }
} }
@@ -42,11 +43,18 @@ enum Dracula {
NSColor(color(for: index)) NSColor(color(for: index))
} }
static let nsBackground = NSColor(Color(hex: "282A36")) static let nsBackground = NSColor(SwiftUI.Color(hex: "282A36"))
static let nsSelection = NSColor(Color(hex: "44475A")) 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) { init(hex: String) {
let clean = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) let clean = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var value: UInt64 = 0 var value: UInt64 = 0
+45
View File
@@ -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)
}
}
@@ -10,6 +10,7 @@ struct ContentView: View {
@State private var showingEditor = false @State private var showingEditor = false
@State private var editingProfile: ConnectionProfile? @State private var editingProfile: ConnectionProfile?
@State private var showingKeys = false @State private var showingKeys = false
@State private var showingAbout = false
@State private var statusMessage = "Bereit." @State private var statusMessage = "Bereit."
var body: some View { var body: some View {
@@ -59,6 +60,8 @@ struct ContentView: View {
.help("Duplizieren").disabled(selection == nil) .help("Duplizieren").disabled(selection == nil)
Button(action: { showingKeys = true }) { Image(systemName: "key") } Button(action: { showingKeys = true }) { Image(systemName: "key") }
.help("SSH-Keys verwalten") .help("SSH-Keys verwalten")
Button(action: { showingAbout = true }) { Image(systemName: "info.circle") }
.help("Info")
} }
} }
} detail: { } detail: {
@@ -116,6 +119,9 @@ struct ContentView: View {
.sheet(isPresented: $showingKeys) { .sheet(isPresented: $showingKeys) {
SSHKeysView().environmentObject(store) SSHKeysView().environmentObject(store)
} }
.sheet(isPresented: $showingAbout) {
AboutView()
}
} }
var selectedProfile: ConnectionProfile? { var selectedProfile: ConnectionProfile? {
@@ -1,19 +1,16 @@
import SwiftUI import SwiftUI
import AppKit import AppKit
/// In-App-Terminal im Dracula-Look: Ausgabe (NSTextView, read-only) + Eingabezeile. /// Full interactive terminal (SwiftTerm emulator, Dracula themed):
/// Verbindet erst nach TCP-Vorab-Check (falscher Port/Host -> sofort klare Meldung). /// cursor addressing, colors, scrollback, Tab completion, arrows, Ctrl+C,
/// Zeilenumbruch ist an, Autoscroll ans Ende, Passwort-Modus via Auge-Icon. /// full-screen apps (vim/htop). Keyboard goes straight to the pty.
/// Connects after TCP pre-check + host-key approval (popup).
struct EmbeddedTerminalView: View { struct EmbeddedTerminalView: View {
@ObservedObject var term: PTYSession @ObservedObject var term: PTYSession
let profile: ConnectionProfile let profile: ConnectionProfile
var canConnect: Bool var canConnect: Bool
@State private var input = ""
@State private var secureInput = false
@State private var checking = false @State private var checking = false
@State private var hostKeyInfo: HostKeyInfo? @State private var hostKeyRequest: HostKeyRequest?
@State private var hostKeyChange = false
@State private var showingHostKey = false
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
@@ -61,37 +58,9 @@ struct EmbeddedTerminalView: View {
.overlay(Rectangle().fill(Dracula.red.opacity(0.4)).frame(height: 1), alignment: .bottom) .overlay(Rectangle().fill(Dracula.red.opacity(0.4)).frame(height: 1), alignment: .bottom)
} }
// Ausgabe im Dracula-Theme // Full terminal emulator (click to focus, type directly).
DraculaTextView(lines: term.lines) SwiftTermView(term: term)
.frame(maxWidth: .infinity, maxHeight: .infinity) .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) .background(Dracula.background)
.cornerRadius(10) .cornerRadius(10)
@@ -101,17 +70,14 @@ struct EmbeddedTerminalView: View {
connect() // Auto-Connect beim Öffnen des Tabs connect() // Auto-Connect beim Öffnen des Tabs
} }
} }
.sheet(isPresented: $showingHostKey) { .sheet(item: $hostKeyRequest) { req in
if let info = hostKeyInfo { HostKeySheet(info: req.info, isChange: req.isChange,
HostKeySheet(info: info, isChange: hostKeyChange, onAccept: { acceptHostKey(req) },
onAccept: acceptHostKey, onCancel: {
onCancel: { hostKeyRequest = nil
showingHostKey = false checking = false
checking = false term.fail("Host-Key nicht akzeptiert — Verbindung abgebrochen.")
hostKeyInfo = nil })
term.fail("Host-Key nicht akzeptiert — Verbindung abgebrochen.")
})
}
} }
} }
@@ -125,40 +91,41 @@ struct EmbeddedTerminalView: View {
term.fail("Vorab-Check: \(message)") term.fail("Vorab-Check: \(message)")
return return
} }
// Host-Key vorab per keyscan prüfen (Yes/No-Prompt käme ohne Ctty nicht an) // Host-Key vorab per keyscan prüfen (Yes/No-Prompt käme ohne Ctty nicht an).
let hk = HostKeyService.status(host: profile.host, port: profile.port) // Läuft im Hintergrund: ssh-keyscan blockiert Sekunden (kein UI-Freeze).
switch hk { let host = profile.host
case .known, .unreachable: let port = profile.port
checking = false DispatchQueue.global(qos: .userInitiated).async {
term.start() let hk = HostKeyService.status(host: host, port: port)
case .unknown(let info): DispatchQueue.main.async {
hostKeyInfo = info switch hk {
hostKeyChange = false case .known, .unreachable:
showingHostKey = true checking = false
case .changed(let info): term.start()
hostKeyInfo = info case .unknown(let info):
hostKeyChange = true hostKeyRequest = HostKeyRequest(info: info, isChange: false)
showingHostKey = true case .changed(let info):
hostKeyRequest = HostKeyRequest(info: info, isChange: true)
}
}
} }
} }
} }
} }
func acceptHostKey() { func acceptHostKey(_ req: HostKeyRequest) {
guard let info = hostKeyInfo else { return } hostKeyRequest = nil
showingHostKey = false if req.isChange {
if hostKeyChange {
_ = HostKeyService.revoke(host: profile.host, port: profile.port) _ = HostKeyService.revoke(host: profile.host, port: profile.port)
} }
if HostKeyService.accept(info) { if HostKeyService.accept(req.info) {
checking = false checking = false
term.note("Host-Key akzeptiert (\(info.keyType), \(info.fingerprintSHA256.prefix(19))…).") term.note("Host-Key akzeptiert (\(req.info.keyType), \(req.info.fingerprintSHA256.prefix(19))…).")
term.start() term.start()
} else { } else {
checking = false checking = false
term.fail("Host-Key konnte nicht in known_hosts gespeichert werden.") term.fail("Host-Key konnte nicht in known_hosts gespeichert werden.")
} }
hostKeyInfo = nil
} }
func diagnose() { func diagnose() {
@@ -177,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). /// 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() { func copyReport() {
var r = "RemminaMac Fehlerbericht\n" var r = "RemminaMac Fehlerbericht\n"
r += "Profil: \(profile.name) | \(profile.proto.rawValue) | \(profile.username)@\(profile.host):\(profile.port)\n" r += "Profil: \(profile.name) | \(profile.proto.rawValue) | \(profile.username)@\(profile.host):\(profile.port)\n"
@@ -238,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)
}
}
@@ -1,5 +1,13 @@
import SwiftUI import SwiftUI
/// Sheet-Request (Identifiable): .sheet(item:) rendert IMMER mit Wert —
/// ein `if let` im Sheet kann den Empty-Zweig nehmen und bleibt dann leer.
struct HostKeyRequest: Identifiable {
let id = UUID()
var info: HostKeyInfo
var isChange: Bool
}
/// Popup bei unbekanntem/geändertem Host-Key: Fingerprint prüfen, dann /// Popup bei unbekanntem/geändertem Host-Key: Fingerprint prüfen, dann
/// akzeptieren (→ known_hosts + verbinden) oder abbrechen. /// akzeptieren (→ known_hosts + verbinden) oder abbrechen.
struct HostKeySheet: View { struct HostKeySheet: View {
@@ -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<UInt8>) {
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) {}
}
}
@@ -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<String>
var onSubmit: () -> Void
init(text: Binding<String>, 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
}
}
}
+54 -26
View File
@@ -1,33 +1,61 @@
# Architektur # Architecture
``` ```
Sources/RemminaMac/ Sources/RemminaMac/
RemminaMacApp.swift # @main, WindowGroup, lädt ProfileStore RemminaMacApp.swift # @main, WindowGroup, loads ProfileStore
Models/ConnectionProfile.swift # ProtocolType + ConnectionProfile + StoredSSHKey (Codable) Models/ConnectionProfile.swift # ProtocolType + ConnectionProfile + StoredSSHKey (Codable)
Stores/ProfileStore.swift # ObservableObject, JSON-Persistenz, Filter/Suche/Gruppen Models/ProfileValidation.swift # input checks + trimming (pure, tested)
Security/KeychainHelper.swift# SecItemAdd/Copy/Delete Wrapper Stores/ProfileStore.swift # ObservableObject, JSON persistence, filter/search/groups
Services/ConnectionServices.swift # SSHService (ssh-Befehl) + RDPService (freerdp/rdc/vnc) Security/KeychainHelper.swift # SecItemAdd/Copy/Delete wrapper
Views/ContentView.swift # NavigationSplitView: Sidebar + Tabs + Statusbar Services/ConnectionServices.swift # SSHService (ssh args, secrets, sshpass, agent) + RDPService
Views/SessionView.swift # Detail pro Sitzung Services/PTYSession.swift # real PTY session (posix_openpt), stderr pipe, TerminalManager
Views/ProfileEditorView.swift# Formular Neu/Bearbeiten + Keychain-Anbindung Services/AnsiParser.swift # ANSI/SGR parser for diagnostics buffer (pure, tested)
Views/SSHKeysView.swift # Key-Liste, Import, ssh-keygen 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 ## Data flow
1. `RemminaMacApp` erstellt `ProfileStore` als `@StateObject`, injiziert als `environmentObject`. 1. `RemminaMacApp` creates `ProfileStore` as `@StateObject`, injects as `environmentObject`.
2. `ProfileStore.load()` liest `profiles.json` + `sshkeys.json`, sonst Sample-Daten. 2. `ProfileStore.load()` reads `profiles.json` + `sshkeys.json`, else sample data.
3. `ContentView` filtert via `searchText`/`selectedGroup`, `selection` steuert Detail. 3. `ContentView` filters via `searchText`/`selectedGroup`; `selection` drives detail.
4. `connect(_:)` setzt `lastConnected`, öffnet Tab, delegiert an `SSHService`/`RDPService`. 4. `connect(_:)` stamps `lastConnected`, opens a tab. The terminal view runs:
5. Secrets nur via `KeychainHelper`, JSON enthält nur Account-Referenzen. 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 ## Key files
- `Stores/ProfileStore.swift:13` — Pfade + `load()`/`save()` - `Stores/ProfileStore.swift` — paths + `load()`/`save()`
- `Models/ConnectionProfile.swift:3` — Protokoll-Defaults (Ports, Icons) - `Models/ConnectionProfile.swift` — protocol defaults (ports, icons)
- `Services/ConnectionServices.swift:6` — `sshArguments(for:key:)` - `Services/PTYSession.swift` — spawn, lifecycle, `TerminalManager`
- `Views/ContentView.swift` — Remmina-Layout - `Services/HostKeyService.swift` — fingerprint flow
- `Views/SwiftTermView.swift` — emulator wiring + Dracula theme
- `Views/ContentView.swift` — Remmina layout
## Nächste Ausbaustufen ## Verification strategy
1. SwiftTerm-View statt externem Terminal + `swift-nio-ssh` für echte SSH-Sessions. Logic is verified with throwaway `swiftc` harnesses in `/tmp` (never committed):
2. FreeRDP als XPC-Service einbetten statt `Process`. ANSI parser, PTY roundtrips, real `ssh` runs, Keychain roundtrips, TCP checks,
3. SFTP-Browser (NIO SFTP oder libssh2-Bridge). host-key flow against a local paramiko test server, SwiftTerm core headless
4. Export/Import-Buttons + Drag&Drop für Keys. (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.
+150 -220
View File
@@ -1,243 +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 ## Overview
| # | Zeitpunkt | Phase | Fehler | Schwere | Status | | # | Date | Phase | Error | Severity | Status |
|---|-----------|-------|--------|---------|--------| |---|------|-------|-------|----------|--------|
| 1 | 2026-09-21 | `swift build` (debug) | `WindowGroup<some View> has no member 'windowTitle'` | blockierend | ✅ behoben | | 1 | 2026-09-21 | `swift build` (debug) | `WindowGroup<some View> has no member 'windowTitle'` | blocking | ✅ fixed |
| 2 | 2026-09-21 | `swift build -c release` | keine Fehler, nur lange Link-Zeit (~40 s) | Info | ✅ ok | | 2 | 2026-09-21 | `swift build -c release` | no errors, just slow first link (~40 s) | info | ✅ ok |
| 3 | 2026-09-21 | Release-Skript | `dist/` war nicht in `.gitignore` → Binary drohte committet zu werden | mittel | ✅ behoben (ignoriert) | | 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 invalid username, password or token` mit alten Credentials | blockierend (Push/Release) | ✅ behoben — neue Credentials ok, Push + Release v0.1.0 hochgeladen | | 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 | keine Fehler: `build-app.sh` (Signatur ok), `build-dmg.sh` (UDZO, 200K), Mount-Verifikation ok (App + Applications/Programme-Links) | Info | ✅ ok | | 5 | 2026-09-21 | app bundle + DMG | no errors: signing ok, UDZO DMG, mount verified | 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 | | 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()` flusht Chunk erst NACH `parseCSI` → Text bekam den neuen statt alten Stil (`rot` mit fg=nil statt 1) | mittel | ✅ behoben — Flush vor Stilwechsel | | 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 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 | | 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<Void, String>`: String conformt nicht zu Error (Folgefehler: DispatchQueue-Overload) | blockierend | ✅ behoben — Completion als `(String?) -> Void` | | 9 | 2026-09-21 | `swift build` | `Result<Void, String>`: String is not Error (follow-on DispatchQueue errors) | blocking | ✅ fixed — completion as `(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 | | 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 | Passwort-Feature | Wunsch: Passwort + Keys speichern UND nutzen (bisher nur gespeichert, nie benutzt) | Feature | ✅ umgesetzt — Auto-Login via sshpass, sonst interaktiv | | 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 | „Immer Verbindung falsch" | User-Report ohne Details (Ziel/Credentials unbekannt) | offen | ⬜ Teils: Editor-Validierung + kopierbarer Fehlerbericht; warte auf Bericht | | 12 | 2026-09-21 | "always connection wrong" | user report without details | open | ⬜ partly: editor validation + copyable error report |
| 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) | | 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 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 | | 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 bei Tippen + Senden | Ursache unklar (headless nicht reproduzierbar) | kritisch | ⬜ Gehärtet (v0.5.3); Crash-Log angefragt | | 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 | Profil-Änderungen wirkungslos | Session-Cache ignorierte Host-/Passwort-/Key-Korrekturen | hoch | ✅ behoben — Fingerprint-Reconfig (9/9 Tests) | | 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: Vorab-Check Timeout | Port 22 wird serverseitig gedroppt (443 ok) — kein App-Fehler | extern | ⬜ Server-Seite: fail2ban/VPN/Firewall prüfen | | 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 unmöglich | ssh ohne Controlling-Terminal: /dev/tty ENXIO → stummer Abbruch | kritisch | ✅ ctty-run-Helper (Double-Fork) + Fingerprint-Popup | | 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` ## Template for new errors
**Phase:** erster `swift build`
**Log (gekürzt):**
```
Sources/RemminaMac/RemminaMacApp.swift:14:10: error: value of type 'WindowGroup<some View>' 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
```markdown ```markdown
## #N — Titel ## #N — Title
**Phase:** ... **Phase:** ...
**Log:** ... **Log:** ...
**Ursache:** ... **Cause:** ...
**Fix:** ... **Fix:** ...
**Verifikation:** ... **Verified:** ...
**Status:** ⬜ offen / ✅ behoben **Status:** ⬜ open / ✅ fixed
``` ```
## Wie Fehler hierher kommen ## How errors get here
- Jeder `swift build` / `swift build -c release` / Skript-Lauf mit Fehler → Log-Ausschnitt + Exit-Code hier eintragen. - Every `swift build` / `swift build -c release` / script run with an error → log excerpt + exit code goes here.
- Gitea-Push/Release-Fehler (z. B. 401, Repo existiert schon, Asset-Upload schlägt fehl) ebenfalls hier dokumentieren. - Gitea push/release errors likewise.
- Neue Tests: `AnsiParser` per `swiftc`-Harness (`/tmp`, SGR/CRLF/Clear/Map), `PTYSession` per Smoke-Test (`/bin/echo` + `ssh -G` durchs echte PTY). - 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) ## #1 — `windowTitle` doesn't exist on `WindowGroup`
**Phase:** Laufzeit, SSH-Profil → Verbinden **Phase:** first `swift build`
**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<Void, String>` 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)
**Log:** **Log:**
``` ```
GET /user -> 401 RemminaMacApp.swift:14:10: error: value of type 'WindowGroup<some View>' has no member 'windowTitle'
{"message":"invalid username, password or token","url":"https://git.sysdaemon.xyz/api/swagger"}
``` ```
Getestet mit Login `x3` und `x3@sysdaemon.xyz` — beide 401. Kein Quote-/Encoding-Problem (Basic-Auth via Python, kein Shell-Escaping). **Cause:** `windowTitle(_:)` is not a `WindowGroup` modifier; the title goes in the `WindowGroup` init.
**Mögliche Ursachen:** **Fix:** `WindowGroup("RemminaMac") { ... }`.
1. Passwort falsch/ablaufen bzw. Sonderzeichen-Typo **Status:** ✅ fixed.
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://<user>@git.sysdaemon.xyz/<owner>/remmina-mac.git
git push -u origin main
export GITEA_HOST="https://git.sysdaemon.xyz" GITEA_OWNER="<owner>" GITEA_REPO="remmina-mac" GITEA_TOKEN="<token>" VERSION="0.1.0"
./scripts/publish-gitea-release.sh
```
Oder mit Token als Passwort: `git push https://<owner>:<token>@git.sysdaemon.xyz/<owner>/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.
## #12 — „Immer Verbindung falsch" (User-Report, Details offen) ## #2 — Slow but clean release build
**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). **Phase:** `bash scripts/build-release.sh 0.1.0` → `swift build -c release` (~40 s first link).
**Häufigste reale Ursachen (Checkliste):** **Cause:** none — release optimization + link just take a while cold; faster on rebuilds (`.build/` cache).
1. `user@host` im Host-Feld oder `ssh://`-Präfix → jetzt Live-Warnung im Editor **Status:** ✅ ok.
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.
## #13 — Permission denied trotz richtigem Passwort (User-Fehlerbericht) ## #3 — `dist/` almost committed
**Bericht:** `administrator@sysdaemon.xyz:22`, ssh fragt 3x Passwort → `Permission denied (publickey,password)`. Gleiches Passwort im Terminal.app funktioniert. **Cause:** `dist/` held ZIP + unpacked binary, `.gitignore` didn't know it.
**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:** ignore `.build/`, `dist/`, `*.zip`, `*.sha256`.
**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). **Status:** ✅ fixed.
**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.
## #14 — Terminal.app loggt ein, RemminaMac nicht (gleiches Passwort) ## #4 — Gitea upload: 401 despite reachable server
**Stand (User-Antworten):** v0.5.1 installiert, Passwort-Login, Terminal.app funktioniert mit denselben Daten. **Phase:** `POST /api/v1/user/repos` + `GET /api/v1/user` on `https://git.sysdaemon.xyz` (v1.27.3 confirmed via `/api/v1/version`).
**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. **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:** `SSHService.agentSocket()` (launchctl + Existenzprüfung) → `TerminalManager` injiziert `SSH_AUTH_SOCK` ins ssh-Env (koexistiert mit SSHPASS). Banner-Tipp ergänzt. **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.
**Verifikation:** Harness 4/4 (Auflösung 0.07 s, Pfad existiert, `ssh -G` ok mit echtem + kaputtem Socket). **Status:** ✅ fixed.
**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.
## #15 — Crash bei Tippen + Senden (User-Report) ## #6 — SSH click opens no terminal (user bug report)
**Stand:** „Sobald ich was eintippe und absende, crasht das Programm." Headless nicht reproduzierbar — alle Ebenen einzeln grün. **Cause:** `SSHService.openInTerminal` used `NSAppleScript(tell application "Terminal" …)` — fails silently without `NSAppleEventsUsageDescription` and with no error handling.
**Ausgeschlossen per Test:** **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).
- SIGPIPE beim Schreiben (Harness: macOS liefert EIO statt Signal; `send` nach `stop()` überlebt) — Hypothese widerlegt **Verified:** build ok; parser 8/8; PTY smoke (`/bin/echo` → output + exit 0); `ssh -G` through PTY ok.
- View-Schicht (echter Coordinator + NSTextView: Tippen mit Umlauten/Quotes/Paste, Enter, 200 Programm-Sets) — grün **Status:** ✅ fixed.
- 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.
## #16 — Profil-Änderungen wirkungslos („geht immer noch nicht") ## #7 — ANSI parser: text got the wrong style
**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. **Phase:** `swiftc` harness during #6. Log: `text("rot", fg: nil)` instead of `fg: 1`.
**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). **Cause:** chunk flushed only after the SGR switch.
**Verifikation:** Harness 9/9 (Host-Wechsel reconfiguriert, Secret-Wechsel erkannt, laufende ssh-Session an Banner-lesendem Socket unangetastet). **Fix:** `flush()` before `parseCSI`. Harness 8/8 green.
**Status:** ✅ behoben — bitte mit v0.5.4 erneut testen (Profil ggf. einmal neu speichern). **Status:** ✅ fixed.
## #17 — rzsv: Vorab-Check Timeout (kein App-Fehler, Screenshot-Beweis) ## #8 — Terminal connects then dies instantly, no message (user bug report)
**Bild:** Tab rzsv (`administrator@sysdaemon.xyz:22`), Banner „Vorab-Check: Keine Antwort in 4 s", ssh wird korrekt NICHT gestartet. **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.
**Messung (gleiche Maschine, 22.09.):** **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.
- `sysdaemon.xyz` → `185.117.250.190` (auch `git.sysdaemon.xyz`) **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.
- Port 443: OK in 0.02 s — Port 22: Timeout (DROP, kein Refused) **Verified:** refused harness now shows the error (PASS); `cat` roundtrip, `ssh -G`, TCP harness (closed/open/empty) PASS.
- 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. **Status:** ✅ fixed.
**Wahrscheinlichste Ursachen (Reihenfolge):**
1. **fail2ban-Bann** nach den vielen `Permission denied`-Versuchen (`#13`-Ära): `fail2ban-client status sshd` → `unbanip <eigene-IP>`
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.
## #18 — Host-Key-Yes/No unmöglich (User-Screenshot: Terminal geht, App sagt nur „Host key verification failed.") ## #9 — Build: `Result<Void, String>` invalid
**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. **Log:** `error: type 'String' does not conform to protocol 'Error'` (+ follow-on errors).
**Fix:** **Fix:** `TCPCheck.check` uses `(String?) -> Void` (nil = ok, else message).
- `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). **Status:** ✅ fixed.
- `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. ## #10 — Icon generator build errors
**Status:** ✅ behoben (v0.6.0) — Fingerprint im Popup mit Terminal-Ausgabe vergleichbar. **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.
+30 -21
View File
@@ -1,51 +1,60 @@
# Installation # Installation
## Voraussetzungen ## Requirements
- macOS 14+ - 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/` 1. `open Package.swift` in `RemminaMac/`
2. Target `RemminaMac` wählen, Destination `My Mac` 2. Select target `RemminaMac`, destination `My Mac`
3. `Cmd+R` → App startet 3. `Cmd+R` → app launches
4. Erster Start legt an: 4. First launch creates:
- `~/Library/Application Support/RemminaMac/profiles.json` - `~/Library/Application Support/RemminaMac/profiles.json`
- `~/Library/Application Support/RemminaMac/sshkeys.json` - `~/Library/Application Support/RemminaMac/sshkeys.json`
## Variante B: Swift CLI ## Option B: Swift CLI
```bash ```bash
cd RemminaMac cd RemminaMac
swift build swift build
.build/debug/RemminaMac .build/debug/RemminaMac
``` ```
## Variante C: Release-Binary (ohne Xcode) ## Option C: Release binary (no Xcode)
```bash ```bash
./scripts/build-release.sh ./scripts/build-release.sh
unzip dist/RemminaMac-*-macos-arm64.zip -d /tmp/rmm unzip dist/RemminaMac-*-macos-arm64.zip -d /tmp/rmm
/tmp/rmm/RemminaMac /tmp/rmm/RemminaMac
``` ```
## Variante D: Installer-DMG (Empfohlen für Endnutzer) ## Option D: Installer DMG (recommended for end users)
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`) 1. Download `RemminaMac-<version>-macos-arm64.dmg` from a Gitea release
2. DMG doppelklicken → Finder-Fenster mit `RemminaMac.app` + `Programme`-Link (or build locally: `./scripts/build-app.sh <version> && ./scripts/build-dmg.sh <version>`)
3. `RemminaMac.app` auf `Programme` ziehen → liegt in `/Applications/RemminaMac.app` 2. Double-click the DMG → Finder window with `RemminaMac.app` + `Applications` link
4. Auswerfen, aus Programme starten. Beim ersten Start: Rechtsklick → Öffnen (ad-hoc-Signatur, kein Apple-Notarization). 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 ```bash
# Prüfen: # Verify:
spctl -a -vv dist/RemminaMac.app 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 ```bash
# Für volle RDP-Unterstützung: # For full RDP support:
brew install freerdp brew install freerdp
which xfreerdp 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 ## Troubleshooting
- `swift build` schlägt fehl → `swift --version` prüfen (>= 5.9), Xcode Command Line Tools: `xcode-select --install` - `swift build` fails → check `swift --version` (>= 5.9), Xcode Command Line Tools: `xcode-select --install`
- Keychain-Prompt → einmal erlauben, Service `RemminaMac` - Keychain prompt → allow once, service `RemminaMac`
- `xfreerdp` nicht gefunden → Fallback nutzt `rdc://`, braucht MS Remote Desktop - `xfreerdp` not found → fallback uses `rdc://`, needs MS Remote Desktop
+29 -23
View File
@@ -1,41 +1,47 @@
# Releasing # Releasing
## Lokal bauen ## Build locally
```bash ```bash
./scripts/build-release.sh [version] # ZIP mit Binary + Docs ./scripts/build-release.sh [version] # ZIP with binary + docs
./scripts/build-app.sh [version] # RemminaMac.app (Bundle, ad-hoc-signiert, mit Icon) ./scripts/build-app.sh [version] # RemminaMac.app (bundle, ad-hoc signed, icon + ctty-run)
./scripts/build-dmg.sh [version] # DMG mit App + Programme-Link ./scripts/build-dmg.sh [version] # DMG with app + Applications link
./scripts/generate-icon.sh # AppIcon.icns neu rendern (Dracula-Terminal) ./scripts/generate-icon.sh # re-render AppIcon.icns (Dracula terminal)
# Beispiel: # Example:
./scripts/build-release.sh 0.1.0 ./scripts/build-release.sh 0.7.0
./scripts/build-app.sh 0.1.0 ./scripts/build-app.sh 0.7.0
./scripts/build-dmg.sh 0.1.0 ./scripts/build-dmg.sh 0.7.0
# Output: # Output:
# dist/RemminaMac-0.1.0-macos-arm64.zip (+ .sha256) # dist/RemminaMac-0.7.0-macos-arm64.zip (+ .sha256)
# dist/RemminaMac.app/ # dist/RemminaMac.app/ (with ctty-run helper + icon)
# dist/RemminaMac-0.1.0-macos-arm64.dmg (+ .sha256) # dist/RemminaMac-0.7.0-macos-arm64.dmg (+ .sha256)
``` ```
## Was das Skript macht Bump `AppInfo.version` in `Views/AboutView.swift` with each release.
1. `swift build -c release`
2. Binary nach `dist/RemminaMac-<ver>-macos-arm64/` kopieren
3. `README.md` + `docs/` + `LICENSE` dazulegen
4. ZIP + SHA256 erzeugen
## Gitea-Release erstellen ## What the scripts do
1. `swift build -c release` (builds both `RemminaMac` and `ctty-run`)
2. Copy binaries into `dist/RemminaMac-<ver>-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 ```bash
export GITEA_HOST="https://git.sysdaemon.xyz" export GITEA_HOST="https://git.sysdaemon.xyz"
export GITEA_OWNER="<owner>" export GITEA_OWNER="<owner>"
export GITEA_REPO="remmina-mac" export GITEA_REPO="remmina-mac"
export GITEA_TOKEN="<token>" # statt Passwort: User → Settings → Applications → Token export GITEA_TOKEN="<token>" # not password: user → Settings → Applications → token
export VERSION="0.1.0" export VERSION="0.7.0"
./scripts/publish-gitea-release.sh ./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 ```bash
curl -u "user:pass" -H "Content-Type: application/json" \ 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" "$GITEA_HOST/api/v1/repos/$OWNER/$REPO/releases"
``` ```
+33 -33
View File
@@ -1,46 +1,46 @@
# Benutzung # Usage
## Layout (wie Remmina) ## Layout (Remmina-style)
- **Links:** Gruppen-Dropdown, Suche, Profil-Liste. Rechtsklick: Verbinden / Duplizieren / Bearbeiten / Löschen. Doppelklick verbindet. - **Left:** group dropdown, search, profile list. Right-click: Connect / Duplicate / Edit / Delete. Double-click connects.
- **Rechts:** Tabs aller offenen Sitzungen + Verbindungsdetails (kopierbarer Befehl) + Hinweis-Box. - **Right:** tabs for open sessions + connection details (copyable command) + info box.
- **Unten:** Statusbar mit Profil-/Sitzungs-Zähler. - **Bottom:** status bar with profile/session counts.
- **Toolbar:** `+` Neu, Stift Bearbeiten, Kopie-Symbol Duplizieren, Schlüssel Keys, `▶` Verbinden, `⚡` Quick-Connect. - **Toolbar:** `+` New, pencil Edit, copy Duplicate, key SSH keys, `ⓘ` About, `▶` Connect, `⚡` Quick-Connect.
## Profil anlegen ## Creating a profile
1. `+` → Name, Gruppe, Protokoll (SSH/SFTP/RDP/VNC), Host, Port, Benutzername, **Passwort direkt darunter** ausfüllen (Keychain-Haken setzen zum Speichern). 1. `+` → name, group, protocol (SSH/SFTP/RDP/VNC), host, port, username, **password right below** (check Keychain to save it).
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. 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. SSH-Key wählen (optional, Bereich darunter — vorher unter Schlüssel-Icon importieren). 3. Pick an SSH key (optional, section below — import it first via the key icon).
4. Speichern → liegt sofort in `profiles.json`. 4. Save → stored immediately in `profiles.json`.
5. Schlägt eine Verbindung fehl: rotes Banner → **Bericht kopieren** und zur Fehlersuche mitschicken (enthält kein Passwort). 5. If a connection fails: red banner → **Copy report** and send it in for debugging (contains no password).
## Verbinden ## Connecting
- Profil wählen → `▶ Verbinden` oder Doppelklick. - Select a profile → `▶ Connect` or double-click.
- 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. - 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.
- Alternativ: `In Terminal.app öffnen` (öffnet eine `.command`-Datei, braucht keine Automation-Berechtigung) oder `Befehl kopieren`. - Alternative: `Open in Terminal.app` (opens a `.command` file, no automation permission needed) or `Copy command`.
- RDP: startet `xfreerdp` (falls vorhanden) oder öffnet MS Remote Desktop. - RDP: launches `xfreerdp` (if present) or opens Microsoft Remote Desktop.
- VNC: öffnet `vnc://host:port`. - VNC: opens `vnc://host:port`.
## SSH-Keys verwalten (Schlüssel-Icon) ## Managing SSH keys (key icon)
- `Import aus ~/.ssh`: übernimmt alle privaten Keys (ohne `*.pub`, `known_hosts`, `config`). - `Import from ~/.ssh`: picks up all private keys (skips `*.pub`, `known_hosts`, `config`).
- `Wählen…`: Picker für Key-Datei. - `Choose…`: file picker for a key file.
- `Neuen Key generieren`: ruft `/usr/bin/ssh-keygen -t ed25519 -f ~/.ssh/remminamac_<timestamp>` auf. - `Generate new key`: runs `/usr/bin/ssh-keygen -t ed25519 -f ~/.ssh/remminamac_<timestamp>`.
- Passphrase → Keychain (`sshkey-<UUID>`). - Passphrase → Keychain (`sshkey-<UUID>`).
## Host-Key prüfen & akzeptieren (Popup) ## Host-key verification (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). - 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).
- Bei GEÄNDERTEM Key: rote Warnung (MITM möglich!) — nur ersetzen, wenn erwartet (Server neu aufgesetzt). Zurücksetzen auch manuell: `ssh-keygen -R HOST`. - On CHANGED key: red warning (possible MITM!) — only replace if expected (freshly provisioned server). Manual reset: `ssh-keygen -R HOST`.
- Technik: `Sources/ctty-run/ctty-run.c` gibt ssh ein Controlling-Terminal (sonst scheitern alle interaktiven Prompts still). - Tech note: `Sources/ctty-run/ctty-run.c` gives ssh a controlling terminal (without it all interactive prompts fail silently).
## Passwort speichern & Auto-Login ## Saving passwords & 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. - Profile editor: enter password + check Keychain. A saved password is indicated (✓) and can be deleted. `profiles.json` holds only the reference, never the password.
- Reihenfolge beim Verbinden: Profil-Passwort zuerst, sonst Key-Passphrase. - Order at connect time: profile password first, key passphrase second.
- **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". - **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.
- **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). - **Without sshpass:** password/key passphrase is asked interactively in the terminal.
## Quick-Connect ## 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 ```bash
PROFILE_DIR=~/Library/Application\ Support/RemminaMac PROFILE_DIR=~/Library/Application\ Support/RemminaMac
cp "$PROFILE_DIR/profiles.json" ~/Desktop/remminamac-backup.json cp "$PROFILE_DIR/profiles.json" ~/Desktop/remminamac-backup.json