Merge pull request #2270 from ghostty-org/secureinput

macOS Secure Input
This commit is contained in:
Mitchell Hashimoto
2024-09-19 20:15:35 -07:00
committed by GitHub
19 changed files with 427 additions and 5 deletions

View File

@ -464,6 +464,8 @@ typedef void (*ghostty_runtime_show_desktop_notification_cb)(void*,
typedef void ( typedef void (
*ghostty_runtime_update_renderer_health)(void*, ghostty_renderer_health_e); *ghostty_runtime_update_renderer_health)(void*, ghostty_renderer_health_e);
typedef void (*ghostty_runtime_mouse_over_link_cb)(void*, const char*, size_t); typedef void (*ghostty_runtime_mouse_over_link_cb)(void*, const char*, size_t);
typedef void (*ghostty_runtime_set_password_input_cb)(void*, bool);
typedef void (*ghostty_runtime_toggle_secure_input_cb)();
typedef struct { typedef struct {
void* userdata; void* userdata;
@ -494,6 +496,8 @@ typedef struct {
ghostty_runtime_show_desktop_notification_cb show_desktop_notification_cb; ghostty_runtime_show_desktop_notification_cb show_desktop_notification_cb;
ghostty_runtime_update_renderer_health update_renderer_health_cb; ghostty_runtime_update_renderer_health update_renderer_health_cb;
ghostty_runtime_mouse_over_link_cb mouse_over_link_cb; ghostty_runtime_mouse_over_link_cb mouse_over_link_cb;
ghostty_runtime_set_password_input_cb set_password_input_cb;
ghostty_runtime_toggle_secure_input_cb toggle_secure_input_cb;
} ghostty_runtime_config_s; } ghostty_runtime_config_s;
//------------------------------------------------------------------- //-------------------------------------------------------------------

View File

@ -41,6 +41,7 @@
A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */; }; A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */; };
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56D58882ACDE6CA00508D2C /* ServiceProvider.swift */; }; A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56D58882ACDE6CA00508D2C /* ServiceProvider.swift */; };
A571AB1D2A206FCF00248498 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; A571AB1D2A206FCF00248498 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; };
A57D79272C9C879B001D522E /* SecureInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57D79262C9C8798001D522E /* SecureInput.swift */; };
A586167C2B7703CC009BDB1D /* fish in Resources */ = {isa = PBXBuildFile; fileRef = A586167B2B7703CC009BDB1D /* fish */; }; A586167C2B7703CC009BDB1D /* fish in Resources */ = {isa = PBXBuildFile; fileRef = A586167B2B7703CC009BDB1D /* fish */; };
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; }; A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; };
A59630972AEE163600D64628 /* HostingWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59630962AEE163600D64628 /* HostingWindow.swift */; }; A59630972AEE163600D64628 /* HostingWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59630962AEE163600D64628 /* HostingWindow.swift */; };
@ -57,6 +58,8 @@
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; }; A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; };
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; }; A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; 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 */; }; A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */; };
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CDF1922AAF9E0800513312 /* ConfigurationErrorsController.swift */; }; A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CDF1922AAF9E0800513312 /* ConfigurationErrorsController.swift */; };
A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CDF1942AAFA19600513312 /* ConfigurationErrorsView.swift */; }; A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CDF1942AAFA19600513312 /* ConfigurationErrorsView.swift */; };
@ -105,6 +108,7 @@
A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Shell.swift; sourceTree = "<group>"; }; A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Shell.swift; sourceTree = "<group>"; };
A56D58882ACDE6CA00508D2C /* ServiceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceProvider.swift; sourceTree = "<group>"; }; A56D58882ACDE6CA00508D2C /* ServiceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceProvider.swift; sourceTree = "<group>"; };
A571AB1C2A206FC600248498 /* Ghostty-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Ghostty-Info.plist"; sourceTree = "<group>"; }; A571AB1C2A206FC600248498 /* Ghostty-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Ghostty-Info.plist"; sourceTree = "<group>"; };
A57D79262C9C8798001D522E /* SecureInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInput.swift; sourceTree = "<group>"; };
A586167B2B7703CC009BDB1D /* fish */ = {isa = PBXFileReference; lastKnownFileType = folder; name = fish; path = "../zig-out/share/fish"; sourceTree = "<group>"; }; A586167B2B7703CC009BDB1D /* fish */ = {isa = PBXFileReference; lastKnownFileType = folder; name = fish; path = "../zig-out/share/fish"; sourceTree = "<group>"; };
A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; }; A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
A59630962AEE163600D64628 /* HostingWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostingWindow.swift; sourceTree = "<group>"; }; A59630962AEE163600D64628 /* HostingWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostingWindow.swift; sourceTree = "<group>"; };
@ -122,6 +126,8 @@
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; }; A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInputOverlay.swift; sourceTree = "<group>"; };
A5CC36142C9CDA03004D6760 /* View+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extension.swift"; sourceTree = "<group>"; };
A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConfigurationErrors.xib; sourceTree = "<group>"; }; A5CDF1902AAF9A5800513312 /* ConfigurationErrors.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ConfigurationErrors.xib; sourceTree = "<group>"; };
A5CDF1922AAF9E0800513312 /* ConfigurationErrorsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationErrorsController.swift; sourceTree = "<group>"; }; A5CDF1922AAF9E0800513312 /* ConfigurationErrorsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationErrorsController.swift; sourceTree = "<group>"; };
A5CDF1942AAFA19600513312 /* ConfigurationErrorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationErrorsView.swift; sourceTree = "<group>"; }; A5CDF1942AAFA19600513312 /* ConfigurationErrorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationErrorsView.swift; sourceTree = "<group>"; };
@ -191,6 +197,7 @@
A56D58872ACDE6BE00508D2C /* Services */, A56D58872ACDE6BE00508D2C /* Services */,
A59630982AEE1C4400D64628 /* Terminal */, A59630982AEE1C4400D64628 /* Terminal */,
A5E112912AF73E4D00C6E0C2 /* ClipboardConfirmation */, A5E112912AF73E4D00C6E0C2 /* ClipboardConfirmation */,
A57D79252C9C8782001D522E /* Secure Input */,
A534263E2A7DCC5800EBB7A2 /* Settings */, A534263E2A7DCC5800EBB7A2 /* Settings */,
A51BFC1C2B2FB5AB00E92F16 /* About */, A51BFC1C2B2FB5AB00E92F16 /* About */,
A51BFC292B30F69F00E92F16 /* Update */, A51BFC292B30F69F00E92F16 /* Update */,
@ -211,6 +218,7 @@
C1F26EA62B738B9900404083 /* NSView+Extension.swift */, C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */, AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */,
A5985CD62C320C4500C57AD3 /* String+Extension.swift */, A5985CD62C320C4500C57AD3 /* String+Extension.swift */,
A5CC36142C9CDA03004D6760 /* View+Extension.swift */,
C1F26EE72B76CBFC00404083 /* VibrantLayer.h */, C1F26EE72B76CBFC00404083 /* VibrantLayer.h */,
C1F26EE82B76CBFC00404083 /* VibrantLayer.m */, C1F26EE82B76CBFC00404083 /* VibrantLayer.m */,
A5CEAFDA29B8005900646FDA /* SplitView */, A5CEAFDA29B8005900646FDA /* SplitView */,
@ -295,6 +303,15 @@
path = Services; path = Services;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
A57D79252C9C8782001D522E /* Secure Input */ = {
isa = PBXGroup;
children = (
A57D79262C9C8798001D522E /* SecureInput.swift */,
A5CC36122C9CD729004D6760 /* SecureInputOverlay.swift */,
);
path = "Secure Input";
sourceTree = "<group>";
};
A59630982AEE1C4400D64628 /* Terminal */ = { A59630982AEE1C4400D64628 /* Terminal */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -496,6 +513,7 @@
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */, A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */, C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */,
A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */, A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */,
A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */,
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */, A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */,
A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */, A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */,
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */, A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */,
@ -516,10 +534,12 @@
A5FEB3002ABB69450068369E /* main.swift in Sources */, A5FEB3002ABB69450068369E /* main.swift in Sources */,
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */, A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */,
A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */, A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */,
A57D79272C9C879B001D522E /* SecureInput.swift in Sources */,
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */, A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */,
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */, A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */,
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */, A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */,
A55685E029A03A9F004303CE /* AppError.swift in Sources */, A55685E029A03A9F004303CE /* AppError.swift in Sources */,
A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */,
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */,
A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */, A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */,
A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */, A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */,

View File

@ -22,6 +22,7 @@ class AppDelegate: NSObject,
@IBOutlet private var menuCheckForUpdates: NSMenuItem? @IBOutlet private var menuCheckForUpdates: NSMenuItem?
@IBOutlet private var menuOpenConfig: NSMenuItem? @IBOutlet private var menuOpenConfig: NSMenuItem?
@IBOutlet private var menuReloadConfig: NSMenuItem? @IBOutlet private var menuReloadConfig: NSMenuItem?
@IBOutlet private var menuSecureInput: NSMenuItem?
@IBOutlet private var menuQuit: NSMenuItem? @IBOutlet private var menuQuit: NSMenuItem?
@IBOutlet private var menuNewWindow: NSMenuItem? @IBOutlet private var menuNewWindow: NSMenuItem?
@ -105,6 +106,11 @@ class AppDelegate: NSObject,
"ApplePressAndHoldEnabled": false, "ApplePressAndHoldEnabled": false,
]) ])
// Check if secure input was enabled when we last quit.
if (UserDefaults.standard.bool(forKey: "SecureInput") != SecureInput.shared.enabled) {
toggleSecureInput(self)
}
// Hook up updater menu // Hook up updater menu
menuCheckForUpdates?.target = updaterController menuCheckForUpdates?.target = updaterController
menuCheckForUpdates?.action = #selector(SPUStandardUpdaterController.checkForUpdates(_:)) menuCheckForUpdates?.action = #selector(SPUStandardUpdaterController.checkForUpdates(_:))
@ -294,6 +300,8 @@ class AppDelegate: NSObject,
syncMenuShortcut(action: "reset_font_size", menuItem: self.menuResetFontSize) syncMenuShortcut(action: "reset_font_size", menuItem: self.menuResetFontSize)
syncMenuShortcut(action: "inspector:toggle", menuItem: self.menuTerminalInspector) syncMenuShortcut(action: "inspector:toggle", menuItem: self.menuTerminalInspector)
syncMenuShortcut(action: "toggle_secure_input", menuItem: self.menuSecureInput)
// This menu item is NOT synced with the configuration because it disables macOS // This menu item is NOT synced with the configuration because it disables macOS
// global fullscreen keyboard shortcut. The shortcut in the Ghostty config will continue // global fullscreen keyboard shortcut. The shortcut in the Ghostty config will continue
// to work but it won't be reflected in the menu item. // to work but it won't be reflected in the menu item.
@ -484,4 +492,11 @@ class AppDelegate: NSObject,
guard let url = URL(string: "https://github.com/ghostty-org/ghostty") else { return } guard let url = URL(string: "https://github.com/ghostty-org/ghostty") else { return }
NSWorkspace.shared.open(url) NSWorkspace.shared.open(url)
} }
@IBAction func toggleSecureInput(_ sender: Any) {
let input = SecureInput.shared
input.global.toggle()
self.menuSecureInput?.state = if (input.global) { .on } else { .off }
UserDefaults.standard.set(input.global, forKey: "SecureInput")
}
} }

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23094" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies> <dependencies>
<deployment identifier="macosx"/> <deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23094"/>
</dependencies> </dependencies>
<objects> <objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication"> <customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
@ -35,14 +35,15 @@
<outlet property="menuQuit" destination="4sb-4s-VLi" id="qYN-S1-6UW"/> <outlet property="menuQuit" destination="4sb-4s-VLi" id="qYN-S1-6UW"/>
<outlet property="menuReloadConfig" destination="KKH-XX-5py" id="Wvp-7J-wqX"/> <outlet property="menuReloadConfig" destination="KKH-XX-5py" id="Wvp-7J-wqX"/>
<outlet property="menuResetFontSize" destination="Jah-MY-aLX" id="ger-qM-wrm"/> <outlet property="menuResetFontSize" destination="Jah-MY-aLX" id="ger-qM-wrm"/>
<outlet property="menuSecureInput" destination="oC6-w4-qI7" id="PCc-pe-Mda"/>
<outlet property="menuSelectAll" destination="q2h-lq-e4r" id="s98-r1-Jcv"/> <outlet property="menuSelectAll" destination="q2h-lq-e4r" id="s98-r1-Jcv"/>
<outlet property="menuSelectSplitAbove" destination="0yU-hC-8xF" id="aPc-lS-own"/> <outlet property="menuSelectSplitAbove" destination="0yU-hC-8xF" id="aPc-lS-own"/>
<outlet property="menuSelectSplitBelow" destination="QDz-d9-CBr" id="FsH-Dq-jij"/> <outlet property="menuSelectSplitBelow" destination="QDz-d9-CBr" id="FsH-Dq-jij"/>
<outlet property="menuSelectSplitLeft" destination="cTK-oy-KuV" id="Jpr-5q-dqz"/> <outlet property="menuSelectSplitLeft" destination="cTK-oy-KuV" id="Jpr-5q-dqz"/>
<outlet property="menuSelectSplitRight" destination="upj-mc-L7X" id="nLY-o1-lky"/> <outlet property="menuSelectSplitRight" destination="upj-mc-L7X" id="nLY-o1-lky"/>
<outlet property="menuServices" destination="aQe-vS-j8Q" id="uWQ-Wo-T1L"/> <outlet property="menuServices" destination="aQe-vS-j8Q" id="uWQ-Wo-T1L"/>
<outlet property="menuSplitRight" destination="VUR-Ld-nLx" id="RxO-Zw-ovb"/>
<outlet property="menuSplitDown" destination="UDZ-4y-6xL" id="fgZ-Wb-8OR"/> <outlet property="menuSplitDown" destination="UDZ-4y-6xL" id="fgZ-Wb-8OR"/>
<outlet property="menuSplitRight" destination="VUR-Ld-nLx" id="RxO-Zw-ovb"/>
<outlet property="menuTerminalInspector" destination="QwP-M5-fvh" id="wJi-Dh-S9f"/> <outlet property="menuTerminalInspector" destination="QwP-M5-fvh" id="wJi-Dh-S9f"/>
<outlet property="menuToggleFullScreen" destination="8kY-Pi-KaY" id="yQg-6V-OO6"/> <outlet property="menuToggleFullScreen" destination="8kY-Pi-KaY" id="yQg-6V-OO6"/>
<outlet property="menuZoomSplit" destination="oPd-mn-IEH" id="wTu-jK-egI"/> <outlet property="menuZoomSplit" destination="oPd-mn-IEH" id="wTu-jK-egI"/>
@ -76,6 +77,12 @@
<action selector="reloadConfig:" target="bbz-4X-AYv" id="h5x-tu-Izk"/> <action selector="reloadConfig:" target="bbz-4X-AYv" id="h5x-tu-Izk"/>
</connections> </connections>
</menuItem> </menuItem>
<menuItem title="Secure Keyboard Entry" id="oC6-w4-qI7">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleSecureInput:" target="bbz-4X-AYv" id="vWx-z8-5Sy"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/> <menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Services" id="rJe-5J-bwL"> <menuItem title="Services" id="rJe-5J-bwL">
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>

