improve ui on keydetailview

add set(keypair, onHost) to set a keypair on host
remove getkeys it always returns []
add delete button to
remove rsa from keytype
rename symbolpreview ->hostsymbolpreview
This commit is contained in:
neon443
2025-07-02 21:55:50 +01:00
parent 6ad8ed22a0
commit c9d7b06305
10 changed files with 110 additions and 88 deletions

View File

@@ -63,7 +63,7 @@
A9D8192F2E0F1BEE00442D38 /* ThemePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D8192E2E0F1BEE00442D38 /* ThemePreview.swift */; }; A9D8192F2E0F1BEE00442D38 /* ThemePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D8192E2E0F1BEE00442D38 /* ThemePreview.swift */; };
A9D819312E102D8700442D38 /* HostkeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D819302E102D8700442D38 /* HostkeysView.swift */; }; A9D819312E102D8700442D38 /* HostkeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D819302E102D8700442D38 /* HostkeysView.swift */; };
A9DA97712E0D30ED00142DDC /* HostSymbol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DA97702E0D30ED00142DDC /* HostSymbol.swift */; }; A9DA97712E0D30ED00142DDC /* HostSymbol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DA97702E0D30ED00142DDC /* HostSymbol.swift */; };
A9DA97732E0D40C100142DDC /* SymbolPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DA97722E0D40C100142DDC /* SymbolPreview.swift */; }; A9DA97732E0D40C100142DDC /* HostSymbolPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DA97722E0D40C100142DDC /* HostSymbolPreview.swift */; };
A9FD37552E143D23005319A8 /* SecKeyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD37542E143D23005319A8 /* SecKeyConvertible.swift */; }; A9FD37552E143D23005319A8 /* SecKeyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD37542E143D23005319A8 /* SecKeyConvertible.swift */; };
A9FD37572E143D5A005319A8 /* SecKeyStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD37562E143D5A005319A8 /* SecKeyStore.swift */; }; A9FD37572E143D5A005319A8 /* SecKeyStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD37562E143D5A005319A8 /* SecKeyStore.swift */; };
A9FD37592E143D74005319A8 /* GenericPasswordConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD37582E143D74005319A8 /* GenericPasswordConvertible.swift */; }; A9FD37592E143D74005319A8 /* GenericPasswordConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD37582E143D74005319A8 /* GenericPasswordConvertible.swift */; };
@@ -165,7 +165,7 @@
A9D8192E2E0F1BEE00442D38 /* ThemePreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemePreview.swift; sourceTree = "<group>"; }; A9D8192E2E0F1BEE00442D38 /* ThemePreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemePreview.swift; sourceTree = "<group>"; };
A9D819302E102D8700442D38 /* HostkeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostkeysView.swift; sourceTree = "<group>"; }; A9D819302E102D8700442D38 /* HostkeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostkeysView.swift; sourceTree = "<group>"; };
A9DA97702E0D30ED00142DDC /* HostSymbol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostSymbol.swift; sourceTree = "<group>"; }; A9DA97702E0D30ED00142DDC /* HostSymbol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostSymbol.swift; sourceTree = "<group>"; };
A9DA97722E0D40C100142DDC /* SymbolPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolPreview.swift; sourceTree = "<group>"; }; A9DA97722E0D40C100142DDC /* HostSymbolPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostSymbolPreview.swift; sourceTree = "<group>"; };
A9FD37542E143D23005319A8 /* SecKeyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecKeyConvertible.swift; sourceTree = "<group>"; }; A9FD37542E143D23005319A8 /* SecKeyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecKeyConvertible.swift; sourceTree = "<group>"; };
A9FD37562E143D5A005319A8 /* SecKeyStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecKeyStore.swift; sourceTree = "<group>"; }; A9FD37562E143D5A005319A8 /* SecKeyStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecKeyStore.swift; sourceTree = "<group>"; };
A9FD37582E143D74005319A8 /* GenericPasswordConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericPasswordConvertible.swift; sourceTree = "<group>"; }; A9FD37582E143D74005319A8 /* GenericPasswordConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericPasswordConvertible.swift; sourceTree = "<group>"; };
@@ -349,6 +349,7 @@
A93143C52DF61FE300FCD5DB /* ViewModifiers.swift */, A93143C52DF61FE300FCD5DB /* ViewModifiers.swift */,
A9B15A992E0ABA0400F66E02 /* DialogView.swift */, A9B15A992E0ABA0400F66E02 /* DialogView.swift */,
A96C90A02E12B87900724253 /* TextBox.swift */, A96C90A02E12B87900724253 /* TextBox.swift */,
A9DA97722E0D40C100142DDC /* HostSymbolPreview.swift */,
); );
path = Misc; path = Misc;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -379,7 +380,6 @@
A93143BF2DF61B3200FCD5DB /* Host.swift */, A93143BF2DF61B3200FCD5DB /* Host.swift */,
A98554602E058433009051BD /* HostsManager.swift */, A98554602E058433009051BD /* HostsManager.swift */,
A9DA97702E0D30ED00142DDC /* HostSymbol.swift */, A9DA97702E0D30ED00142DDC /* HostSymbol.swift */,
A9DA97722E0D40C100142DDC /* SymbolPreview.swift */,
); );
path = Host; path = Host;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -601,7 +601,7 @@
A93143C62DF61FE300FCD5DB /* ViewModifiers.swift in Sources */, A93143C62DF61FE300FCD5DB /* ViewModifiers.swift in Sources */,
A98554632E0587DF009051BD /* HostsView.swift in Sources */, A98554632E0587DF009051BD /* HostsView.swift in Sources */,
A96C6A8A2E0C0B1100F377FE /* SSHState.swift in Sources */, A96C6A8A2E0C0B1100F377FE /* SSHState.swift in Sources */,
A9DA97732E0D40C100142DDC /* SymbolPreview.swift in Sources */, A9DA97732E0D40C100142DDC /* HostSymbolPreview.swift in Sources */,
A96BE6A62E113DB000C0FEE9 /* ColorCodable.swift in Sources */, A96BE6A62E113DB000C0FEE9 /* ColorCodable.swift in Sources */,
A92538C82DEE0742007E0A18 /* ContentView.swift in Sources */, A92538C82DEE0742007E0A18 /* ContentView.swift in Sources */,
A96BE6A42E113D9400C0FEE9 /* ThemeCodable.swift in Sources */, A96BE6A42E113D9400C0FEE9 /* ThemeCodable.swift in Sources */,

