diff --git a/ShhShell.xcodeproj/project.pbxproj b/ShhShell.xcodeproj/project.pbxproj index 8aad188..47adc32 100644 --- a/ShhShell.xcodeproj/project.pbxproj +++ b/ShhShell.xcodeproj/project.pbxproj @@ -76,6 +76,7 @@ A9BA1D192E1D9AE1005BDCEF /* SwiftTerm.Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BA1D182E1D9AE1005BDCEF /* SwiftTerm.Color.swift */; }; A9BA1D1E2E1EAD51005BDCEF /* SF-Mono-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = A9BA1D1C2E1EAD51005BDCEF /* SF-Mono-Bold.otf */; }; A9BA1D1F2E1EAD51005BDCEF /* SF-Mono-BoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = A9BA1D1D2E1EAD51005BDCEF /* SF-Mono-BoldItalic.otf */; }; + A9C060EB2E357FD300CA9374 /* Haptics.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C060EA2E357FD300CA9374 /* Haptics.swift */; }; A9C4140C2E096DB7005E3047 /* SSHError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C4140B2E096DB7005E3047 /* SSHError.swift */; }; A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C897EE2DF1A9A400EF9A5F /* SSHHandler.swift */; }; A9D819292E0E904200442D38 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D819282E0E904200442D38 /* Theme.swift */; }; @@ -202,6 +203,7 @@ A9BA1D182E1D9AE1005BDCEF /* SwiftTerm.Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftTerm.Color.swift; sourceTree = ""; }; A9BA1D1C2E1EAD51005BDCEF /* SF-Mono-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Mono-Bold.otf"; sourceTree = ""; }; A9BA1D1D2E1EAD51005BDCEF /* SF-Mono-BoldItalic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Mono-BoldItalic.otf"; sourceTree = ""; }; + A9C060EA2E357FD300CA9374 /* Haptics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Haptics.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 = ""; }; A9D819282E0E904200442D38 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; @@ -357,6 +359,7 @@ A9D8192A2E0E904900442D38 /* Themes */, A90936862E1AC4C600856059 /* Fonts */, A93F283F2E2A5EC80092B8D5 /* Snippets */, + A9C060E92E357FC400CA9374 /* Misc */, A92538D32DEE0749007E0A18 /* Views */, A90936852E1AC33C00856059 /* Info.plist */, A93143C22DF61F5700FCD5DB /* ShhShell.entitlements */, @@ -535,6 +538,14 @@ path = Keys; sourceTree = ""; }; + A9C060E92E357FC400CA9374 /* Misc */ = { + isa = PBXGroup; + children = ( + A9C060EA2E357FD300CA9374 /* Haptics.swift */, + ); + path = Misc; + sourceTree = ""; + }; A9C8976F2DF1980900EF9A5F /* Frameworks */ = { isa = PBXGroup; children = ( @@ -783,6 +794,7 @@ A9485C762E1AF59F00209824 /* FontManagerView.swift in Sources */, A98554592E0553AA009051BD /* KeyManager.swift in Sources */, A93F283D2E2A5DCB0092B8D5 /* SnippetManagerView.swift in Sources */, + A9C060EB2E357FD300CA9374 /* Haptics.swift in Sources */, A9C897EF2DF1A9A400EF9A5F /* SSHHandler.swift in Sources */, A9D819312E102D8700442D38 /* HostkeysView.swift in Sources */, A98554552E05535F009051BD /* KeyManagerView.swift in Sources */, diff --git a/ShhShell/Host/HostsManager.swift b/ShhShell/Host/HostsManager.swift index 59c21b3..406b706 100644 --- a/ShhShell/Host/HostsManager.swift +++ b/ShhShell/Host/HostsManager.swift @@ -188,12 +188,14 @@ class HostsManager: ObservableObject, @unchecked Sendable { var newTheme = themes[index] newTheme.name = newName newTheme.id = UUID().uuidString + Haptic.medium.trigger() withAnimation { themes[index] = newTheme } saveThemes() } func deleteTheme(_ themeToDel: Theme) { guard let index = themes.firstIndex(where: {$0 == themeToDel}) else { return } + Haptic.medium.trigger() themes.remove(at: index) saveThemes() } @@ -204,6 +206,7 @@ class HostsManager: ObservableObject, @unchecked Sendable { guard var theme = Theme.decodeTheme(data: data) else { return } theme.name = fromUrl?.lastPathComponent.replacingOccurrences(of: ".itermcolors", with: "") ?? "" self.themes.append(theme) + Haptic.success.trigger() saveThemes() } @@ -234,6 +237,7 @@ class HostsManager: ObservableObject, @unchecked Sendable { } else { withAnimation { hosts.append(updatedHost) } } + Haptic.medium.trigger() } func duplicateHost(_ hostToDup: Host) { @@ -241,6 +245,7 @@ class HostsManager: ObservableObject, @unchecked Sendable { hostNewID.id = UUID() if let index = hosts.firstIndex(where: { $0 == hostToDup }) { hosts.insert(hostNewID, at: index+1) + Haptic.medium.trigger() } } @@ -270,6 +275,7 @@ class HostsManager: ObservableObject, @unchecked Sendable { func removeHost(_ host: Host) { if let index = hosts.firstIndex(where: { $0.id == host.id }) { let _ = withAnimation { hosts.remove(at: index) } + Haptic.medium.trigger() saveHosts() } } diff --git a/ShhShell/Misc/Haptics.swift b/ShhShell/Misc/Haptics.swift new file mode 100644 index 0000000..5650028 --- /dev/null +++ b/ShhShell/Misc/Haptics.swift @@ -0,0 +1,68 @@ +// +// Haptics.swift +// ShhShell +// +// Created by neon443 on 26/07/2025. +// + +import Foundation +#if canImport(UIKit) +import UIKit + +enum Haptic { + case success + case error + case light + case medium + case heavy + case soft + case rigid + + var isUIImpact: Bool { + switch self { + case .light, .medium, .heavy, .soft, .rigid: + return true + case .success, .error: + return false + } + } + + @MainActor + var uiImpact: UIImpactFeedbackGenerator? { + guard self.isUIImpact else { return nil } + switch self { + case .light, .medium, .heavy, .soft, .rigid: + switch self { + case .light: + return UIImpactFeedbackGenerator(style: .light) + case .medium: + return UIImpactFeedbackGenerator(style: .medium) + case .heavy: + return UIImpactFeedbackGenerator(style: .heavy) + case .soft: + return UIImpactFeedbackGenerator(style: .soft) + case .rigid: + return UIImpactFeedbackGenerator(style: .rigid) + default: return nil + } + default: return nil + } + } + + func trigger() { + Task { @MainActor in + if self.isUIImpact { + self.uiImpact?.impactOccurred() + } else { + switch self { + case .success: + UINotificationFeedbackGenerator().notificationOccurred(.success) + case .error: + UINotificationFeedbackGenerator().notificationOccurred(.error) + default: print("idk atp") + } + } + } + } +} +#endif diff --git a/ShhShell/Views/Hosts/ConnectionView.swift b/ShhShell/Views/Hosts/ConnectionView.swift index 0612042..6251798 100644 --- a/ShhShell/Views/Hosts/ConnectionView.swift +++ b/ShhShell/Views/Hosts/ConnectionView.swift @@ -80,13 +80,13 @@ struct ConnectionView: View { TextBox(label: "Password", text: $handler.host.password, prompt: "not required if using publickeys", secure: true) Picker("Private key", selection: $handler.host.privateKeyID) { - Text("None") - .tag(nil as UUID?) - Divider() ForEach(keyManager.keypairs) { keypair in Text(keypair.label) .tag(keypair.id as UUID?) } + Divider() + Text("None") + .tag(nil as UUID?) } } diff --git a/ShhShell/Views/Snippets/AddSnippetView.swift b/ShhShell/Views/Snippets/AddSnippetView.swift index b1a2235..ba56c4d 100644 --- a/ShhShell/Views/Snippets/AddSnippetView.swift +++ b/ShhShell/Views/Snippets/AddSnippetView.swift @@ -30,6 +30,7 @@ struct AddSnippetView: View { .foregroundStyle(.gray) .listRowSeparator(.hidden) .multilineTextAlignment(.leading) + .frame(alignment: .leading) TextEditor(text: $content) .autocorrectionDisabled() .textInputAutocapitalization(.never) diff --git a/ShhShell/Views/Snippets/SnippetManagerView.swift b/ShhShell/Views/Snippets/SnippetManagerView.swift index 3aad89a..69b41ad 100644 --- a/ShhShell/Views/Snippets/SnippetManagerView.swift +++ b/ShhShell/Views/Snippets/SnippetManagerView.swift @@ -17,12 +17,31 @@ struct SnippetManagerView: View { .ignoresSafeArea(.all) NavigationStack { List { + if hostsManager.snippets.isEmpty { + VStack(alignment: .leading) { + Image(systemName: "questionmark.square.dashed") + .resizable().scaledToFit() + .frame(width: 50) + .foregroundStyle(hostsManager.tint) + .shadow(color: hostsManager.tint, radius: 2) + Text("No Snippets") + .font(.title) + .monospaced() + .padding(.bottom) + Text("Snippets are strings of commands that can be run at once in a terminal.") + .padding(.bottom) + .foregroundStyle(.gray) + .foregroundStyle(.foreground.opacity(0.7)) + } + } ForEach(hostsManager.snippets) { snip in - Group { + VStack(alignment: .leading) { Text(snip.name) .bold() + .foregroundStyle(.gray) .font(.subheadline) Text(snip.content) + .lineLimit(3) } .swipeActions(edge: .trailing) { Button(role: .destructive) { @@ -37,6 +56,12 @@ struct SnippetManagerView: View { Label("Duplicate", systemImage: "square.filled.on.square") } .tint(.blue) + Button { + UIPasteboard().string = snip.content + } label: { + Label("Copy", systemImage: "doc.on.clipboard") + } + .tint(.blue) } } }