Files
remmina-mac/Sources/RemminaMac/Views/EmbeddedTerminalView.swift
T

158 lines
6.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
import AppKit
/// In-App-Terminal im Dracula-Look: Ausgabe (NSTextView, read-only) + Eingabezeile.
/// Zeilenumbruch ist an, Autoscroll ans Ende, Passwort-Modus via Auge-Icon.
struct EmbeddedTerminalView: View {
@ObservedObject var term: PTYSession
var canConnect: Bool
@State private var input = ""
@State private var secureInput = false
var body: some View {
VStack(spacing: 0) {
// Statuszeile
HStack(spacing: 8) {
statusDot
Text(statusText).font(.caption).foregroundStyle(.secondary)
Spacer()
if term.isRunning {
Button("Trennen") { term.stop() }.buttonStyle(.link).font(.caption)
} else if canConnect {
Button("Verbinden") { term.start() }.buttonStyle(.link).font(.caption)
}
Button("Leeren") { term.clear() }.buttonStyle(.link).font(.caption)
}
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(Dracula.currentLine.opacity(0.35))
// Ausgabe im Dracula-Theme
DraculaTextView(lines: term.lines)
.frame(maxWidth: .infinity, maxHeight: .infinity)
// Eingabezeile
HStack(spacing: 6) {
Text("❯").font(.system(.body, design: .monospaced)).foregroundStyle(Dracula.green)
if secureInput {
SecureField("Eingabe (versteckt, z. B. Passwort) …", text: $input, onCommit: submit)
.textFieldStyle(.plain)
.font(.system(.body, design: .monospaced))
.foregroundStyle(Dracula.foreground)
} else {
TextField("Eingabe …", text: $input, onCommit: submit)
.textFieldStyle(.plain)
.font(.system(.body, design: .monospaced))
.foregroundStyle(Dracula.foreground)
}
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)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Dracula.currentLine, lineWidth: 1))
.onAppear {
if case .never = term.state, canConnect {
term.start() // Auto-Connect beim Öffnen des Tabs (Bugfix: kein externes Terminal mehr nötig)
}
}
}
func submit() {
guard term.isRunning, !input.isEmpty else { return }
term.send(input + "\n")
input = ""
}
var statusDot: some View {
Group {
if term.isRunning { Image(systemName: "circle.fill").foregroundStyle(Dracula.green) }
else if case .done = term.state { Image(systemName: "circle.fill").foregroundStyle(Dracula.red) }
else { Image(systemName: "circle.fill").foregroundStyle(Dracula.comment) }
}.font(.caption2)
}
var statusText: String {
switch term.state {
case .never: return canConnect ? "Bereit — verbindet beim Öffnen." : "Host/Benutzer fehlt."
case .running: return "Verbunden (PTY, TERM=xterm-256color)."
case .done(_, let msg): return msg
}
}
}
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)
}
}