View File

@@ -169,12 +169,10 @@ class HostsManager: ObservableObject, @unchecked Sendable {
} }
} }
func getKeys() -> [Keypair] { func set(keypair: Keypair, onHost: Host) {
var result: [Keypair] = [] guard let index = hosts.firstIndex(where: { $0.id == onHost.id }) else { return }
for host in hosts { hosts[index].privateKeyID = keypair.id
guard let keyID = host.privateKeyID else { continue } saveHosts()
}
return result
} }
func getHostsUsingKeys(_ keys: [Keypair]) -> [Host] { func getHostsUsingKeys(_ keys: [Keypair]) -> [Host] {

View File

@@ -140,7 +140,8 @@ class KeyManager: ObservableObject {
func importKey(type: KeyType, priv: String, name: String) { func importKey(type: KeyType, priv: String, name: String) {
if type == .ed25519 { if type == .ed25519 {
saveToKeychain(KeyManager.importSSHPrivkey(priv: priv)) guard let importedKeypair = KeyManager.importSSHPrivkey(priv: priv) else { return }
saveToKeychain(importedKeypair)
saveKeypairs() saveKeypairs()
} else { fatalError() } } else { fatalError() }
} }
@@ -156,8 +157,6 @@ class KeyManager: ObservableObject {
) )
saveToKeychain(keypair) saveToKeychain(keypair)
saveKeypairs() saveKeypairs()
case .rsa:
fatalError("unimplemented")
} }
loadKeypairs() loadKeypairs()
} }
@@ -187,7 +186,8 @@ class KeyManager: ObservableObject {
return Data(pubkeyline.utf8) return Data(pubkeyline.utf8)
} }
static func importSSHPrivkey(priv: String) -> Keypair { static func importSSHPrivkey(priv: String) -> Keypair? {
guard !priv.isEmpty else { return nil }
var split = priv.replacingOccurrences(of: "-----BEGIN OPENSSH PRIVATE KEY-----\n", with: "") var split = priv.replacingOccurrences(of: "-----BEGIN OPENSSH PRIVATE KEY-----\n", with: "")
split = split.replacingOccurrences(of: "-----BEGIN OPENSSH PRIVATE KEY-----", with: "") split = split.replacingOccurrences(of: "-----BEGIN OPENSSH PRIVATE KEY-----", with: "")
split = split.replacingOccurrences(of: "\n-----END OPENSSH PRIVATE KEY-----\n", with: "") split = split.replacingOccurrences(of: "\n-----END OPENSSH PRIVATE KEY-----\n", with: "")

View File

@@ -9,14 +9,11 @@ import Foundation
enum KeyType: Codable, Equatable, Hashable, CustomStringConvertible, CaseIterable { enum KeyType: Codable, Equatable, Hashable, CustomStringConvertible, CaseIterable {
case ed25519 case ed25519
case rsa
var description: String { var description: String {
switch self { switch self {
case .ed25519: case .ed25519:
return "Ed25519" return "Ed25519"
case .rsa:
return "RSA"
} }
} }
} }

View File

@@ -35,7 +35,7 @@ struct ConnectionView: View {
RoundedRectangle(cornerRadius: 10) RoundedRectangle(cornerRadius: 10)
.fill(.gray.opacity(0.5)) .fill(.gray.opacity(0.5))
} }
SymbolPreview(symbol: symbol, label: handler.host.label) HostSymbolPreview(symbol: symbol, label: handler.host.label)
.padding(5) .padding(5)
} }
.frame(width: 60, height: 60) .frame(width: 60, height: 60)
@@ -47,7 +47,7 @@ struct ConnectionView: View {
} }
HStack { HStack {
SymbolPreview(symbol: handler.host.symbol, label: handler.host.label) HostSymbolPreview(symbol: handler.host.symbol, label: handler.host.label)
.id(handler.host) .id(handler.host)
.frame(width: 60, height: 60) .frame(width: 60, height: 60)

View File

@@ -26,7 +26,7 @@ struct HostsView: View {
keyManager: keyManager keyManager: keyManager
) )
} label: { } label: {
SymbolPreview(symbol: host.symbol, label: host.label) HostSymbolPreview(symbol: host.symbol, label: host.label)
.frame(width: 40, height: 40) .frame(width: 40, height: 40)
Text(host.description) Text(host.description)
} }

