From c3d6356a8773f843244a3dfd7ea576fb5c15a1bf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Sep 2024 16:24:19 -0700 Subject: [PATCH] macos: show secure input overlay when it is enabled --- macos/Ghostty.xcodeproj/project.pbxproj | 14 +++- .../Features/Secure Input/SecureInput.swift | 4 +- .../Secure Input/SecureInputOverlay.swift | 67 +++++++++++++++++++ macos/Sources/Ghostty/SurfaceView.swift | 11 +++ macos/Sources/Helpers/View+Extension.swift | 18 +++++ 5 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 macos/Sources/Features/Secure Input/SecureInputOverlay.swift create mode 100644 macos/Sources/Helpers/View+Extension.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 88c90ba33..713058e19 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -58,6 +58,8 @@ A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; }; A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; }; A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; + A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */; }; + A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CC36142C9CDA03004D6760 /* View+Extension.swift */; }; A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */; }; A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CDF1922AAF9E0800513312 /* ConfigurationErrorsController.swift */; }; A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CDF1942AAFA19600513312 /* ConfigurationErrorsView.swift */; }; @@ -124,6 +126,8 @@ A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; }; A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = ""; }; + A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInputOverlay.swift; sourceTree = ""; }; + A5CC36142C9CDA03004D6760 /* View+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extension.swift"; sourceTree = ""; }; A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConfigurationErrors.xib; sourceTree = ""; }; A5CDF1922AAF9E0800513312 /* ConfigurationErrorsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationErrorsController.swift; sourceTree = ""; }; A5CDF1942AAFA19600513312 /* ConfigurationErrorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationErrorsView.swift; sourceTree = ""; }; @@ -214,6 +218,7 @@ C1F26EA62B738B9900404083 /* NSView+Extension.swift */, AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */, A5985CD62C320C4500C57AD3 /* String+Extension.swift */, + A5CC36142C9CDA03004D6760 /* View+Extension.swift */, C1F26EE72B76CBFC00404083 /* VibrantLayer.h */, C1F26EE82B76CBFC00404083 /* VibrantLayer.m */, A5CEAFDA29B8005900646FDA /* SplitView */, @@ -302,6 +307,7 @@ isa = PBXGroup; children = ( A57D79262C9C8798001D522E /* SecureInput.swift */, + A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */, ); path = "Secure Input"; sourceTree = ""; @@ -507,6 +513,7 @@ A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */, C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */, A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */, + A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */, A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */, A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */, A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */, @@ -532,6 +539,7 @@ A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */, A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */, A55685E029A03A9F004303CE /* AppError.swift in Sources */, + A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */, A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */, @@ -646,7 +654,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 12.4; MARKETING_VERSION = 0.1; "OTHER_LDFLAGS[arch=*]" = "-lstdc++"; PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.ghostty; @@ -797,7 +805,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 12.4; MARKETING_VERSION = 0.1; "OTHER_LDFLAGS[arch=*]" = "-lstdc++"; PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.ghostty; @@ -836,7 +844,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 12.4; MARKETING_VERSION = 0.1; "OTHER_LDFLAGS[arch=*]" = "-lstdc++"; PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.ghostty; diff --git a/macos/Sources/Features/Secure Input/SecureInput.swift b/macos/Sources/Features/Secure Input/SecureInput.swift index 231306f5d..f999ce5ca 100644 --- a/macos/Sources/Features/Secure Input/SecureInput.swift +++ b/macos/Sources/Features/Secure Input/SecureInput.swift @@ -12,7 +12,7 @@ import OSLog // it. You have to yield secure input on application deactivation (because // it'll affect other apps) and reacquire on reactivation, and every enable // needs to be balanced with a disable. -class SecureInput { +class SecureInput : ObservableObject { static let shared = SecureInput() private static let logger = Logger( @@ -31,7 +31,7 @@ class SecureInput { private var scoped: [ObjectIdentifier: Bool] = [:] // This is set to true when we've successfully called EnableSecureInput. - private var enabled: Bool = false + @Published private(set) var enabled: Bool = false // This is true if we want to enable secure input. We want to enable // secure input if its enabled globally or any of the scoped objects are diff --git a/macos/Sources/Features/Secure Input/SecureInputOverlay.swift b/macos/Sources/Features/Secure Input/SecureInputOverlay.swift new file mode 100644 index 000000000..50b94fdf8 --- /dev/null +++ b/macos/Sources/Features/Secure Input/SecureInputOverlay.swift @@ -0,0 +1,67 @@ +import SwiftUI + +struct SecureInputOverlay: View { + // Animations + @State private var shadowAngle: Angle = .degrees(0) + @State private var shadowWidth: CGFloat = 6 + + // Popover explainer text + @State private var isPopover = false + + var body: some View { + VStack { + HStack { + Spacer() + + Image(systemName: "lock.shield.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 25, height: 25) + .foregroundColor(.primary) + .padding(5) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(.background) + .innerShadow( + using: RoundedRectangle(cornerRadius: 12), + stroke: AngularGradient( + gradient: Gradient(colors: [.cyan, .blue, .yellow, .blue, .cyan]), + center: .center, + angle: shadowAngle + ), + width: shadowWidth + ) + ) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.gray, lineWidth: 1) + ) + .onTapGesture { + isPopover = true + } + .padding(.top, 10) + .padding(.trailing, 10) + .popover(isPresented: $isPopover, arrowEdge: .bottom) { + Text(""" + Secure Input is active. Secure Input is a macOS security feature that + prevents applications from reading keyboard events. Ghostty turns + this on manually if `Ghostty > Secure Keyboard Entry` is enabled or + automatically when at a password prompt. + """) + .padding(.all) + } + } + + Spacer() + } + .onAppear { + withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) { + shadowAngle = .degrees(360) + } + + withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: true)) { + shadowWidth = 12 + } + } + } +} diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index cd3967052..5cfd3b732 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -52,6 +52,9 @@ extension Ghostty { // True if we're hovering over the left URL view, so we can show it on the right. @State private var isHoveringURLLeft: Bool = false + // Observe SecureInput to detect when its enabled + @ObservedObject private var secureInput = SecureInput.shared + @EnvironmentObject private var ghostty: Ghostty.App var body: some View { @@ -197,6 +200,14 @@ extension Ghostty { } } + // If we have secure input enabled and we're the focused surface and window + // then we want to show the secure input overlay. + if (secureInput.enabled && + surfaceFocus && + windowFocus) { + SecureInputOverlay() + } + // If our surface is not healthy, then we render an error view over it. if (!surfaceView.healthy) { Rectangle().fill(ghostty.config.backgroundColor) diff --git a/macos/Sources/Helpers/View+Extension.swift b/macos/Sources/Helpers/View+Extension.swift new file mode 100644 index 000000000..db17b441f --- /dev/null +++ b/macos/Sources/Helpers/View+Extension.swift @@ -0,0 +1,18 @@ +import SwiftUI + +extension View { + func innerShadow( + using shape: S = Rectangle(), + stroke: ST = Color.black, + width: CGFloat = 6, + blur: CGFloat = 6 + ) -> some View { + return self + .overlay( + shape + .stroke(stroke, lineWidth: width) + .blur(radius: blur) + .mask(shape) + ) + } +}