View File

@ -0,0 +1,135 @@
import Carbon
import Cocoa
import OSLog
// Manages the secure keyboard input state. Secure keyboard input is an old Carbon
// API still in use by applications such as Webkit. From the old Carbon docs:
// "When secure event input mode is enabled, keyboard input goes only to the
// application with keyboard focus and is not echoed to other applications that
// might be using the event monitor target to watch keyboard input."
//
// Secure input is global and stateful so you need a singleton class to manage
// 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 : ObservableObject {
static let shared = SecureInput()
private static let logger = Logger(
subsystem: Bundle.main.bundleIdentifier!,
category: String(describing: SecureInput.self)
)
// True if you want to enable secure input globally.
var global: Bool = false {
didSet {
apply()
}
}
// The scoped objects and whether they're currently in focus.
private var scoped: [ObjectIdentifier: Bool] = [:]
// This is set to true when we've successfully called EnableSecureInput.
@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
// in focus.
private var desired: Bool {
global || scoped.contains(where: { $0.value })
}
private init() {
// Add notifications for application active/resign so we can disable
// secure input. This is only useful for global enabling of secure
// input.
let center = NotificationCenter.default
center.addObserver(
self,
selector: #selector(onDidResignActive(notification:)),
name: NSApplication.didResignActiveNotification,
object: nil)
center.addObserver(
self,
selector: #selector(onDidBecomeActive(notification:)),
name: NSApplication.didBecomeActiveNotification,
object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
// Reset our state so that we can ensure we set the proper secure input
// system state
scoped.removeAll()
global = false
apply()
}
// Add a scoped object that has secure input enabled. The focused value will
// determine if the object currently has focus. This is used so that secure
// input is only enabled while the object is focused.
func setScoped(_ object: ObjectIdentifier, focused: Bool) {
scoped[object] = focused
apply()
}
// Remove a scoped object completely.
func removeScoped(_ object: ObjectIdentifier) {
scoped[object] = nil
apply()
}
private func apply() {
// If we aren't active then we don't do anything. The become/resign
// active notifications will handle applying for us.
guard NSApp.isActive else { return }
// We only need to apply if we're not in our desired state
guard enabled != desired else { return }
let err: OSStatus
if (enabled) {
err = DisableSecureEventInput()
} else {
err = EnableSecureEventInput()
}
if (err == noErr) {
enabled = desired
Self.logger.debug("secure input state=\(self.enabled)")
return
}
Self.logger.warning("secure input apply failed err=\(err)")
}
// MARK: Notifications
@objc private func onDidBecomeActive(notification: NSNotification) {
// We only want to re-enable if we're not already enabled and we
// desire to be enabled.
guard !enabled && desired else { return }
let err = EnableSecureEventInput()
if (err == noErr) {
enabled = true
Self.logger.debug("secure input enabled on activation")
return
}
Self.logger.warning("secure input apply failed err=\(err)")
}
@objc private func onDidResignActive(notification: NSNotification) {
// We only want to disable if we're enabled.
guard enabled else { return }
let err = DisableSecureEventInput()
if (err == noErr) {
enabled = false
Self.logger.debug("secure input disabled on deactivation")
return
}
Self.logger.warning("secure input apply failed err=\(err)")
}
}