View File

@@ -22,79 +22,102 @@ struct KeyDetailView: View {
hostsManager.selectedTheme.background.suiColor.opacity(0.7) hostsManager.selectedTheme.background.suiColor.opacity(0.7)
.ignoresSafeArea(.all) .ignoresSafeArea(.all)
List { List {
TextBox(label: "Name", text: $keyname, prompt: "A name for your key")
.onAppear {
keyname = keypair.name
}
.onChange(of: keyname) { _ in
keyManager.renameKey(keypair: keypair, newName: keyname)
}
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Used on") Text("Used on")
.bold() .bold()
ForEach(hostsManager.getHostsUsingKeys([keypair])) { host in ForEach(hostsManager.getHostsUsingKeys([keypair])) { host in
HStack { HStack {
SymbolPreview(symbol: host.symbol, label: host.label) HostSymbolPreview(symbol: host.symbol, label: host.label)
.frame(width: 40, height: 40) .frame(width: 40, height: 40)
Text(host.description) Text(host.description)
} }
} }
} Menu("Add") {
VStack(alignment: .leading) { let hostsNotUsingKey = hostsManager.hosts.filter(
Text("Public key") {
.bold() hostsManager.getHostsUsingKeys([keypair]).contains($0)
Text(keypair.openSshPubkey) })
} ForEach(hostsNotUsingKey) { host in
VStack(alignment: .leading) { Button() {
Text("Private key") hostsManager.set(keypair: keypair, onHost: host)
.bold() } label: {
.frame(maxWidth: .infinity) Image(systemName: "plus")
ZStack(alignment: .center) { .resizable().scaledToFit()
Text(keypair.openSshPrivkey) .foregroundStyle(.blue)
.blur(radius: reveal ? 0 : 5) .frame(width: 30, height: 30)
VStack { Text("Add")
Image(systemName: "eye.slash.fill") .foregroundStyle(.blue)
.resizable().scaledToFit()
.frame(width: 50)
Text("Tap to reveal")
}
.opacity(reveal ? 0 : 1)
}
.frame(maxWidth: .infinity)
.onTapGesture {
Task {
if !reveal {
guard await authWithBiometrics() else { return }
} }
withAnimation(.spring) { reveal.toggle() }
} }
} }
} }
Button { Section() {
UIPasteboard.general.string = keypair.openSshPubkey TextBox(label: "Name", text: $keyname, prompt: "A name for your key")
} label: { .onAppear {
CenteredLabel(title: "Copy public key", systemName: "document.on.document") keyname = keypair.name
} }
.listRowSeparator(.hidden) .onChange(of: keyname) { _ in
keyManager.renameKey(keypair: keypair, newName: keyname)
Button { }
Task {
guard await authWithBiometrics() else { return } Button {
UIPasteboard.general.string = String(data: KeyManager.makeSSHPrivkey(keypair), encoding: .utf8) ?? "" UIPasteboard.general.string = keypair.openSshPubkey
} label: {
CenteredLabel(title: "Copy public key", systemName: "document.on.document")
} }
} label: { .listRowSeparator(.hidden)
CenteredLabel(title: "Copy private key", systemName: "document.on.document")
} Button {
.listRowSeparator(.hidden) Task {
guard await authWithBiometrics() else { return }
CenteredLabel(title: "Delete", systemName: "trash") UIPasteboard.general.string = String(data: KeyManager.makeSSHPrivkey(keypair), encoding: .utf8) ?? ""
.foregroundStyle(.red) }
.onTapGesture { } label: {
keyManager.deleteKey(keypair) CenteredLabel(title: "Copy private key", systemName: "document.on.document")
dismiss()
} }
.listRowSeparator(.hidden)
CenteredLabel(title: "Delete", systemName: "trash")
.foregroundStyle(.red)
.onTapGesture {
keyManager.deleteKey(keypair)
dismiss()
}
}
Section("Key") {
VStack(alignment: .leading) {
Text("Public key")
.bold()
Text(keypair.openSshPubkey.dropLast(2))
}
VStack(alignment: .leading) {
Text("Private key")
.bold()
.frame(maxWidth: .infinity)
ZStack(alignment: .center) {
Text(keypair.openSshPrivkey.dropLast(2))
.blur(radius: reveal ? 0 : 5)
VStack {
Image(systemName: "eye.slash.fill")
.resizable().scaledToFit()
.frame(width: 50)
Text("Tap to reveal")
}
.opacity(reveal ? 0 : 1)
}
.frame(maxWidth: .infinity)
.onTapGesture {
Task {
if !reveal {
guard await authWithBiometrics() else { return }
}
withAnimation(.spring) { reveal.toggle() }
}
}
}
}
} }
.scrollContentBackground(.hidden) .scrollContentBackground(.hidden)
} }

