mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-06-09 08:58:39 +03:00
macos: secure input manager, global option in app
This commit is contained in:
@ -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 */; };
|
||||||
@ -105,6 +106,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>"; };
|
||||||
@ -191,6 +193,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 */,
|
||||||
@ -295,6 +298,14 @@
|
|||||||
path = Services;
|
path = Services;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
A57D79252C9C8782001D522E /* Secure Input */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A57D79262C9C8798001D522E /* SecureInput.swift */,
|
||||||
|
);
|
||||||
|
path = "Secure Input";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
A59630982AEE1C4400D64628 /* Terminal */ = {
|
A59630982AEE1C4400D64628 /* Terminal */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -516,6 +527,7 @@
|
|||||||
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 */,
|
||||||
|
@ -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?
|
||||||
@ -294,6 +295,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)
|
||||||
|
|
||||||
|
// TODO: sync secure keyboard entry toggle
|
||||||
|
|
||||||
// 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 +487,10 @@ 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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"/>
|
||||||
|
135
macos/Sources/Features/Secure Input/SecureInput.swift
Normal file
135
macos/Sources/Features/Secure Input/SecureInput.swift
Normal 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 {
|
||||||
|
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.
|
||||||
|
private 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)")
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user