From 6ace63acc4a8eac8a47214c1b63ae9b435709a55 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 27 Jan 2026 08:09:41 -0800 Subject: [PATCH 1/6] macOS: xib changes from xcode Literally just opening these on Xcode 26.2 does this, so we should commit it lol. --- macos/Sources/Features/Terminal/Window Styles/Terminal.xib | 6 +++--- .../Terminal/Window Styles/TerminalTabsTitlebarTahoe.xib | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/Terminal.xib b/macos/Sources/Features/Terminal/Window Styles/Terminal.xib index cfbb2221c..5b76a9b12 100644 --- a/macos/Sources/Features/Terminal/Window Styles/Terminal.xib +++ b/macos/Sources/Features/Terminal/Window Styles/Terminal.xib @@ -1,8 +1,8 @@ - + - + @@ -17,7 +17,7 @@ - + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebarTahoe.xib b/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebarTahoe.xib index deaeded9f..ae7751b9c 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebarTahoe.xib +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebarTahoe.xib @@ -1,8 +1,8 @@ - + - + @@ -17,7 +17,7 @@ - + From 0de5f43254066d17e6fab1f556d0a66ed73555ec Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 27 Jan 2026 08:17:20 -0800 Subject: [PATCH 2/6] macos: filter only supported commands in the command palette This got accidentally regressed when we did the jump options. --- .../Command Palette/TerminalCommandPalette.swift | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index e0237f257..008fc992a 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -120,14 +120,16 @@ struct TerminalCommandPaletteView: View { /// Custom commands from the command-palette-entry configuration. private var terminalOptions: [CommandOption] { guard let appDelegate = NSApp.delegate as? AppDelegate else { return [] } - return appDelegate.ghostty.config.commandPaletteEntries.map { c in - CommandOption( - title: c.title, - description: c.description - ) { - onAction(c.action) + return appDelegate.ghostty.config.commandPaletteEntries + .filter(\.isSupported) + .map { c in + CommandOption( + title: c.title, + description: c.description + ) { + onAction(c.action) + } } - } } /// Commands for jumping to other terminal surfaces. From 7feb30a836487a52390f95cb7c25942a7062b435 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 27 Jan 2026 08:28:34 -0800 Subject: [PATCH 3/6] inspector: mode rows need a unique ID --- src/inspector/Inspector.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/inspector/Inspector.zig b/src/inspector/Inspector.zig index 156e2cb18..6ffb43d43 100644 --- a/src/inspector/Inspector.zig +++ b/src/inspector/Inspector.zig @@ -583,10 +583,12 @@ fn renderModesWindow(self: *Inspector) void { const tag: terminal.modes.ModeTag = @bitCast(@as(terminal.modes.ModeTag.Backing, field.value)); cimgui.c.ImGui_TableNextRow(); + cimgui.c.ImGui_PushIDInt(@intCast(field.value)); + defer cimgui.c.ImGui_PopID(); { _ = cimgui.c.ImGui_TableSetColumnIndex(0); var value: bool = t.modes.get(@field(terminal.Mode, field.name)); - _ = cimgui.c.ImGui_Checkbox("", &value); + _ = cimgui.c.ImGui_Checkbox("##checkbox", &value); } { _ = cimgui.c.ImGui_TableSetColumnIndex(1); From 32f5677a9482776a013137f04953f313dc81bd20 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 27 Jan 2026 08:34:31 -0800 Subject: [PATCH 4/6] macos: slow down inspector trackpad (precision) scrolling --- .../Sources/Ghostty/Surface View/InspectorView.swift | 10 ++-------- src/apprt/embedded.zig | 11 +++++++---- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/macos/Sources/Ghostty/Surface View/InspectorView.swift b/macos/Sources/Ghostty/Surface View/InspectorView.swift index 2a004ac76..e8eaf3a80 100644 --- a/macos/Sources/Ghostty/Surface View/InspectorView.swift +++ b/macos/Sources/Ghostty/Surface View/InspectorView.swift @@ -269,16 +269,10 @@ extension Ghostty { // Builds up the "input.ScrollMods" bitmask var mods: Int32 = 0 - var x = event.scrollingDeltaX - var y = event.scrollingDeltaY + let x = event.scrollingDeltaX + let y = event.scrollingDeltaY if event.hasPreciseScrollingDeltas { mods = 1 - - // We do a 2x speed multiplier. This is subjective, it "feels" better to me. - x *= 2; - y *= 2; - - // TODO(mitchellh): do we have to scale the x/y here by window scale factor? } // Determine our momentum value diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index b4ad7f885..a035caf62 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1133,15 +1133,18 @@ pub const Inspector = struct { yoff: f64, mods: input.ScrollMods, ) void { - _ = mods; - self.queueRender(); cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); + + // For precision scrolling (trackpads), the values are in pixels which + // scroll way too fast. Scale them down to approximate discrete wheel + // notches. imgui expects 1.0 to scroll ~5 lines of text. + const scale: f64 = if (mods.precision) 0.1 else 1.0; cimgui.c.ImGuiIO_AddMouseWheelEvent( io, - @floatCast(xoff), - @floatCast(yoff), + @floatCast(xoff * scale), + @floatCast(yoff * scale), ); } From f85653414c9e0178fb9328f3a98014e7dfe4e25e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 27 Jan 2026 09:08:37 -0800 Subject: [PATCH 5/6] renderer: keep a draw timer on when we have an inspector --- src/renderer/Thread.zig | 60 ++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index c1b377b3d..d651fed79 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -254,7 +254,7 @@ fn threadMain_(self: *Thread) !void { ); // Start the draw timer - self.startDrawTimer(); + self.syncDrawTimer(); // Run log.debug("starting renderer thread", .{}); @@ -292,11 +292,33 @@ fn setQosClass(self: *const Thread) void { } } -fn startDrawTimer(self: *Thread) void { - // If our renderer doesn't support animations then we never run this. - if (!@hasDecl(rendererpkg.Renderer, "hasAnimations")) return; - if (!self.renderer.hasAnimations()) return; - if (self.config.custom_shader_animation == .false) return; +fn syncDrawTimer(self: *Thread) void { + skip: { + // If we have an inspector, we always run the draw timer. + if (self.flags.has_inspector) break :skip; + + // If our renderer supports animations and has them, then we + // always have a draw timer. + if (@hasDecl(rendererpkg.Renderer, "hasAnimations") and + self.renderer.hasAnimations()) + { + break :skip; + } + + // If our config says to always animate, we do so. + switch (self.config.custom_shader_animation) { + // Always animate + .always => break :skip, + // Only when focused + .true => if (self.flags.focused) break :skip, + // Never animate + .false => {}, + } + + // We're skipping the draw timer. Stop it on the next iteration. + self.draw_active = false; + return; + } // Set our active state so it knows we're running. We set this before // even checking the active state in case we have a pending shutdown. @@ -316,11 +338,6 @@ fn startDrawTimer(self: *Thread) void { ); } -fn stopDrawTimer(self: *Thread) void { - // This will stop the draw on the next iteration. - self.draw_active = false; -} - /// Drain the mailbox. fn drainMailbox(self: *Thread) !void { // There's probably a more elegant way to do this... @@ -377,12 +394,10 @@ fn drainMailbox(self: *Thread) !void { // Set it on the renderer try self.renderer.setFocus(v); - if (!v) { - if (self.config.custom_shader_animation != .always) { - // Stop the draw timer - self.stopDrawTimer(); - } + // We always resync our draw timer (may disable it) + self.syncDrawTimer(); + if (!v) { // If we're not focused, then we stop the cursor blink if (self.cursor_c.state() == .active and self.cursor_c_cancel.state() == .dead) @@ -397,9 +412,6 @@ fn drainMailbox(self: *Thread) !void { ); } } else { - // Start the draw timer - self.startDrawTimer(); - // If we're focused, we immediately show the cursor again // and then restart the timer. if (self.cursor_c.state() != .active) { @@ -446,8 +458,7 @@ fn drainMailbox(self: *Thread) !void { // Stop and start the draw timer to capture the new // hasAnimations value. - self.stopDrawTimer(); - self.startDrawTimer(); + self.syncDrawTimer(); }, .search_viewport_matches => |v| { @@ -466,7 +477,12 @@ fn drainMailbox(self: *Thread) !void { self.renderer.search_matches_dirty = true; }, - .inspector => |v| self.flags.has_inspector = v, + .inspector => |v| { + self.flags.has_inspector = v; + // Reset our draw timer state, which might change due + // to the inspector change. + self.syncDrawTimer(); + }, .macos_display_id => |v| { if (@hasDecl(rendererpkg.Renderer, "setMacOSDisplayID")) { From bdfb45bca7d8aacaa3e6be6cff4e72958df62cd1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 27 Jan 2026 09:25:58 -0800 Subject: [PATCH 6/6] imgui delta time needs to use float math Our prior math converted to float after int which made our delta time wrong making hovers and stuff not work. --- src/apprt/embedded.zig | 7 ++++--- src/apprt/gtk/class/imgui_widget.zig | 12 ++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index a035caf62..2fbae8fdf 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1205,10 +1205,11 @@ pub const Inspector = struct { // Determine our delta time const now = try std.time.Instant.now(); io.DeltaTime = if (self.instant) |prev| delta: { - const since_ns = now.since(prev); - const since_s: f32 = @floatFromInt(since_ns / std.time.ns_per_s); + const since_ns: f64 = @floatFromInt(now.since(prev)); + const ns_per_s: f64 = @floatFromInt(std.time.ns_per_s); + const since_s: f32 = @floatCast(since_ns / ns_per_s); break :delta @max(0.00001, since_s); - } else (1 / 60); + } else (1.0 / 60.0); self.instant = now; } }; diff --git a/src/apprt/gtk/class/imgui_widget.zig b/src/apprt/gtk/class/imgui_widget.zig index 79e85fad2..8ad75f5d0 100644 --- a/src/apprt/gtk/class/imgui_widget.zig +++ b/src/apprt/gtk/class/imgui_widget.zig @@ -131,21 +131,17 @@ pub const ImguiWidget = extern struct { /// Initialize the frame. Expects that the context is already current. fn newFrame(self: *Self) void { - // If we can't determine the time since the last frame we default to - // 1/60th of a second. - const default_delta_time = 1 / 60; - const priv = self.private(); - const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); // Determine our delta time const now = std.time.Instant.now() catch unreachable; io.DeltaTime = if (priv.instant) |prev| delta: { - const since_ns = now.since(prev); - const since_s: f32 = @floatFromInt(since_ns / std.time.ns_per_s); + const since_ns: f64 = @floatFromInt(now.since(prev)); + const ns_per_s: f64 = @floatFromInt(std.time.ns_per_s); + const since_s: f32 = @floatCast(since_ns / ns_per_s); break :delta @max(0.00001, since_s); - } else default_delta_time; + } else (1.0 / 60.0); priv.instant = now; }