View File

@@ -46,14 +46,18 @@ struct KeyImporterView: View {
.foregroundStyle(.gray) .foregroundStyle(.gray)
} }
Button() {
keyManager.importKey(type: keyType, priv: privkeyStr, name: keyName)
dismiss()
} label: {
Label("Import", systemImage: "key.horizontal")
}
.buttonStyle(.borderedProminent)
} }
Button() {
keyManager.importKey(type: keyType, priv: privkeyStr, name: keyName)
dismiss()
} label: {
Text("Import")
}
.onTapGesture {
UINotificationFeedbackGenerator().notificationOccurred(.success)
}
.buttonStyle(.borderedProminent)
} }
} }

View File

@@ -1,5 +1,5 @@
// //
// SymbolPreview.swift // HostSymbolPreview.swift
// ShhShell // ShhShell
// //
// Created by neon443 on 26/06/2025. // Created by neon443 on 26/06/2025.
@@ -7,7 +7,7 @@
import SwiftUI import SwiftUI
struct SymbolPreview: View { struct HostSymbolPreview: View {
@State var symbol: HostSymbol @State var symbol: HostSymbol
@State var label: String @State var label: String
@@ -30,5 +30,5 @@ struct SymbolPreview: View {
} }
#Preview { #Preview {
SymbolPreview(symbol: HostSymbol.desktopcomputer, label: "lo0") HostSymbolPreview(symbol: HostSymbol.desktopcomputer, label: "lo0")
} }

View File

@@ -28,7 +28,7 @@ struct SessionView: View {
.resizable().scaledToFit() .resizable().scaledToFit()
.frame(width: 40, height: 40) .frame(width: 40, height: 40)
.foregroundStyle(.terminalGreen) .foregroundStyle(.terminalGreen)
SymbolPreview(symbol: host.symbol, label: host.label) HostSymbolPreview(symbol: host.symbol, label: host.label)
.frame(width: 40, height: 40) .frame(width: 40, height: 40)
Text(host.description) Text(host.description)
} }