fix adding new hosts, it will navigate u to a new connection view with a host.blank, and add the host if it doesnt exist

added addhostifneeded
extracted hostskeys stuff to hostkeysview
added nav titles to everything
removed tabs, now just a list
renamed savedHosts -> hosts
This commit is contained in:
neon443
2025-06-28 15:37:50 +01:00
parent 7ceef899df
commit 4affc532d9
9 changed files with 185 additions and 156 deletions

View File

@@ -42,6 +42,7 @@
A9D819292E0E904200442D38 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D819282E0E904200442D38 /* Theme.swift */; }; A9D819292E0E904200442D38 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D819282E0E904200442D38 /* Theme.swift */; };
A9D8192D2E0E9EB500442D38 /* ThemeManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D8192C2E0E9EB500442D38 /* ThemeManagerView.swift */; }; A9D8192D2E0E9EB500442D38 /* ThemeManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D8192C2E0E9EB500442D38 /* ThemeManagerView.swift */; };
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 */; };
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 /* SymbolPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DA97722E0D40C100142DDC /* SymbolPreview.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@@ -118,6 +119,7 @@
A9D819282E0E904200442D38 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; }; A9D819282E0E904200442D38 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
A9D8192C2E0E9EB500442D38 /* ThemeManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManagerView.swift; sourceTree = "<group>"; }; A9D8192C2E0E9EB500442D38 /* ThemeManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManagerView.swift; sourceTree = "<group>"; };
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>"; };
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 /* SymbolPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolPreview.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@@ -282,6 +284,7 @@
children = ( children = (
A96C6AFF2E0C45FE00F377FE /* KeyDetailView.swift */, A96C6AFF2E0C45FE00F377FE /* KeyDetailView.swift */,
A98554542E05535F009051BD /* KeyManagerView.swift */, A98554542E05535F009051BD /* KeyManagerView.swift */,
A9D819302E102D8700442D38 /* HostkeysView.swift */,
); );
path = Keys; path = Keys;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -497,6 +500,7 @@
A985545D2E055D4D009051BD /* ConnectionView.swift in Sources */, A985545D2E055D4D009051BD /* ConnectionView.swift in Sources */,
A98554592E0553AA009051BD /* KeyManager.swift in Sources */, A98554592E0553AA009051BD /* KeyManager.swift in Sources */,
A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */, A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */,
A9D819312E102D8700442D38 /* HostkeysView.swift in Sources */,
A98554552E05535F009051BD /* KeyManagerView.swift in Sources */, A98554552E05535F009051BD /* KeyManagerView.swift in Sources */,
A923172D2E07138000ECE1E6 /* SSHTerminalDelegate.swift in Sources */, A923172D2E07138000ECE1E6 /* SSHTerminalDelegate.swift in Sources */,
A96C6AFE2E0C43B600F377FE /* Keypair.swift in Sources */, A96C6AFE2E0C43B600F377FE /* Keypair.swift in Sources */,

View File

