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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,89 +12,76 @@ struct HostsView: View {
@ObservedObject var hostsManager: HostsManager
@ObservedObject var keyManager: KeyManager
var body: some View {
NavigationStack {
List {
if hostsManager.savedHosts.isEmpty {
Text("Add your first Host!")
Button() {
withAnimation { hostsManager.savedHosts.append(Host.blank) }
} label: {
Text("Create")
}
.buttonStyle(.borderedProminent)
}
var body: some View {
if hostsManager.hosts.isEmpty {
Text("Add your first Host!")
}
//proves that u can connect to multiple at the same time
NavigationLink() {
ForEach(hostsManager.savedHosts) { host in
let miniHandler = SSHHandler(host: host)
TerminalController(handler: miniHandler)
.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")
}
}
//proves that u can connect to multiple at the same time
NavigationLink() {
ForEach(hostsManager.hosts) { host in
let miniHandler = SSHHandler(host: host)
TerminalController(handler: miniHandler)
.onAppear { miniHandler.go() }
}
.transition(.opacity)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
let host = Host.blank
NavigationLink {
ConnectionView(
handler: SSHHandler(host: host),
hostsManager: hostsManager,
keyManager: keyManager
)
.onAppear {
withAnimation { hostsManager.savedHosts.append(host) }
}
} label: {
Label("Add", systemImage: "plus")
}
} label: {
Label("multiview", systemImage: "square.split.2x2")
}
ForEach(hostsManager.hosts) { 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)
.navigationTitle("ShhShell")
.toolbar {
ToolbarItem(placement: .confirmationAction) {
NavigationLink {
ConnectionView(
handler: SSHHandler(host: Host.blank),
hostsManager: hostsManager,
keyManager: keyManager
)
} label: {
Label("Add", systemImage: "plus")
}
}
}
}
}
#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") {
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)
.scrollIndicators(.hidden)
.navigationTitle("Themes")
.alert("Enter URL", isPresented: $showAlert) {
TextField("", text: $importURL, prompt: Text("URL"))
Button() {