View File

@ -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. This is enabled
automatically whenever Ghostty detects a password prompt in the terminal,
or at all times if `Ghostty > Secure Keyboard Entry` is active.
""")
.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
}
}
}
}

View File

@ -94,7 +94,9 @@ extension Ghostty {
show_desktop_notification_cb: { userdata, title, body in show_desktop_notification_cb: { userdata, title, body in
App.showUserNotification(userdata, title: title, body: body) }, App.showUserNotification(userdata, title: title, body: body) },
update_renderer_health_cb: { userdata, health in App.updateRendererHealth(userdata, health: health) }, update_renderer_health_cb: { userdata, health in App.updateRendererHealth(userdata, health: health) },
mouse_over_link_cb: { userdata, ptr, len in App.mouseOverLink(userdata, uri: ptr, len: len) } mouse_over_link_cb: { userdata, ptr, len in App.mouseOverLink(userdata, uri: ptr, len: len) },
set_password_input_cb: { userdata, value in App.setPasswordInput(userdata, value: value) },
toggle_secure_input_cb: { App.toggleSecureInput() }
) )
// Create the ghostty app. // Create the ghostty app.
@ -299,6 +301,8 @@ extension Ghostty {
static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer<CChar>?, body: UnsafePointer<CChar>?) {} static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer<CChar>?, body: UnsafePointer<CChar>?) {}
static func updateRendererHealth(_ userdata: UnsafeMutableRawPointer?, health: ghostty_renderer_health_e) {} static func updateRendererHealth(_ userdata: UnsafeMutableRawPointer?, health: ghostty_renderer_health_e) {}
static func mouseOverLink(_ userdata: UnsafeMutableRawPointer?, uri: UnsafePointer<CChar>?, len: Int) {} static func mouseOverLink(_ userdata: UnsafeMutableRawPointer?, uri: UnsafePointer<CChar>?, len: Int) {}
static func setPasswordInput(_ userdata: UnsafeMutableRawPointer?, value: Bool) {}
static func toggleSecureInput() {}
#endif #endif
#if os(macOS) #if os(macOS)
@ -544,6 +548,18 @@ extension Ghostty {
surfaceView.hoverUrl = String(data: buffer, encoding: .utf8) surfaceView.hoverUrl = String(data: buffer, encoding: .utf8)
} }
static func setPasswordInput(_ userdata: UnsafeMutableRawPointer?, value: Bool) {
let surfaceView = self.surfaceUserdata(from: userdata)
guard let appState = self.appState(fromView: surfaceView) else { return }
guard appState.config.autoSecureInput else { return }
surfaceView.passwordInput = value
}
static func toggleSecureInput() {
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return }
appDelegate.toggleSecureInput(self)
}
static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer<CChar>?, body: UnsafePointer<CChar>?) { static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer<CChar>?, body: UnsafePointer<CChar>?) {
let surfaceView = self.surfaceUserdata(from: userdata) let surfaceView = self.surfaceUserdata(from: userdata)
guard let title = String(cString: title!, encoding: .utf8) else { return } guard let title = String(cString: title!, encoding: .utf8) else { return }

View File

@ -371,6 +371,22 @@ extension Ghostty {
let str = String(cString: ptr) let str = String(cString: ptr)
return AutoUpdate(rawValue: str) ?? defaultValue return AutoUpdate(rawValue: str) ?? defaultValue
} }
var autoSecureInput: Bool {
guard let config = self.config else { return true }
var v = false;
let key = "macos-auto-secure-input"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v
}
var secureInputIndication: Bool {
guard let config = self.config else { return true }
var v = false;
let key = "macos-secure-input-indication"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v
}
} }
} }

View File

@ -52,6 +52,11 @@ extension Ghostty {
// True if we're hovering over the left URL view, so we can show it on the right. // True if we're hovering over the left URL view, so we can show it on the right.
@State private var isHoveringURLLeft: Bool = false @State private var isHoveringURLLeft: Bool = false
#if canImport(AppKit)
// Observe SecureInput to detect when its enabled
@ObservedObject private var secureInput = SecureInput.shared
#endif
@EnvironmentObject private var ghostty: Ghostty.App @EnvironmentObject private var ghostty: Ghostty.App
var body: some View { var body: some View {
@ -197,6 +202,17 @@ extension Ghostty {
} }
} }
#if canImport(AppKit)
// If we have secure input enabled and we're the focused surface and window
// then we want to show the secure input overlay.
if (ghostty.config.secureInputIndication &&
secureInput.enabled &&
surfaceFocus &&
windowFocus) {
SecureInputOverlay()
}
#endif
// If our surface is not healthy, then we render an error view over it. // If our surface is not healthy, then we render an error view over it.
if (!surfaceView.healthy) { if (!surfaceView.healthy) {
Rectangle().fill(ghostty.config.backgroundColor) Rectangle().fill(ghostty.config.backgroundColor)

View File

@ -42,6 +42,21 @@ extension Ghostty {
// then the view is moved to a new window. // then the view is moved to a new window.
var initialSize: NSSize? = nil var initialSize: NSSize? = nil
// Set whether the surface is currently on a password input or not. This is
// detected with the set_password_input_cb on the Ghostty state.
var passwordInput: Bool = false {
didSet {
// We need to update our state within the SecureInput manager.
let input = SecureInput.shared
let id = ObjectIdentifier(self)
if (passwordInput) {
input.setScoped(id, focused: focused)
} else {
input.removeScoped(id)
}
}
}
// Returns true if quit confirmation is required for this surface to // Returns true if quit confirmation is required for this surface to
// exit safely. // exit safely.
var needsConfirmQuit: Bool { var needsConfirmQuit: Bool {
@ -59,6 +74,7 @@ extension Ghostty {
if (v.count == 0) { return nil } if (v.count == 0) { return nil }
return v return v
} }
// Returns the inspector instance for this surface, or nil if the // Returns the inspector instance for this surface, or nil if the
// surface has been closed. // surface has been closed.
var inspector: ghostty_inspector_t? { var inspector: ghostty_inspector_t? {
@ -185,6 +201,9 @@ extension Ghostty {
mouseExited(with: NSEvent()) mouseExited(with: NSEvent())
} }
// Remove ourselves from secure input if we have to
SecureInput.shared.removeScoped(ObjectIdentifier(self))
guard let surface = self.surface else { return } guard let surface = self.surface else { return }
ghostty_surface_free(surface) ghostty_surface_free(surface)
} }
@ -209,6 +228,11 @@ extension Ghostty {
self.focused = focused self.focused = focused
ghostty_surface_set_focus(surface, focused) ghostty_surface_set_focus(surface, focused)
// Update our secure input state if we are a password input
if (passwordInput) {
SecureInput.shared.setScoped(ObjectIdentifier(self), focused: focused)
}
// On macOS 13+ we can store our continuous clock... // On macOS 13+ we can store our continuous clock...
if #available(macOS 13, iOS 16, *) { if #available(macOS 13, iOS 16, *) {
if (focused) { if (focused) {

View File

@ -0,0 +1,18 @@
import SwiftUI
extension View {
func innerShadow<S: Shape, ST: ShapeStyle>(
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)
)
}
}

5
pkg/macos/carbon.zig Normal file
View File

@ -0,0 +1,5 @@
pub const c = @import("carbon/c.zig").c;
test {
@import("std").testing.refAllDecls(@This());
}

3
pkg/macos/carbon/c.zig Normal file
View File

@ -0,0 +1,3 @@
pub const c = @cImport({
@cInclude("Carbon/Carbon.h");
});

View File

@ -1,3 +1,4 @@
pub const carbon = @import("carbon.zig");
pub const foundation = @import("foundation.zig"); pub const foundation = @import("foundation.zig");
pub const animation = @import("animation.zig"); pub const animation = @import("animation.zig");
pub const dispatch = @import("dispatch.zig"); pub const dispatch = @import("dispatch.zig");

View File

@ -837,6 +837,11 @@ fn passwordInput(self: *Surface, v: bool) !void {
self.io.terminal.flags.password_input = v; self.io.terminal.flags.password_input = v;
} }
// Notify our apprt so it can do whatever it wants.
if (@hasDecl(apprt.Surface, "setPasswordInput")) {
self.rt_surface.setPasswordInput(v);
}
try self.queueRender(); try self.queueRender();
} }
@ -3717,6 +3722,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
} else log.warn("runtime doesn't implement toggleWindowDecorations", .{}); } else log.warn("runtime doesn't implement toggleWindowDecorations", .{});
}, },
.toggle_secure_input => {
if (@hasDecl(apprt.Surface, "toggleSecureInput")) {
self.rt_surface.toggleSecureInput();
} else log.warn("runtime doesn't implement toggleSecureInput", .{});
},
.select_all => { .select_all => {
const sel = self.io.terminal.screen.selectAll(); const sel = self.io.terminal.screen.selectAll();
if (sel) |s| { if (sel) |s| {

View File

@ -133,6 +133,14 @@ pub const App = struct {
/// parameter. The link target will be null if the mouse is no longer /// parameter. The link target will be null if the mouse is no longer
/// over a link. /// over a link.
mouse_over_link: ?*const fn (SurfaceUD, ?[*]const u8, usize) void = null, mouse_over_link: ?*const fn (SurfaceUD, ?[*]const u8, usize) void = null,
/// Notifies that a password input has been started for the given
/// surface. The apprt can use this to modify UI, enable features
/// such as macOS secure input, etc.
set_password_input: ?*const fn (SurfaceUD, bool) callconv(.C) void = null,
/// Toggle secure input for the application.
toggle_secure_input: ?*const fn () callconv(.C) void = null,
}; };
core_app: *CoreApp, core_app: *CoreApp,
@ -1005,6 +1013,24 @@ pub const Surface = struct {
func(self.userdata, nonNativeFullscreen); func(self.userdata, nonNativeFullscreen);
} }
pub fn toggleSecureInput(self: *Surface) void {
const func = self.app.opts.toggle_secure_input orelse {
log.info("runtime embedder does not toggle_secure_input", .{});
return;
};
func();
}
pub fn setPasswordInput(self: *Surface, v: bool) void {
const func = self.app.opts.set_password_input orelse {
log.info("runtime embedder does not set_password_input", .{});
return;
};
func(self.userdata, v);
}
pub fn newTab(self: *const Surface) !void { pub fn newTab(self: *const Surface) !void {
const func = self.app.opts.new_tab orelse { const func = self.app.opts.new_tab orelse {
log.info("runtime embedder does not support new_tab", .{}); log.info("runtime embedder does not support new_tab", .{});

View File

@ -12,7 +12,7 @@ pub const fish_completions = comptimeGenerateFishCompletions();
fn comptimeGenerateFishCompletions() []const u8 { fn comptimeGenerateFishCompletions() []const u8 {
comptime { comptime {
@setEvalBranchQuota(16000); @setEvalBranchQuota(17000);
var counter = std.io.countingWriter(std.io.null_writer); var counter = std.io.countingWriter(std.io.null_writer);
try writeFishCompletions(&counter.writer()); try writeFishCompletions(&counter.writer());

View File

@ -1348,6 +1348,34 @@ keybind: Keybinds = .{},
/// find false more visually appealing. /// find false more visually appealing.
@"macos-window-shadow": bool = true, @"macos-window-shadow": bool = true,
/// If true, Ghostty on macOS will automatically enable the "Secure Input"
/// feature when it detects that a password prompt is being displayed.
///
/// "Secure Input" is a macOS security feature that prevents applications from
/// reading keyboard events. This can always be enabled manually using the
/// `Ghostty > Secure Keyboard Entry` menu item.
///
/// Note that automatic password prompt detection is based on heuristics
/// and may not always work as expected. Specifically, it does not work
/// over SSH connections, but there may be other cases where it also
/// doesn't work.
///
/// A reason to disable this feature is if you find that it is interfering
/// with legitimate accessibility software (or software that uses the
/// accessibility APIs), since secure input prevents any application from
/// reading keyboard events.
@"macos-auto-secure-input": bool = true,
/// If true, Ghostty will show a graphical indication when secure input is
/// enabled. This indication is generally recommended to know when secure input
/// is enabled.
///
/// Normally, secure input is only active when a password prompt is displayed
/// or it is manually (and typically temporarily) enabled. However, if you
/// always have secure input enabled, the indication can be distracting and
/// you may want to disable it.
@"macos-secure-input-indication": bool = true,
/// Put every surface (tab, split, window) into a dedicated Linux cgroup. /// Put every surface (tab, split, window) into a dedicated Linux cgroup.
/// ///
/// This makes it so that resource management can be done on a per-surface /// This makes it so that resource management can be done on a per-surface

View File

@ -297,6 +297,16 @@ pub const Action = union(enum) {
/// Toggle window decorations on and off. This only works on Linux. /// Toggle window decorations on and off. This only works on Linux.
toggle_window_decorations: void, toggle_window_decorations: void,
/// Toggle secure input mode on or off. This is used to prevent apps
/// that monitor input from seeing what you type. This is useful for
/// entering passwords or other sensitive information.
///
/// This applies to the entire application, not just the focused
/// terminal. You must toggle it off to disable it, or quit Ghostty.
///
/// This only works on macOS, since this is a system API on macOS.
toggle_secure_input: void,
/// Quit ghostty. /// Quit ghostty.
quit: void, quit: void,