mirror of
https://github.com/neon443/ShhShell.git
synced 2026-03-11 13:26:16 +00:00
implemented actually using Host.publicKey and Host.privateKey to store pubkeys and privkeys
- this gives u icloud synced pub/privkeys - per host!! made it load the key's string representation (if present) into the textboxes to tell the user that the key is there made the privkey box a securefield to show .... instead of the key added a handler.go() to handler disconnecting if connected and choosing password/pubkey auth - really cleaned up the view code added getPubkey(_: SecKey) to keychainmanager added SSHErrors - commfail, connectionFail, backenderror added AuthError - rejectedCreds, notconnected added KeyError - importpub/privError, pub/priv rejected made most functions throw in sshhandler fix memoryLayoutsize(ofvalue to buffer.count in testexec [weak self] in the async loop to prevent memory leaks i guess added loccritical for easy red logging
This commit is contained in:
@@ -32,6 +32,7 @@
|
|||||||
A985545F2E056EDD009051BD /* KeychainLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A985545E2E056EDD009051BD /* KeychainLayer.swift */; };
|
A985545F2E056EDD009051BD /* KeychainLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A985545E2E056EDD009051BD /* KeychainLayer.swift */; };
|
||||||
A98554612E058433009051BD /* HostsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554602E058433009051BD /* HostsManager.swift */; };
|
A98554612E058433009051BD /* HostsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554602E058433009051BD /* HostsManager.swift */; };
|
||||||
A98554632E0587DF009051BD /* HostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554622E0587DF009051BD /* HostsView.swift */; };
|
A98554632E0587DF009051BD /* HostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98554622E0587DF009051BD /* HostsView.swift */; };
|
||||||
|
A9C4140C2E096DB7005E3047 /* SSHError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C4140B2E096DB7005E3047 /* SSHError.swift */; };
|
||||||
A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */; };
|
A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
@@ -97,6 +98,7 @@
|
|||||||
A985545E2E056EDD009051BD /* KeychainLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainLayer.swift; sourceTree = "<group>"; };
|
A985545E2E056EDD009051BD /* KeychainLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainLayer.swift; sourceTree = "<group>"; };
|
||||||
A98554602E058433009051BD /* HostsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostsManager.swift; sourceTree = "<group>"; };
|
A98554602E058433009051BD /* HostsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostsManager.swift; sourceTree = "<group>"; };
|
||||||
A98554622E0587DF009051BD /* HostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostsView.swift; sourceTree = "<group>"; };
|
A98554622E0587DF009051BD /* HostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostsView.swift; sourceTree = "<group>"; };
|
||||||
|
A9C4140B2E096DB7005E3047 /* SSHError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHError.swift; sourceTree = "<group>"; };
|
||||||
A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHHandler.swift; sourceTree = "<group>"; };
|
A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHHandler.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
@@ -219,6 +221,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */,
|
A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */,
|
||||||
|
A9C4140B2E096DB7005E3047 /* SSHError.swift */,
|
||||||
);
|
);
|
||||||
path = SSH;
|
path = SSH;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -425,6 +428,7 @@
|
|||||||
A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */,
|
A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */,
|
||||||
A98554552E05535F009051BD /* KeyManagerView.swift in Sources */,
|
A98554552E05535F009051BD /* KeyManagerView.swift in Sources */,
|
||||||
A923172D2E07138000ECE1E6 /* SSHTerminalView.swift in Sources */,
|
A923172D2E07138000ECE1E6 /* SSHTerminalView.swift in Sources */,
|
||||||
|
A9C4140C2E096DB7005E3047 /* SSHError.swift in Sources */,
|
||||||
A923172A2E07113100ECE1E6 /* TerminalController.swift in Sources */,
|
A923172A2E07113100ECE1E6 /* TerminalController.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
import Security
|
||||||
|
|
||||||
struct Key: Identifiable, Hashable {
|
struct Key: Identifiable, Hashable {
|
||||||
var id = UUID()
|
var id = UUID()
|
||||||
@@ -62,10 +63,15 @@ class KeyManager: ObservableObject {
|
|||||||
print(privateKey)
|
print(privateKey)
|
||||||
|
|
||||||
print(SecKeyCopyPublicKey(privateKey) ?? "")
|
print(SecKeyCopyPublicKey(privateKey) ?? "")
|
||||||
|
print(SecKeyCopyExternalRepresentation(privateKey, nil) as Any)
|
||||||
// do {
|
// do {
|
||||||
// try storeKey(privateKey, label: label)
|
// try storeKey(privateKey, label: label)
|
||||||
// } catch {
|
// } catch {
|
||||||
// print(error.localizedDescription)
|
// print(error.localizedDescription)
|
||||||
// }
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPubkey(_ privateKey: SecKey) -> SecKey? {
|
||||||
|
return SecKeyCopyPublicKey(privateKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
ShhShell/SSH/SSHError.swift
Normal file
26
ShhShell/SSH/SSHError.swift
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// SSHError.swift
|
||||||
|
// ShhShell
|
||||||
|
//
|
||||||
|
// Created by neon443 on 23/06/2025.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum SSHError: Error {
|
||||||
|
case connectionFailed(String)
|
||||||
|
case communicationError(String)
|
||||||
|
case backendError(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AuthError: Error {
|
||||||
|
case rejectedCredentials
|
||||||
|
case notConnected
|
||||||
|
}
|
||||||
|
|
||||||
|
enum KeyError: Error {
|
||||||
|
case importPubkeyError
|
||||||
|
case importPrivkeyError
|
||||||
|
case pubkeyRejected
|
||||||
|
case privkeyRejected
|
||||||
|
}
|
||||||
@@ -45,11 +45,31 @@ class SSHHandler: ObservableObject {
|
|||||||
return Data(base64Encoded: String(cString: data))
|
return Data(base64Encoded: String(cString: data))
|
||||||
}
|
}
|
||||||
|
|
||||||
// func connect(_: Host) {
|
func go() {
|
||||||
//
|
guard !connected else {
|
||||||
// }
|
disconnect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let _ = try? connect() else { return }
|
||||||
|
if !host.password.isEmpty {
|
||||||
|
do { try authWithPw() } catch {
|
||||||
|
print("pw auth error")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
do {
|
||||||
|
if let publicKey = host.publicKey,
|
||||||
|
let privateKey = host.privateKey {
|
||||||
|
try authWithPubkey(pub: publicKey, priv: privateKey, pass: host.passphrase)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("error with pubkey auth")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
openShell()
|
||||||
|
}
|
||||||
|
|
||||||
func connect() {
|
func connect() throws(SSHError) {
|
||||||
defer {
|
defer {
|
||||||
getAuthMethods()
|
getAuthMethods()
|
||||||
self.host.key = getHostkey()
|
self.host.key = getHostkey()
|
||||||
@@ -60,7 +80,7 @@ class SSHHandler: ObservableObject {
|
|||||||
session = ssh_new()
|
session = ssh_new()
|
||||||
guard session != nil else {
|
guard session != nil else {
|
||||||
withAnimation { connected = false }
|
withAnimation { connected = false }
|
||||||
return
|
throw .backendError("Failed opening session")
|
||||||
}
|
}
|
||||||
|
|
||||||
ssh_options_set(session, SSH_OPTIONS_HOST, host.address)
|
ssh_options_set(session, SSH_OPTIONS_HOST, host.address)
|
||||||
@@ -72,24 +92,20 @@ class SSHHandler: ObservableObject {
|
|||||||
logger.critical("connection not ok: \(status)")
|
logger.critical("connection not ok: \(status)")
|
||||||
logSshGetError()
|
logSshGetError()
|
||||||
withAnimation { connected = false }
|
withAnimation { connected = false }
|
||||||
return
|
throw .connectionFailed("Failed connecting")
|
||||||
}
|
}
|
||||||
withAnimation { connected = true }
|
withAnimation { connected = true }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func disconnect() {
|
func disconnect() {
|
||||||
guard session != nil else {
|
guard session != nil else { return }
|
||||||
print("cant disconnect when im not connected")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ssh_disconnect(session)
|
ssh_disconnect(session)
|
||||||
ssh_free(session)
|
ssh_free(session)
|
||||||
withAnimation { authorized = false }
|
withAnimation { authorized = false }
|
||||||
withAnimation { connected = false }
|
withAnimation { connected = false }
|
||||||
withAnimation { testSuceeded = nil }
|
withAnimation { testSuceeded = nil }
|
||||||
session = nil
|
session = nil
|
||||||
// host.key = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testExec() {
|
func testExec() {
|
||||||
@@ -135,7 +151,7 @@ class SSHHandler: ObservableObject {
|
|||||||
nbytes = ssh_channel_read(
|
nbytes = ssh_channel_read(
|
||||||
channel,
|
channel,
|
||||||
&buffer,
|
&buffer,
|
||||||
UInt32(MemoryLayout.size(ofValue: CChar.self)),
|
UInt32(buffer.count),
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
while nbytes > 0 {
|
while nbytes > 0 {
|
||||||
@@ -148,7 +164,7 @@ class SSHHandler: ObservableObject {
|
|||||||
withAnimation { testSuceeded = false }
|
withAnimation { testSuceeded = false }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
nbytes = ssh_channel_read(channel, &buffer, UInt32(MemoryLayout.size(ofValue: Character.self)), 0)
|
nbytes = ssh_channel_read(channel, &buffer, UInt32(buffer.count), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if nbytes < 0 {
|
if nbytes < 0 {
|
||||||
@@ -168,7 +184,7 @@ class SSHHandler: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func authWithPubkey(pub pubInp: Data, priv privInp: Data, pass: String) {
|
func authWithPubkey(pub pubInp: Data, priv privInp: Data, pass: String) throws(KeyError) {
|
||||||
guard session != nil else {
|
guard session != nil else {
|
||||||
withAnimation { authorized = false }
|
withAnimation { authorized = false }
|
||||||
return
|
return
|
||||||
@@ -191,23 +207,21 @@ class SSHHandler: ObservableObject {
|
|||||||
|
|
||||||
var pubkey: ssh_key?
|
var pubkey: ssh_key?
|
||||||
if ssh_pki_import_pubkey_file(tempPubkey.path(), &pubkey) != 0 {
|
if ssh_pki_import_pubkey_file(tempPubkey.path(), &pubkey) != 0 {
|
||||||
print("pubkey import error")
|
throw .importPrivkeyError
|
||||||
}
|
}
|
||||||
|
|
||||||
if ssh_userauth_try_publickey(session, nil, pubkey) != 0 {
|
if ssh_userauth_try_publickey(session, nil, pubkey) != 0 {
|
||||||
print("pubkey pubkey auth error")
|
throw .pubkeyRejected
|
||||||
}
|
}
|
||||||
|
|
||||||
var privkey: ssh_key?
|
var privkey: ssh_key?
|
||||||
if ssh_pki_import_privkey_file(tempKey.path(), pass, nil, nil, &privkey) != 0 {
|
if ssh_pki_import_privkey_file(tempKey.path(), pass, nil, nil, &privkey) != 0 {
|
||||||
print("privkey import error")
|
throw .importPrivkeyError
|
||||||
print("likely incorrect passphrase")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ssh_userauth_publickey(session, nil, privkey) != 0) {
|
if (ssh_userauth_publickey(session, nil, privkey) != 0) {
|
||||||
withAnimation { authorized = false }
|
withAnimation { authorized = false }
|
||||||
print("auth failed lol")
|
throw .privkeyRejected
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//if u got this far, youre authed!
|
//if u got this far, youre authed!
|
||||||
@@ -223,26 +237,24 @@ class SSHHandler: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func authWithPw() -> Bool {
|
func authWithPw() throws(AuthError) {
|
||||||
var status: CInt
|
var status: CInt
|
||||||
status = ssh_userauth_password(session, host.username, host.password)
|
status = ssh_userauth_password(session, host.username, host.password)
|
||||||
guard status == SSH_AUTH_SUCCESS.rawValue else {
|
guard status == SSH_AUTH_SUCCESS.rawValue else {
|
||||||
print("ssh pw auth error")
|
|
||||||
logSshGetError()
|
logSshGetError()
|
||||||
return false
|
throw .rejectedCredentials
|
||||||
}
|
}
|
||||||
print("auth success")
|
|
||||||
withAnimation { authorized = true }
|
withAnimation { authorized = true }
|
||||||
return true
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func authWithNone() -> Bool {
|
func authWithNone() throws(AuthError) {
|
||||||
let status = ssh_userauth_none(session, nil)
|
let status = ssh_userauth_none(session, nil)
|
||||||
guard status == SSH_AUTH_SUCCESS.rawValue else { return false }
|
guard status == SSH_AUTH_SUCCESS.rawValue else { throw .rejectedCredentials }
|
||||||
|
|
||||||
print("no security moment lol")
|
logCritical("no security moment lol")
|
||||||
withAnimation { authorized = true }
|
withAnimation { authorized = true }
|
||||||
return true
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//always unknown idk why
|
//always unknown idk why
|
||||||
@@ -324,6 +336,10 @@ class SSHHandler: ObservableObject {
|
|||||||
logger.critical("\(String(cString: ssh_get_error(&self.session)))")
|
logger.critical("\(String(cString: ssh_get_error(&self.session)))")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func logCritical(_ logMessage: String) {
|
||||||
|
logger.critical("\(logMessage)")
|
||||||
|
}
|
||||||
|
|
||||||
func writeToChannel(_ string: String?) {
|
func writeToChannel(_ string: String?) {
|
||||||
guard let string = string else { return }
|
guard let string = string else { return }
|
||||||
guard ssh_channel_is_open(channel) != 0 else { return }
|
guard ssh_channel_is_open(channel) != 0 else { return }
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ struct ConnectionView: View {
|
|||||||
@State var pubkeyStr: String = ""
|
@State var pubkeyStr: String = ""
|
||||||
@State var privkeyStr: String = ""
|
@State var privkeyStr: String = ""
|
||||||
|
|
||||||
@State var pubkey: Data?
|
|
||||||
@State var privkey: Data?
|
|
||||||
|
|
||||||
@State var privPickerPresented: Bool = false
|
@State var privPickerPresented: Bool = false
|
||||||
@State var pubPickerPresented: Bool = false
|
@State var pubPickerPresented: Bool = false
|
||||||
|
|
||||||
@@ -65,7 +62,7 @@ struct ConnectionView: View {
|
|||||||
TextField("", text: $pubkeyStr, prompt: Text("Public Key"))
|
TextField("", text: $pubkeyStr, prompt: Text("Public Key"))
|
||||||
.onSubmit {
|
.onSubmit {
|
||||||
let newStr = pubkeyStr.replacingOccurrences(of: "\r\n", with: "")
|
let newStr = pubkeyStr.replacingOccurrences(of: "\r\n", with: "")
|
||||||
pubkey = Data(newStr.utf8)
|
handler.host.publicKey = Data(newStr.utf8)
|
||||||
}
|
}
|
||||||
Button() {
|
Button() {
|
||||||
pubPickerPresented.toggle()
|
pubPickerPresented.toggle()
|
||||||
@@ -81,7 +78,7 @@ struct ConnectionView: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer { fileURL.stopAccessingSecurityScopedResource() }
|
defer { fileURL.stopAccessingSecurityScopedResource() }
|
||||||
pubkey = try? Data(contentsOf: fileURL)
|
handler.host.publicKey = try? Data(contentsOf: fileURL)
|
||||||
print(fileURL)
|
print(fileURL)
|
||||||
} catch {
|
} catch {
|
||||||
print(error.localizedDescription)
|
print(error.localizedDescription)
|
||||||
@@ -90,10 +87,10 @@ struct ConnectionView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
TextField("", text: $privkeyStr, prompt: Text("Private Key"))
|
SecureField("", text: $privkeyStr, prompt: Text("Private Key"))
|
||||||
.onSubmit {
|
.onSubmit {
|
||||||
let newStr = privkeyStr.replacingOccurrences(of: "\r\n", with: "")
|
let newStr = privkeyStr.replacingOccurrences(of: "\r\n", with: "")
|
||||||
privkey = Data(newStr.utf8)
|
handler.host.privateKey = Data(newStr.utf8)
|
||||||
}
|
}
|
||||||
Button() {
|
Button() {
|
||||||
privPickerPresented.toggle()
|
privPickerPresented.toggle()
|
||||||
@@ -109,8 +106,8 @@ struct ConnectionView: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer { fileURL.stopAccessingSecurityScopedResource() }
|
defer { fileURL.stopAccessingSecurityScopedResource() }
|
||||||
privkey = try? Data(contentsOf: fileURL)
|
handler.host.privateKey = try? Data(contentsOf: fileURL)
|
||||||
print(privkey ?? "")
|
print(handler.host.privateKey ?? "")
|
||||||
print(fileURL)
|
print(fileURL)
|
||||||
} catch {
|
} catch {
|
||||||
print(error.localizedDescription)
|
print(error.localizedDescription)
|
||||||
@@ -164,27 +161,12 @@ struct ConnectionView: View {
|
|||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem() {
|
ToolbarItem() {
|
||||||
if handler.connected {
|
Button() {
|
||||||
Button() {
|
handler.go()
|
||||||
handler.disconnect()
|
} label: {
|
||||||
} label: {
|
Label(
|
||||||
Label("Disconnect", systemImage: "xmark.app.fill")
|
handler.connected ? "Disconnect" : "Connect",
|
||||||
}
|
systemImage: handler.connected ? "xmark.app.fill" : "power"
|
||||||
} else {
|
|
||||||
Button() {
|
|
||||||
handler.connect()
|
|
||||||
if pubkey != nil && privkey != nil {
|
|
||||||
handler.authWithPubkey(pub: pubkey!, priv: privkey!, pass: passphrase)
|
|
||||||
} else {
|
|
||||||
let _ = handler.authWithPw()
|
|
||||||
}
|
|
||||||
handler.openShell()
|
|
||||||
} label: {
|
|
||||||
Label("Connect", systemImage: "power")
|
|
||||||
}
|
|
||||||
.disabled(
|
|
||||||
pubkey == nil && privkey == nil &&
|
|
||||||
handler.host.username.isEmpty && handler.host.password.isEmpty
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,6 +178,14 @@ struct ConnectionView: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.task {
|
||||||
|
if let publicKeyData = handler.host.publicKey {
|
||||||
|
pubkeyStr = String(data: publicKeyData, encoding: .utf8) ?? ""
|
||||||
|
}
|
||||||
|
if let privateKeyData = handler.host.privateKey {
|
||||||
|
privkeyStr = String(data: privateKeyData, encoding: .utf8) ?? ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,13 @@ class SSHTerminalView: TerminalView, TerminalViewDelegate {
|
|||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
terminalDelegate = self
|
terminalDelegate = self
|
||||||
sshQueue.async {
|
sshQueue.async { [weak self] in
|
||||||
guard let handler = self.handler else { return }
|
guard let handler = self?.handler else { return }
|
||||||
|
|
||||||
while handler.connected {
|
while handler.connected {
|
||||||
guard let handler = self.handler else { break }
|
guard let handler = self?.handler else { break }
|
||||||
if let read = handler.readFromChannel() {
|
if let read = handler.readFromChannel() {
|
||||||
DispatchQueue.main.async { self.feed(text: read) }
|
DispatchQueue.main.async { self?.feed(text: read) }
|
||||||
} else {
|
} else {
|
||||||
usleep(1_000)
|
usleep(1_000)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,17 +18,12 @@ struct ShellView: View {
|
|||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem {
|
ToolbarItem {
|
||||||
Button() {
|
Button() {
|
||||||
if handler.connected {
|
handler.go()
|
||||||
handler.disconnect()
|
|
||||||
} else {
|
|
||||||
handler.connect()
|
|
||||||
}
|
|
||||||
} label: {
|
} label: {
|
||||||
if handler.connected {
|
Label(
|
||||||
Label("Disconnect", systemImage: "xmark.square.fill")
|
handler.connected ? "Disconnect" : "Connect",
|
||||||
} else {
|
systemImage: handler.connected ? "xmark.app.fill" : "power"
|
||||||
Label("Connect", image: "power")
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user