Full interactive terminal via SwiftTerm, About sheet, English docs
This commit is contained in:
@@ -33,6 +33,9 @@ final class PTYSession: ObservableObject {
|
||||
private var readSource: DispatchSourceRead?
|
||||
private var errPipe: Pipe?
|
||||
private let ioQueue = DispatchQueue(label: "RemminaMac.pty")
|
||||
/// Raw output hook for the terminal emulator (SwiftTerm feeds on this).
|
||||
/// Called on ioQueue for every chunk from pty master and stderr pipe.
|
||||
var onData: ((Data) -> Void)?
|
||||
private var parser = AnsiParser()
|
||||
private var finished = false
|
||||
private var epoch = 0 // nur Main-Thread
|
||||
@@ -132,6 +135,7 @@ final class PTYSession: ObservableObject {
|
||||
h.readabilityHandler = nil // EOF
|
||||
return
|
||||
}
|
||||
self.onData?(data)
|
||||
self.ioQueue.async { [weak self] in
|
||||
guard let self else { return }
|
||||
let events = self.parser.feed(data)
|
||||
@@ -152,7 +156,9 @@ final class PTYSession: ObservableObject {
|
||||
var buf = [UInt8](repeating: 0, count: 8192)
|
||||
let n = read(fd, &buf, buf.count)
|
||||
if n > 0 {
|
||||
let events = parser.feed(Data(buf[..<n]))
|
||||
let data = Data(buf[..<n])
|
||||
onData?(data)
|
||||
let events = parser.feed(data)
|
||||
DispatchQueue.main.async { [weak self] in self?.apply(events) }
|
||||
} else {
|
||||
readSource?.cancel() // EOF/EIO — Rest holt finalFlush bzw. ist verworfen
|
||||
@@ -217,9 +223,14 @@ final class PTYSession: ObservableObject {
|
||||
}
|
||||
|
||||
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
|
||||
guard fd >= 0 else { return }
|
||||
guard let data = string.data(using: .utf8), !data.isEmpty else { return }
|
||||
guard fd >= 0, !data.isEmpty else { return }
|
||||
ioQueue.async {
|
||||
data.withUnsafeBytes { ptr in
|
||||
guard let base = ptr.baseAddress else { return }
|
||||
@@ -233,6 +244,16 @@ final class PTYSession: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// Terminal size to the pty (TIOCSWINSZ) so vim etc. use the full area.
|
||||
func setWinsize(cols: Int, rows: Int) {
|
||||
let fd = masterFD
|
||||
guard fd >= 0, cols > 0, rows > 0 else { return }
|
||||
var ws = winsize(ws_row: UInt16(min(rows, 65535)),
|
||||
ws_col: UInt16(min(cols, 65535)),
|
||||
ws_xpixel: 0, ws_ypixel: 0)
|
||||
_ = ioctl(fd, TIOCSWINSZ, &ws)
|
||||
}
|
||||
|
||||
func clear() {
|
||||
lines = [[]]
|
||||
}
|
||||
@@ -324,27 +345,35 @@ final class TerminalManager: ObservableObject {
|
||||
private var sessions: [UUID: PTYSession] = [:]
|
||||
|
||||
func session(for profile: ConnectionProfile, key: StoredSSHKey?) -> PTYSession {
|
||||
// Fingerprint aus allem, was die Verbindung bestimmt (inkl. Secret-Wert!):
|
||||
// Ändert der User Host/Passwort/Key, wird neu konfiguriert statt stale weiterzuwählen.
|
||||
// Fingerprint aus allem, was die Verbindung bestimmt (inkl. Secret-Wert
|
||||
// und sshpass-Verfügbarkeit): Ändert der User Host/Passwort/Key oder wird
|
||||
// sshpass (nach-)installiert, wird neu konfiguriert statt stale weiterzuwählen.
|
||||
let secret = SSHService.loginSecret(for: profile, key: key)
|
||||
let tag = [profile.proto.rawValue, profile.host, "\(profile.port)", profile.username,
|
||||
profile.extraArgs, key?.privateKeyPath ?? "", secret ?? ""].joined(separator: "|")
|
||||
let sshpass = SSHService.findSshpass()
|
||||
let tag = Self.configTag(profile: profile, keyPath: key?.privateKeyPath,
|
||||
secret: secret, sshpass: sshpass)
|
||||
if let existing = sessions[profile.id] {
|
||||
// Laufende Session niemals unter den Füßen wegkonfigurieren — erst Trennen.
|
||||
if existing.isRunning || existing.configTag == tag { return existing }
|
||||
existing.stop()
|
||||
configure(existing, profile: profile, key: key, secret: secret)
|
||||
configure(existing, profile: profile, key: key, secret: secret, sshpass: sshpass)
|
||||
existing.configTag = tag
|
||||
return existing
|
||||
}
|
||||
let s = PTYSession()
|
||||
configure(s, profile: profile, key: key, secret: secret)
|
||||
configure(s, profile: profile, key: key, secret: secret, sshpass: sshpass)
|
||||
s.configTag = tag
|
||||
sessions[profile.id] = s
|
||||
return s
|
||||
}
|
||||
|
||||
private func configure(_ s: PTYSession, profile: ConnectionProfile, key: StoredSSHKey?, secret: String?) {
|
||||
/// Pure tag builder (testable): any change here must reconfigure.
|
||||
nonisolated static func configTag(profile: ConnectionProfile, keyPath: String?, secret: String?, sshpass: String?) -> String {
|
||||
[profile.proto.rawValue, profile.host, "\(profile.port)", profile.username,
|
||||
profile.extraArgs, keyPath ?? "", secret ?? "", sshpass ?? ""].joined(separator: "|")
|
||||
}
|
||||
|
||||
private func configure(_ s: PTYSession, profile: ConnectionProfile, key: StoredSSHKey?, secret: String?, sshpass: String?) {
|
||||
let baseExe: String
|
||||
let baseArgs: [String]
|
||||
if profile.proto == .sftp {
|
||||
@@ -358,7 +387,7 @@ final class TerminalManager: ObservableObject {
|
||||
// SSH-Agent durchreichen (falls vorhanden): gleiche Keys wie in Terminal.app.
|
||||
let cfg = SSHService.sessionConfig(executable: baseExe, args: baseArgs,
|
||||
password: secret,
|
||||
sshpassPath: SSHService.findSshpass())
|
||||
sshpassPath: sshpass)
|
||||
s.configure(executable: cfg.executable, arguments: cfg.arguments)
|
||||
var env = cfg.extraEnv
|
||||
if let sock = SSHService.agentSocket() {
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import SwiftUI
|
||||
import AppKit
|
||||
import SwiftTerm
|
||||
|
||||
/// Offizielle Dracula-Palette (https://draculatheme.com/contribute).
|
||||
enum Dracula {
|
||||
static let background = Color(hex: "282A36")
|
||||
static let currentLine = Color(hex: "44475A")
|
||||
static let foreground = Color(hex: "F8F8F2")
|
||||
static let comment = Color(hex: "6272A4")
|
||||
static let cyan = Color(hex: "8BE9FD")
|
||||
static let green = Color(hex: "50FA7B")
|
||||
static let orange = Color(hex: "FFB86C")
|
||||
static let pink = Color(hex: "FF79C6")
|
||||
static let purple = Color(hex: "BD93F9")
|
||||
static let red = Color(hex: "FF5555")
|
||||
static let yellow = Color(hex: "F1FA8C")
|
||||
static let background = SwiftUI.Color(hex: "282A36")
|
||||
static let currentLine = SwiftUI.Color(hex: "44475A")
|
||||
static let foreground = SwiftUI.Color(hex: "F8F8F2")
|
||||
static let comment = SwiftUI.Color(hex: "6272A4")
|
||||
static let cyan = SwiftUI.Color(hex: "8BE9FD")
|
||||
static let green = SwiftUI.Color(hex: "50FA7B")
|
||||
static let orange = SwiftUI.Color(hex: "FFB86C")
|
||||
static let pink = SwiftUI.Color(hex: "FF79C6")
|
||||
static let purple = SwiftUI.Color(hex: "BD93F9")
|
||||
static let red = SwiftUI.Color(hex: "FF5555")
|
||||
static let yellow = SwiftUI.Color(hex: "F1FA8C")
|
||||
|
||||
/// ANSI-Index 0-15 -> Dracula-Farbe.
|
||||
static func color(for index: Int?) -> Color {
|
||||
static func color(for index: Int?) -> SwiftUI.Color {
|
||||
guard let index else { return foreground }
|
||||
switch index {
|
||||
case 0: return Color(hex: "21222C")
|
||||
case 0: return SwiftUI.Color(hex: "21222C")
|
||||
case 1: return red
|
||||
case 2: return green
|
||||
case 3: return yellow
|
||||
@@ -28,13 +29,13 @@ enum Dracula {
|
||||
case 6: return cyan
|
||||
case 7: return foreground
|
||||
case 8: return comment
|
||||
case 9: return Color(hex: "FF6E6E")
|
||||
case 10: return Color(hex: "69FF94")
|
||||
case 11: return Color(hex: "FFFFA5")
|
||||
case 12: return Color(hex: "D6ACFF")
|
||||
case 13: return Color(hex: "FF92DF")
|
||||
case 14: return Color(hex: "A4FFFF")
|
||||
default: return Color(hex: "FFFFFF")
|
||||
case 9: return SwiftUI.Color(hex: "FF6E6E")
|
||||
case 10: return SwiftUI.Color(hex: "69FF94")
|
||||
case 11: return SwiftUI.Color(hex: "FFFFA5")
|
||||
case 12: return SwiftUI.Color(hex: "D6ACFF")
|
||||
case 13: return SwiftUI.Color(hex: "FF92DF")
|
||||
case 14: return SwiftUI.Color(hex: "A4FFFF")
|
||||
default: return SwiftUI.Color(hex: "FFFFFF")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,11 +43,18 @@ enum Dracula {
|
||||
NSColor(color(for: index))
|
||||
}
|
||||
|
||||
static let nsBackground = NSColor(Color(hex: "282A36"))
|
||||
static let nsSelection = NSColor(Color(hex: "44475A"))
|
||||
static let nsBackground = NSColor(SwiftUI.Color(hex: "282A36"))
|
||||
static let nsSelection = NSColor(SwiftUI.Color(hex: "44475A"))
|
||||
|
||||
/// 16 ANSI colors for the terminal emulator (SwiftTerm palette).
|
||||
static var ansiPalette: [SwiftTerm.Color] {
|
||||
["21222C", "FF5555", "50FA7B", "F1FA8C", "BD93F9", "FF79C6", "8BE9FD", "F8F8F2",
|
||||
"6272A4", "FF6E6E", "69FF94", "FFFFA5", "D6ACFF", "FF92DF", "A4FFFF", "FFFFFF"]
|
||||
.compactMap { SwiftTerm.Color.parse("#\($0)") }
|
||||
}
|
||||
}
|
||||
|
||||
extension Color {
|
||||
extension SwiftUI.Color {
|
||||
init(hex: String) {
|
||||
let clean = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||
var value: UInt64 = 0
|
||||
|
||||
Reference in New Issue
Block a user