diff --git a/Resources/Assets.xcassets/custom.pc.symbolset/Contents.json b/Resources/Assets.xcassets/custom.pc.symbolset/Contents.json new file mode 100644 index 0000000..571dcd7 --- /dev/null +++ b/Resources/Assets.xcassets/custom.pc.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "custom.pc.svg", + "idiom" : "universal" + } + ] +} diff --git a/Resources/Assets.xcassets/custom.pc.symbolset/custom.pc.svg b/Resources/Assets.xcassets/custom.pc.symbolset/custom.pc.svg new file mode 100644 index 0000000..b112618 --- /dev/null +++ b/Resources/Assets.xcassets/custom.pc.symbolset/custom.pc.svg @@ -0,0 +1,117 @@ + + + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.4.0 + Requires Xcode 14 or greater + Generated from custom.pc + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ShhShell.xcodeproj/project.pbxproj b/ShhShell.xcodeproj/project.pbxproj index 92951b6..1357dd5 100644 --- a/ShhShell.xcodeproj/project.pbxproj +++ b/ShhShell.xcodeproj/project.pbxproj @@ -39,6 +39,8 @@ A9B15A9A2E0ABA0400F66E02 /* DialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B15A992E0ABA0400F66E02 /* DialogView.swift */; }; A9C4140C2E096DB7005E3047 /* SSHError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C4140B2E096DB7005E3047 /* SSHError.swift */; }; A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */; }; + A9DA97712E0D30ED00142DDC /* Symbol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DA97702E0D30ED00142DDC /* Symbol.swift */; }; + A9DA97732E0D40C100142DDC /* SymbolPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DA97722E0D40C100142DDC /* SymbolPreview.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -110,6 +112,8 @@ A9B15A992E0ABA0400F66E02 /* DialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialogView.swift; sourceTree = ""; }; A9C4140B2E096DB7005E3047 /* SSHError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHError.swift; sourceTree = ""; }; A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHHandler.swift; sourceTree = ""; }; + A9DA97702E0D30ED00142DDC /* Symbol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Symbol.swift; sourceTree = ""; }; + A9DA97722E0D40C100142DDC /* SymbolPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolPreview.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -279,6 +283,8 @@ children = ( A93143BF2DF61B3200FCD5DB /* Host.swift */, A98554602E058433009051BD /* HostsManager.swift */, + A9DA97702E0D30ED00142DDC /* Symbol.swift */, + A9DA97722E0D40C100142DDC /* SymbolPreview.swift */, ); path = Host; sourceTree = ""; @@ -452,11 +458,13 @@ A93143C62DF61FE300FCD5DB /* ViewModifiers.swift in Sources */, A98554632E0587DF009051BD /* HostsView.swift in Sources */, A96C6A8A2E0C0B1100F377FE /* SSHState.swift in Sources */, + A9DA97732E0D40C100142DDC /* SymbolPreview.swift in Sources */, A92538C82DEE0742007E0A18 /* ContentView.swift in Sources */, A93143C02DF61B3200FCD5DB /* Host.swift in Sources */, A9B15A9A2E0ABA0400F66E02 /* DialogView.swift in Sources */, A92538C92DEE0742007E0A18 /* ShhShellApp.swift in Sources */, A96C6B002E0C45FE00F377FE /* KeyDetailView.swift in Sources */, + A9DA97712E0D30ED00142DDC /* Symbol.swift in Sources */, A98554612E058433009051BD /* HostsManager.swift in Sources */, A985545D2E055D4D009051BD /* ConnectionView.swift in Sources */, A98554592E0553AA009051BD /* KeyManager.swift in Sources */, diff --git a/ShhShell/Host/Host.swift b/ShhShell/Host/Host.swift index 66381fc..84f3392 100644 --- a/ShhShell/Host/Host.swift +++ b/ShhShell/Host/Host.swift @@ -6,10 +6,13 @@ // import Foundation +import SwiftUI protocol HostPr: Codable, Identifiable, Equatable { var id: UUID { get set } var name: String { get set } + var symbol: Symbol { get set } + var label: String { get set } var address: String { get set } var port: Int { get set } var username: String { get set } @@ -22,18 +25,22 @@ protocol HostPr: Codable, Identifiable, Equatable { struct Host: HostPr { var id = UUID() - var name: String = "" - var address: String = "" + var name: String + var symbol: Symbol + var label: String + var address: String var port: Int var username: String var password: String var publicKey: Data? var privateKey: Data? - var passphrase: String = "" + var passphrase: String var key: String? init( name: String = "", + symbol: Symbol = .genericServer, + label: String = "", address: String, port: Int = 22, username: String = "", @@ -44,6 +51,8 @@ struct Host: HostPr { hostkey: String? = nil ) { self.name = name + self.symbol = symbol + self.label = label self.address = address self.port = port self.username = username @@ -62,6 +71,7 @@ extension Host { static var debug: Host { Host( name: "name for localhost", + label: "lo0", address: "localhost", port: 22, username: "neon443", diff --git a/ShhShell/Host/HostsManager.swift b/ShhShell/Host/HostsManager.swift index 74d1e50..ecb4fe4 100644 --- a/ShhShell/Host/HostsManager.swift +++ b/ShhShell/Host/HostsManager.swift @@ -50,6 +50,23 @@ class HostsManager: ObservableObject, @unchecked Sendable { } } + func makeLabel(forHost: Host) -> String { + if forHost.name.isEmpty && forHost.address.isEmpty { + return forHost.id.uuidString + } else if forHost.name.isEmpty { + return forHost.address + } else if forHost.address.isEmpty { + return forHost.name + } else { + return forHost.name + } + } + + func moveHost(from: IndexSet, to: Int) { + savedHosts.move(fromOffsets: from, toOffset: to) + saveSavedHosts() + } + func loadSavedHosts() { userDefaults.synchronize() let decoder = JSONDecoder() diff --git a/ShhShell/Host/Symbol.swift b/ShhShell/Host/Symbol.swift new file mode 100644 index 0000000..fd12a5c --- /dev/null +++ b/ShhShell/Host/Symbol.swift @@ -0,0 +1,64 @@ +// +// Symbol.swift +// ShhShell +// +// Created by neon443 on 26/06/2025. +// + +import Foundation +import SwiftUI + +enum Symbol: Codable, Hashable, Equatable, CaseIterable { + case desktopcomputer + case laptopcomputer + + case trashcan + + case genericPC + case genericServer + case genericServerVertical + + var sf: String { + switch self { + case .desktopcomputer: + return "desktopcomputer" + case .laptopcomputer: + return "laptopcomputer" + + case .trashcan: + return "macpro.gen2" + + case .genericPC: + return "custom.pc" + case .genericServer: + return "rectangle" + case .genericServerVertical: + return "rectangle.portrait" + } + + } + + var isCustom: Bool { + switch self { + case .genericPC: + return true + default: + return false + } + } + + var offset: CGSize { + var deltaHeight: Double + switch self { + case .desktopcomputer: + deltaHeight = -6 + case .laptopcomputer: + deltaHeight = -2 + case .genericPC: + deltaHeight = -6 + default: + deltaHeight = 0 + } + return CGSize(width: 0, height: deltaHeight) + } +} diff --git a/ShhShell/Host/SymbolPreview.swift b/ShhShell/Host/SymbolPreview.swift new file mode 100644 index 0000000..f545a21 --- /dev/null +++ b/ShhShell/Host/SymbolPreview.swift @@ -0,0 +1,36 @@ +// +// SymbolPreview.swift +// ShhShell +// +// Created by neon443 on 26/06/2025. +// + +import SwiftUI + +struct SymbolPreview: View { + @State var symbol: Symbol + @State var label: String + + var body: some View { + ZStack(alignment: .center) { + if symbol.isCustom { + Image(symbol.sf) + .resizable().scaledToFit() + .symbolRenderingMode(.monochrome) + .padding(5) + } else { + Image(systemName: symbol.sf) + .resizable().scaledToFit() + .symbolRenderingMode(.monochrome) + .padding(5) + } + Text(label) + .font(.headline) + .offset(symbol.offset) + } + } +} + +#Preview { + SymbolPreview(symbol: Symbol.desktopcomputer, label: "lo0") +} diff --git a/ShhShell/Views/Hosts/ConnectionView.swift b/ShhShell/Views/Hosts/ConnectionView.swift index 9b16c19..705925b 100644 --- a/ShhShell/Views/Hosts/ConnectionView.swift +++ b/ShhShell/Views/Hosts/ConnectionView.swift @@ -24,6 +24,21 @@ struct ConnectionView: View { var body: some View { NavigationStack { List { + Section { + HStack { + Picker("", selection: $handler.host.symbol) { + ForEach(Symbol.allCases, id: \.self) { symbol in + SymbolPreview(symbol: symbol, label: handler.host.label) + .tag(symbol) + .frame(width: 60, height: 60) + } + } + .pickerStyle(SegmentedPickerStyle()) + } + .scrollIndicators(.hidden) + TextField("label", text: $handler.host.label) + .textFieldStyle(.roundedBorder) + } Section { HStack { Text(handler.connected ? "connected" : "not connected") @@ -33,6 +48,7 @@ struct ConnectionView: View { .modifier(foregroundColorStyle(checkAuth(handler.state) ? .green : .red)) Text("\(handler.state)") } + TextField("name", text: $handler.host.name) .textFieldStyle(.roundedBorder) diff --git a/ShhShell/Views/Hosts/HostsView.swift b/ShhShell/Views/Hosts/HostsView.swift index a15e556..b49719d 100644 --- a/ShhShell/Views/Hosts/HostsView.swift +++ b/ShhShell/Views/Hosts/HostsView.swift @@ -44,13 +44,8 @@ struct HostsView: View { keyManager: keyManager ) } label: { - if host.name.isEmpty { - Text(host.address) - } else if host.address.isEmpty { - Text(host.name) - } else { - Text(host.id.uuidString) - } + SymbolPreview(symbol: host.symbol, label: host.label) + .frame(width: 30, height: 30) } .animation(.default, value: host) .swipeActions(edge: .trailing) { @@ -66,6 +61,9 @@ struct HostsView: View { } } } + .onMove(perform: { + hostsManager.moveHost(from: $0, to: $1) + }) } .transition(.opacity) .toolbar {