Full interactive terminal via SwiftTerm, About sheet, English docs
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
import SwiftUI
|
||||
import AppKit
|
||||
|
||||
/// In-App-Terminal im Dracula-Look: Ausgabe (NSTextView, read-only) + Eingabezeile.
|
||||
/// Verbindet erst nach TCP-Vorab-Check (falscher Port/Host -> sofort klare Meldung).
|
||||
/// Zeilenumbruch ist an, Autoscroll ans Ende, Passwort-Modus via Auge-Icon.
|
||||
/// Full interactive terminal (SwiftTerm emulator, Dracula themed):
|
||||
/// cursor addressing, colors, scrollback, Tab completion, arrows, Ctrl+C,
|
||||
/// full-screen apps (vim/htop). Keyboard goes straight to the pty.
|
||||
/// Connects after TCP pre-check + host-key approval (popup).
|
||||
struct EmbeddedTerminalView: View {
|
||||
@ObservedObject var term: PTYSession
|
||||
let profile: ConnectionProfile
|
||||
var canConnect: Bool
|
||||
@State private var input = ""
|
||||
@State private var secureInput = false
|
||||
@State private var checking = false
|
||||
@State private var hostKeyRequest: HostKeyRequest?
|
||||
|
||||
@@ -59,37 +58,9 @@ struct EmbeddedTerminalView: View {
|
||||
.overlay(Rectangle().fill(Dracula.red.opacity(0.4)).frame(height: 1), alignment: .bottom)
|
||||
}
|
||||
|
||||
// Ausgabe im Dracula-Theme
|
||||
DraculaTextView(lines: term.lines)
|
||||
// Full terminal emulator (click to focus, type directly).
|
||||
SwiftTermView(term: term)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
// Eingabezeile
|
||||
HStack(spacing: 6) {
|
||||
Text("❯").font(.system(.body, design: .monospaced)).foregroundStyle(Dracula.green)
|
||||
// Gehärtetes Feld: keine macOS-Text-Ersetzungen (Passwort-Sonderzeichen!)
|
||||
ZStack(alignment: .leading) {
|
||||
if input.isEmpty {
|
||||
Text(secureInput ? "Eingabe (versteckt, z. B. Passwort) …" : "Eingabe …")
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.foregroundStyle(Dracula.comment.opacity(0.7))
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
TerminalInputField(text: $input, isSecure: secureInput, onSubmit: submit)
|
||||
.id(secureInput)
|
||||
.frame(height: 22)
|
||||
}
|
||||
Button(action: { secureInput.toggle() }) {
|
||||
Image(systemName: secureInput ? "eye.slash" : "eye")
|
||||
}
|
||||
.buttonStyle(.plain).foregroundStyle(Dracula.comment)
|
||||
.help("Passwort-Modus (versteckte Eingabe)")
|
||||
Button("Senden") { submit() }
|
||||
.disabled(input.isEmpty || !term.isRunning)
|
||||
ControlButton(label: "^C", tip: "SIGINT (Strg+C)") { term.send("\u{03}") }
|
||||
ControlButton(label: "^D", tip: "EOF (Strg+D)") { term.send("\u{04}") }
|
||||
}
|
||||
.padding(8)
|
||||
.background(Dracula.background)
|
||||
}
|
||||
.background(Dracula.background)
|
||||
.cornerRadius(10)
|
||||
@@ -173,15 +144,7 @@ struct EmbeddedTerminalView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func submit() {
|
||||
guard term.isRunning, !input.isEmpty else { return }
|
||||
term.send(input + "\n")
|
||||
input = ""
|
||||
}
|
||||
|
||||
/// Fehlerbericht ohne Secrets (Passwort steht weder im Profil noch im Befehl).
|
||||
/// Hinweis: Wer das Passwort sichtbar eingetippt hat (nicht Auge-Modus),
|
||||
/// löscht die Ausgabe vorher per „Leeren".
|
||||
func copyReport() {
|
||||
var r = "RemminaMac Fehlerbericht\n"
|
||||
r += "Profil: \(profile.name) | \(profile.proto.rawValue) | \(profile.username)@\(profile.host):\(profile.port)\n"
|
||||
@@ -234,67 +197,3 @@ struct EmbeddedTerminalView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct ControlButton: View {
|
||||
var label: String
|
||||
var tip: String
|
||||
var action: () -> Void
|
||||
var body: some View {
|
||||
Button(label, action: action)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.foregroundStyle(Dracula.orange)
|
||||
.buttonStyle(.plain)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Dracula.currentLine.opacity(0.6), in: RoundedRectangle(cornerRadius: 5))
|
||||
.help(tip)
|
||||
}
|
||||
}
|
||||
|
||||
/// Read-only NSTextView mit Dracula-Farben, baut NSAttributedString aus StyledText-Zeilen.
|
||||
struct DraculaTextView: NSViewRepresentable {
|
||||
var lines: [[StyledText]]
|
||||
|
||||
func makeNSView(context: Context) -> NSScrollView {
|
||||
let scroll = NSScrollView()
|
||||
scroll.hasVerticalScroller = true
|
||||
scroll.drawsBackground = true
|
||||
scroll.backgroundColor = Dracula.nsBackground
|
||||
let tv = NSTextView()
|
||||
tv.isEditable = false
|
||||
tv.isSelectable = true
|
||||
tv.backgroundColor = Dracula.nsBackground
|
||||
tv.textColor = Dracula.nsColor(for: nil)
|
||||
tv.font = .monospacedSystemFont(ofSize: 12, weight: .regular)
|
||||
tv.textContainerInset = NSSize(width: 8, height: 8)
|
||||
tv.isAutomaticQuoteSubstitutionEnabled = false
|
||||
scroll.documentView = tv
|
||||
return scroll
|
||||
}
|
||||
|
||||
func updateNSView(_ scroll: NSScrollView, context: Context) {
|
||||
guard let tv = scroll.documentView as? NSTextView else { return }
|
||||
let attr = NSMutableAttributedString()
|
||||
let font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular)
|
||||
let boldFont = NSFontManager.shared.convert(font, toHaveTrait: .boldFontMask)
|
||||
for (idx, row) in lines.enumerated() {
|
||||
if row.isEmpty {
|
||||
attr.append(NSAttributedString(string: idx < lines.count - 1 ? "\n" : ""))
|
||||
continue
|
||||
}
|
||||
for seg in row {
|
||||
let attrs: [NSAttributedString.Key: Any] = [
|
||||
.font: seg.style.bold ? boldFont : font,
|
||||
.foregroundColor: Dracula.nsColor(for: seg.style.fg),
|
||||
.backgroundColor: seg.style.bg.map { Dracula.nsColor(for: $0) } ?? Dracula.nsBackground
|
||||
]
|
||||
attr.append(NSAttributedString(string: seg.text, attributes: attrs))
|
||||
}
|
||||
if idx < lines.count - 1 { attr.append(NSAttributedString(string: "\n")) }
|
||||
}
|
||||
// Nur neu setzen wenn geändert (Performance bei viel Output)
|
||||
if tv.textStorage?.string != attr.string {
|
||||
tv.textStorage?.setAttributedString(attr)
|
||||
}
|
||||
tv.scrollToEndOfDocument(nil)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user