diff --git a/include/ghostty.h b/include/ghostty.h index b4f6a89a2..c82411820 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -464,6 +464,7 @@ typedef void (*ghostty_runtime_show_desktop_notification_cb)(void*, typedef void ( *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_set_password_input_cb)(void*, bool); typedef void (*ghostty_runtime_toggle_secure_input_cb)(); typedef struct { @@ -495,6 +496,7 @@ typedef struct { ghostty_runtime_show_desktop_notification_cb show_desktop_notification_cb; ghostty_runtime_update_renderer_health update_renderer_health_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; diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 190bb3224..366f83711 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -95,6 +95,7 @@ extension Ghostty { App.showUserNotification(userdata, title: title, body: body) }, 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) }, + set_password_input_cb: { userdata, value in App.setPasswordInput(userdata, value: value) }, toggle_secure_input_cb: { App.toggleSecureInput() } ) @@ -300,6 +301,7 @@ extension Ghostty { static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?, body: UnsafePointer?) {} static func updateRendererHealth(_ userdata: UnsafeMutableRawPointer?, health: ghostty_renderer_health_e) {} static func mouseOverLink(_ userdata: UnsafeMutableRawPointer?, uri: UnsafePointer?, len: Int) {} + static func setPasswordInput(_ userdata: UnsafeMutableRawPointer?, value: Bool) {} static func toggleSecureInput() {} #endif @@ -546,6 +548,11 @@ extension Ghostty { surfaceView.hoverUrl = String(data: buffer, encoding: .utf8) } + static func setPasswordInput(_ userdata: UnsafeMutableRawPointer?, value: Bool) { + let surfaceView = self.surfaceUserdata(from: userdata) + surfaceView.passwordInput = value + } + static func toggleSecureInput() { guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return } appDelegate.toggleSecureInput(self) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 8a617fdd6..c3ae03138 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -42,6 +42,21 @@ extension Ghostty { // then the view is moved to a new window. 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 // exit safely. var needsConfirmQuit: Bool { @@ -59,6 +74,7 @@ extension Ghostty { if (v.count == 0) { return nil } return v } + // Returns the inspector instance for this surface, or nil if the // surface has been closed. var inspector: ghostty_inspector_t? { @@ -185,6 +201,9 @@ extension Ghostty { mouseExited(with: NSEvent()) } + // Remove ourselves from secure input if we have to + SecureInput.shared.removeScoped(ObjectIdentifier(self)) + guard let surface = self.surface else { return } ghostty_surface_free(surface) } @@ -209,6 +228,11 @@ extension Ghostty { self.focused = 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... if #available(macOS 13, iOS 16, *) { if (focused) { diff --git a/src/Surface.zig b/src/Surface.zig index b0c3d67f4..3700df7d9 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -837,6 +837,11 @@ fn passwordInput(self: *Surface, v: bool) !void { 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(); } diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 846805f77..f57b16272 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -134,6 +134,11 @@ pub const App = struct { /// over a link. 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, }; @@ -1017,6 +1022,15 @@ pub const Surface = struct { 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 { const func = self.app.opts.new_tab orelse { log.info("runtime embedder does not support new_tab", .{});