@@ -12,11 +12,11 @@ import SwiftUI
class HostsManager: ObservableObject, @unchecked Sendable { class HostsManager: ObservableObject, @unchecked Sendable {
private let userDefaults = NSUbiquitousKeyValueStore.default private let userDefaults = NSUbiquitousKeyValueStore.default
@Published var savedHosts: [Host] = [] @Published var hosts: [Host] = []
@Published var themes: [Theme] = [] @Published var themes: [Theme] = []
init() { init() {
loadSavedHosts() loadHosts()
loadThemes() loadThemes()
} }
@@ -84,7 +84,7 @@ class HostsManager: ObservableObject, @unchecked Sendable {
} }
func getHostIndexMatching(_ hostSearchingFor: Host) -> Int? { func getHostIndexMatching(_ hostSearchingFor: Host) -> Int? {
if let index = savedHosts.firstIndex(where: { $0.id == hostSearchingFor.id }) { if let index = hosts.firstIndex(where: { $0.id == hostSearchingFor.id }) {
return index return index
} else { } else {
return nil return nil
@@ -93,28 +93,28 @@ class HostsManager: ObservableObject, @unchecked Sendable {
func getHostMatching(_ HostSearchingFor: Host) -> Host? { func getHostMatching(_ HostSearchingFor: Host) -> Host? {
guard let index = getHostIndexMatching(HostSearchingFor) else { return nil } guard let index = getHostIndexMatching(HostSearchingFor) else { return nil }
return savedHosts[index] return hosts[index]
} }
func updateHost(_ updatedHost: Host) { func updateHost(_ updatedHost: Host) {
let oldID = updatedHost.id let oldID = updatedHost.id
if let index = savedHosts.firstIndex(where: { $0.id == updatedHost.id }) { if let index = hosts.firstIndex(where: { $0.id == updatedHost.id }) {
var updateHostWithNewID = updatedHost var updateHostWithNewID = updatedHost
updateHostWithNewID.id = UUID() updateHostWithNewID.id = UUID()
withAnimation { savedHosts[index] = updateHostWithNewID } withAnimation { hosts[index] = updateHostWithNewID }
updateHostWithNewID.id = oldID updateHostWithNewID.id = oldID
withAnimation { savedHosts[index] = updateHostWithNewID } withAnimation { hosts[index] = updateHostWithNewID }
saveSavedHosts() saveHosts()
} }
} }
func duplicateHost(_ hostToDup: Host) { func duplicateHost(_ hostToDup: Host) {
var hostNewID = hostToDup var hostNewID = hostToDup
hostNewID.id = UUID() hostNewID.id = UUID()
if let index = savedHosts.firstIndex(where: { $0 == hostToDup }) { if let index = hosts.firstIndex(where: { $0 == hostToDup }) {
savedHosts.insert(hostNewID, at: index+1) hosts.insert(hostNewID, at: index+1)
} }
} }
@@ -131,38 +131,44 @@ class HostsManager: ObservableObject, @unchecked Sendable {
} }
func moveHost(from: IndexSet, to: Int) { func moveHost(from: IndexSet, to: Int) {
savedHosts.move(fromOffsets: from, toOffset: to) hosts.move(fromOffsets: from, toOffset: to)
saveSavedHosts() saveHosts()
} }
func loadSavedHosts() { func loadHosts() {
userDefaults.synchronize() userDefaults.synchronize()
let decoder = JSONDecoder() let decoder = JSONDecoder()
guard let data = userDefaults.data(forKey: "savedHosts") else { return } guard let data = userDefaults.data(forKey: "savedHosts") else { return }
if let decoded = try? decoder.decode([Host].self, from: data) { if let decoded = try? decoder.decode([Host].self, from: data) {
self.savedHosts = decoded self.hosts = decoded
} }
} }
func saveSavedHosts() { func saveHosts() {
let encoder = JSONEncoder() let encoder = JSONEncoder()
if let encoded = try? encoder.encode(savedHosts) { if let encoded = try? encoder.encode(hosts) {
userDefaults.set(encoded, forKey: "savedHosts") userDefaults.set(encoded, forKey: "savedHosts")
userDefaults.synchronize() userDefaults.synchronize()
} }
} }
func addHostIfNeeded(_ hostToAdd: Host) {
if !hosts.contains(hostToAdd) {
hosts.append(hostToAdd)
}
}
func removeHost(_ host: Host) { func removeHost(_ host: Host) {
if let index = savedHosts.firstIndex(where: { $0.id == host.id }) { if let index = hosts.firstIndex(where: { $0.id == host.id }) {
let _ = withAnimation { savedHosts.remove(at: index) } let _ = withAnimation { hosts.remove(at: index) }
saveSavedHosts() saveHosts()
} }
} }
func getKeys() -> [Keypair] { func getKeys() -> [Keypair] {
var result: [Keypair] = [] var result: [Keypair] = []
for host in savedHosts { for host in hosts {
let keypair = Keypair(publicKey: host.publicKey, privateKey: host.privateKey) let keypair = Keypair(publicKey: host.publicKey, privateKey: host.privateKey)
if !result.contains(keypair) { if !result.contains(keypair) {
result.append(keypair) result.append(keypair)
@@ -174,7 +180,7 @@ class HostsManager: ObservableObject, @unchecked Sendable {
func getHostsKeysUsedOn(_ keys: [Keypair]) -> [Host] { func getHostsKeysUsedOn(_ keys: [Keypair]) -> [Host] {
var result: [Host] = [] var result: [Host] = []
for key in keys { for key in keys {
let hosts = savedHosts.filter({ let hosts = hosts.filter({
$0.publicKey == key.publicKey && $0.publicKey == key.publicKey &&
$0.privateKey == key.privateKey $0.privateKey == key.privateKey
}) })

View File

@@ -17,7 +17,7 @@ struct ShhShellApp: App {
WindowGroup { WindowGroup {
ContentView( ContentView(
handler: sshHandler, handler: sshHandler,
hostsManger: hostsManager, hostsManager: hostsManager,
keyManager: keyManager keyManager: keyManager
) )
.colorScheme(.dark) .colorScheme(.dark)

View File

@@ -9,31 +9,38 @@ import SwiftUI
struct ContentView: View { struct ContentView: View {
@ObservedObject var handler: SSHHandler @ObservedObject var handler: SSHHandler
@ObservedObject var hostsManger: HostsManager @ObservedObject var hostsManager: HostsManager
@ObservedObject var keyManager: KeyManager @ObservedObject var keyManager: KeyManager
var body: some View { var body: some View {
TabView { NavigationStack {
HostsView( List {
handler: handler, HostsView(
hostsManager: hostsManger, handler: handler,
keyManager: keyManager hostsManager: hostsManager,
) keyManager: keyManager
.tabItem { )
Label("Hosts", systemImage: "server.rack")
} NavigationLink {
KeyManagerView(hostsManager: hostsManger, keyManager: keyManager) KeyManagerView(hostsManager: hostsManager, keyManager: keyManager)
.tabItem { } label: {
Label("Keys", systemImage: "key.2.on.ring") Label("Keys", systemImage: "key.fill")
} }
NavigationLink {
HostkeysView(hostsManager: hostsManager)
} label: {
Label("Hostkey Fingerprints", systemImage: "lock.display")
}
}
} }
} }
} }
#Preview { #Preview {
ContentView( ContentView(
handler: SSHHandler(host: Host.debug), handler: SSHHandler(host: Host.debug),
hostsManger: HostsManager(), hostsManager: HostsManager(),
keyManager: KeyManager() keyManager: KeyManager()
) )
} }

View File

@@ -188,6 +188,9 @@ struct ConnectionView: View {
shellView = ShellView(handler: handler) shellView = ShellView(handler: handler)
} }
} }
.onAppear {
hostsManager.addHostIfNeeded(handler.host)
}
} }
} }

View File

@@ -12,89 +12,76 @@ struct HostsView: View {
@ObservedObject var hostsManager: HostsManager @ObservedObject var hostsManager: HostsManager
@ObservedObject var keyManager: KeyManager @ObservedObject var keyManager: KeyManager
var body: some View { var body: some View {
NavigationStack { if hostsManager.hosts.isEmpty {
List { Text("Add your first Host!")
if hostsManager.savedHosts.isEmpty { }
Text("Add your first Host!")
Button() {
withAnimation { hostsManager.savedHosts.append(Host.blank) }
} label: {
Text("Create")
}
.buttonStyle(.borderedProminent)
}
//proves that u can connect to multiple at the same time //proves that u can connect to multiple at the same time
NavigationLink() { NavigationLink() {
ForEach(hostsManager.savedHosts) { host in ForEach(hostsManager.hosts) { host in
let miniHandler = SSHHandler(host: host) let miniHandler = SSHHandler(host: host)
TerminalController(handler: miniHandler) TerminalController(handler: miniHandler)
.onAppear { miniHandler.go() } .onAppear { miniHandler.go() }
}
} label: {
Label("multiview", systemImage: "square.split.2x2")
}
ForEach(hostsManager.savedHosts) { host in
NavigationLink() {
ConnectionView(
handler: SSHHandler(host: host),
hostsManager: hostsManager,
keyManager: keyManager
)
} label: {
SymbolPreview(symbol: host.symbol, label: host.label)
.frame(width: 40, height: 40)
Text(hostsManager.makeLabel(forHost: host))
}
.id(host)
.animation(.default, value: host)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
hostsManager.removeHost(host)
} label: {
Label("Delete", systemImage: "trash")
}
Button() {
hostsManager.duplicateHost(host)
} label: {
Label("Duplicate", systemImage: "square.filled.on.square")
}
}
}
.onMove(perform: {
hostsManager.moveHost(from: $0, to: $1)
})
Section() {
NavigationLink {
ThemeManagerView(hostsManager: hostsManager)
} label: {
Label("Themes", systemImage: "swatchpalette")
}
}
} }
.transition(.opacity) } label: {
.toolbar { Label("multiview", systemImage: "square.split.2x2")
ToolbarItem(placement: .confirmationAction) { }
let host = Host.blank
NavigationLink { ForEach(hostsManager.hosts) { host in
ConnectionView( NavigationLink() {
handler: SSHHandler(host: host), ConnectionView(
hostsManager: hostsManager, handler: SSHHandler(host: host),
keyManager: keyManager hostsManager: hostsManager,
) keyManager: keyManager
.onAppear { )
withAnimation { hostsManager.savedHosts.append(host) } } label: {
} SymbolPreview(symbol: host.symbol, label: host.label)
} label: { .frame(width: 40, height: 40)
Label("Add", systemImage: "plus") Text(hostsManager.makeLabel(forHost: host))
} }
.id(host)
.animation(.default, value: host)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
hostsManager.removeHost(host)
} label: {
Label("Delete", systemImage: "trash")
}
Button() {
hostsManager.duplicateHost(host)
} label: {
Label("Duplicate", systemImage: "square.filled.on.square")
} }
} }
} }
} .onMove(perform: {
hostsManager.moveHost(from: $0, to: $1)
})
Section() {
NavigationLink {
ThemeManagerView(hostsManager: hostsManager)
} label: {
Label("Themes", systemImage: "swatchpalette")
}
}
.transition(.opacity)
.navigationTitle("ShhShell")
.toolbar {
ToolbarItem(placement: .confirmationAction) {
NavigationLink {
ConnectionView(
handler: SSHHandler(host: Host.blank),
hostsManager: hostsManager,
keyManager: keyManager
)
} label: {
Label("Add", systemImage: "plus")
}
}
}
}
} }
#Preview { #Preview {

View File

@@ -0,0 +1,58 @@
//
// HostkeysView.swift
// ShhShell
//
// Created by neon443 on 28/06/2025.
//
import SwiftUI
struct HostkeysView: View {
@ObservedObject var hostsManager: HostsManager
var body: some View {
NavigationStack {
List {
if hostsManager.hosts.isEmpty {
VStack(alignment: .leading) {
Text("Looking empty 'round here...")
.font(.title3)
.bold()
.padding(.bottom)
VStack(alignment: .leading) {
Text("Connect to some hosts to collect more hostkeys!")
.padding(.bottom)
Text("ShhShell remembers hostkey fingerprints for you, and can alert you if they change.")
.font(.subheadline)
Text("This could be due a man in the middle attack, where a bad actor tries to impersonate your server.")
.font(.subheadline)
}
}
}
ForEach(hostsManager.hosts) { host in
VStack(alignment: .leading) {
if !host.name.isEmpty {
Text("name")
.foregroundStyle(.gray)
.font(.caption)
Text(host.name)
.bold()
}
Text("address")
.foregroundStyle(.gray)
.font(.caption)
Text(host.address)
.bold()
Text(host.key ?? "nil")
}
}
}
.navigationTitle("Hostkeys")
}
}
}
#Preview {
HostkeysView(hostsManager: HostsManager())
}

View File

@@ -25,45 +25,7 @@ struct KeyManagerView: View {
} }
} }
} }
Section {
NavigationLink {
List {
if hostsManager.savedHosts.isEmpty {
VStack(alignment: .leading) {
Text("Looking empty 'round here...")
.font(.title3)
.bold()
.padding(.bottom)
VStack(alignment: .leading) {
Text("Connect to some hosts to collect more hostkeys!")
.padding(.bottom)
Text("ShhShell remembers hostkey fingerprints for you, and can alert you if they change.")
.font(.subheadline)
Text("This could be due a man in the middle attack, where a bad actor tries to impersonate your server.")
.font(.subheadline)
}
}
}
ForEach(hostsManager.savedHosts) { host in
VStack(alignment: .leading) {
if !host.name.isEmpty {
Text(host.name)
.bold()
}
Text(host.address)
.bold()
Text(host.key ?? "nil")
}
}
}
} label: {
HStack {
Image(systemName: "server.rack")
Image(systemName: "key.fill")
Text("Hostkey fingerprints")
}
}
}
Button("ed25519") { Button("ed25519") {
keyManager.generateEd25519() keyManager.generateEd25519()
} }
@@ -75,6 +37,7 @@ struct KeyManagerView: View {
} }
} }
} }
.navigationTitle("Keys")
} }
} }
} }

View File

@@ -59,6 +59,7 @@ struct ThemeManagerView: View {
} }
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
.scrollIndicators(.hidden) .scrollIndicators(.hidden)
.navigationTitle("Themes")
.alert("Enter URL", isPresented: $showAlert) { .alert("Enter URL", isPresented: $showAlert) {
TextField("", text: $importURL, prompt: Text("URL")) TextField("", text: $importURL, prompt: Text("URL"))
Button() { Button() {