From d364e421a84de0af7002a237e9930f91237f2af1 Mon Sep 17 00:00:00 2001 From: lorenries Date: Wed, 8 Oct 2025 12:04:42 -0400 Subject: [PATCH] introduce split-preserve-zoom config to maintain zoomed splits during navigation --- .../Terminal/BaseTerminalController.swift | 12 ++++++-- macos/Sources/Ghostty/Ghostty.Config.swift | 14 +++++++++ src/apprt/gtk/class/split_tree.zig | 29 +++++++++++++++++++ src/config/Config.zig | 12 ++++++++ src/config/c_get.zig | 16 ++++++++++ 5 files changed, 81 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 6336f0f55..98f1bcbf8 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -621,9 +621,14 @@ class BaseTerminalController: NSWindowController, return } - // Remove the zoomed state for this surface tree. if surfaceTree.zoomed != nil { - surfaceTree = .init(root: surfaceTree.root, zoomed: nil) + if derivedConfig.splitPreserveZoom.contains(.navigation) { + surfaceTree = SplitTree( + root: surfaceTree.root, + zoomed: surfaceTree.root?.node(view: nextSurface)) + } else { + surfaceTree = SplitTree(root: surfaceTree.root, zoomed: nil) + } } // Move focus to the next surface @@ -1188,17 +1193,20 @@ class BaseTerminalController: NSWindowController, let macosTitlebarProxyIcon: Ghostty.MacOSTitlebarProxyIcon let windowStepResize: Bool let focusFollowsMouse: Bool + let splitPreserveZoom: Ghostty.Config.SplitPreserveZoom init() { self.macosTitlebarProxyIcon = .visible self.windowStepResize = false self.focusFollowsMouse = false + self.splitPreserveZoom = .init() } init(_ config: Ghostty.Config) { self.macosTitlebarProxyIcon = config.macosTitlebarProxyIcon self.windowStepResize = config.windowStepResize self.focusFollowsMouse = config.focusFollowsMouse + self.splitPreserveZoom = config.splitPreserveZoom } } } diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 47826a104..7ea545f7a 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -124,6 +124,14 @@ extension Ghostty { return .init(rawValue: v) } + var splitPreserveZoom: SplitPreserveZoom { + guard let config = self.config else { return .init() } + var v: CUnsignedInt = 0 + let key = "split-preserve-zoom" + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .init() } + return .init(rawValue: v) + } + var initialWindow: Bool { guard let config = self.config else { return true } var v = true; @@ -690,6 +698,12 @@ extension Ghostty.Config { static let border = BellFeatures(rawValue: 1 << 4) } + struct SplitPreserveZoom: OptionSet { + let rawValue: CUnsignedInt + + static let navigation = SplitPreserveZoom(rawValue: 1 << 0) + } + enum MacDockDropBehavior: String { case new_tab = "new-tab" case new_window = "new-window" diff --git a/src/apprt/gtk/class/split_tree.zig b/src/apprt/gtk/class/split_tree.zig index 48656c951..46b3268d9 100644 --- a/src/apprt/gtk/class/split_tree.zig +++ b/src/apprt/gtk/class/split_tree.zig @@ -340,6 +340,35 @@ pub const SplitTree = extern struct { const surface = tree.nodes[target.idx()].leaf; surface.grabFocus(); + // We also need to setup our last_focused to this because if we + // trigger a tree change like below, the grab focus above never + // actually triggers in time to set this and this ensures we + // grab focus to the right thing. + const old_last_focused = self.private().last_focused.get(); + defer if (old_last_focused) |v| v.unref(); // unref strong ref from get + self.private().last_focused.set(surface); + errdefer self.private().last_focused.set(old_last_focused); + + if (tree.zoomed != null) { + const app = Application.default(); + const config_obj = app.getConfig(); + defer config_obj.unref(); + const config = config_obj.get(); + + if (!config.@"split-preserve-zoom".navigation) { + tree.zoomed = null; + } else { + tree.zoom(target); + } + + // When the zoom state changes our tree state changes and + // we need to send the proper notifications to trigger + // relayout. + const object = self.as(gobject.Object); + object.notifyByPspec(properties.tree.impl.param_spec); + object.notifyByPspec(properties.@"is-zoomed".impl.param_spec); + } + return true; } diff --git a/src/config/Config.zig b/src/config/Config.zig index 409e35516..7ced916fe 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -985,6 +985,14 @@ palette: Palette = .{}, /// Available since: 1.1.0 @"split-divider-color": ?Color = null, +/// Control when Ghostty preserves the zoomed state of a split. This is a packed +/// struct so more options can be added in the future. The `navigation` option +/// keeps the current split zoomed when split navigation (`goto_split`) changes +/// the focused split. +/// +/// Example: `split-preserve-zoom = navigation` +@"split-preserve-zoom": SplitPreserveZoom = .{}, + /// The foreground and background color for search matches. This only applies /// to non-focused search matches, also known as candidate matches. /// @@ -7423,6 +7431,10 @@ pub const ShellIntegrationFeatures = packed struct { path: bool = true, }; +pub const SplitPreserveZoom = packed struct { + navigation: bool = false, +}; + pub const RepeatableCommand = struct { value: std.ArrayListUnmanaged(inputpkg.Command) = .empty, diff --git a/src/config/c_get.zig b/src/config/c_get.zig index 0f8f897a2..dcfdc6716 100644 --- a/src/config/c_get.zig +++ b/src/config/c_get.zig @@ -222,3 +222,19 @@ test "c_get: background-blur" { try testing.expectEqual(-2, cval); } } + +test "c_get: split-preserve-zoom" { + const testing = std.testing; + const alloc = testing.allocator; + + var c = try Config.default(alloc); + defer c.deinit(); + + var bits: c_uint = undefined; + try testing.expect(get(&c, .@"split-preserve-zoom", @ptrCast(&bits))); + try testing.expectEqual(@as(c_uint, 0), bits); + + c.@"split-preserve-zoom".navigation = true; + try testing.expect(get(&c, .@"split-preserve-zoom", @ptrCast(&bits))); + try testing.expectEqual(@as(c_uint, 1), bits); +}