From db6d20dce1df0614b4903a4c5c5489a384ab8eeb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 9 Aug 2026 15:45:21 -0700 Subject: [PATCH] apprt/embedded: initialize the TIS keymap lazily The embedded apprt App init created the keyboard layout keymap eagerly, which requires talking to the text input system (TIS). The first TIS call in a process is slow: 6.6ms measured inside ghostty_app_new during app launch (up to ~30ms in a cold process). The keymap is only used for keyboard layout queries (option-as-alt detection, layout change reload), which happen once keyboard events are flowing. By then AppKit has already warmed TIS and the call is effectively free (~0.2us measured warm). So initialize the keymap lazily on first use. If the layout changes before the keymap was ever created, reload is a no-op since lazy init picks up the current layout. Measured on macOS (Apple Silicon) with local timing instrumentation during app launch: embedded app init before: ~6.7ms (keymap 6614us) embedded app init after: ~60us (config clone only) --- src/apprt/embedded.zig | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 4eb63dca4..6bab71e55 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -120,7 +120,13 @@ pub const App = struct { core_app: *CoreApp, opts: Options, - keymap: input.Keymap, + + /// The keyboard layout keymap. This is lazily initialized on first + /// use because creating it requires talking to the text input + /// system (TIS on macOS), and the first such call in a process is + /// slow (multiple milliseconds). It is only needed once keyboard + /// events start flowing, at which point the system is warm. + keymap: ?input.Keymap, /// The configuration for the app. This is owned by this structure. config: Config, @@ -136,19 +142,16 @@ pub const App = struct { var config_clone = try config.clone(alloc); errdefer config_clone.deinit(); - var keymap = try input.Keymap.init(); - errdefer keymap.deinit(); - self.* = .{ .core_app = core_app, .config = config_clone, .opts = opts, - .keymap = keymap, + .keymap = null, }; } pub fn terminate(self: *App) void { - self.keymap.deinit(); + if (self.keymap) |*v| v.deinit(); self.config.deinit(); } @@ -208,8 +211,10 @@ pub const App = struct { /// This should be called whenever the keyboard layout was changed. pub fn reloadKeymap(self: *App) !void { - // Reload the keymap - try self.keymap.reload(); + // Reload the keymap. If it was never initialized we don't need + // to do anything since lazy initialization will pick up the + // current layout. + if (self.keymap) |*v| try v.reload(); } /// Loads the keyboard layout. @@ -217,13 +222,25 @@ pub const App = struct { /// Kind of expensive so this should be avoided if possible. When I say /// "kind of expensive" I mean that its not something you probably want /// to run on every keypress. - pub fn keyboardLayout(self: *const App) input.KeyboardLayout { + pub fn keyboardLayout(self: *App) input.KeyboardLayout { // We only support keyboard layout detection on macOS. if (comptime builtin.os.tag != .macos) return .unknown; + // Lazily initialize the keymap. + const keymap: *input.Keymap = keymap: { + if (self.keymap == null) { + self.keymap = input.Keymap.init() catch |err| { + log.warn("error initializing keymap err={}", .{err}); + return .unknown; + }; + } + + break :keymap &self.keymap.?; + }; + // Any layout larger than this is not something we can handle. var buf: [256]u8 = undefined; - const id = self.keymap.sourceId(&buf) catch |err| { + const id = keymap.sourceId(&buf) catch |err| { comptime assert(@TypeOf(err) == error{OutOfMemory}); return .unknown; };