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 } } }