diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index f5b500b70b3..ad55fa74f2c 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -260,8 +260,12 @@ class BaseTerminalController: NSWindowController, return nil } + // If auto-equalize-splits is enabled, equalize the new tree before + // committing it so all splits in the window become evenly sized. + let finalTree = derivedConfig.autoEqualizeSplits ? newTree.equalized() : newTree + replaceSurfaceTree( - newTree, + finalTree, moveFocusTo: newView, moveFocusFrom: oldView, undoAction: "New Split") @@ -456,8 +460,13 @@ class BaseTerminalController: NSWindowController, nil } + // If auto-equalize-splits is enabled, equalize the post-removal tree + // so the surviving splits in the window become evenly sized. + let removed = surfaceTree.removing(node) + let finalTree = derivedConfig.autoEqualizeSplits ? removed.equalized() : removed + replaceSurfaceTree( - surfaceTree.removing(node), + finalTree, moveFocusTo: nextFocus, moveFocusFrom: focusedSurface, undoAction: "Close Terminal" @@ -1447,12 +1456,14 @@ class BaseTerminalController: NSWindowController, let windowStepResize: Bool let focusFollowsMouse: Bool let splitPreserveZoom: Ghostty.Config.SplitPreserveZoom + let autoEqualizeSplits: Bool init() { self.macosTitlebarProxyIcon = .visible self.windowStepResize = false self.focusFollowsMouse = false self.splitPreserveZoom = .init() + self.autoEqualizeSplits = false } init(_ config: Ghostty.Config) { @@ -1460,6 +1471,7 @@ class BaseTerminalController: NSWindowController, self.windowStepResize = config.windowStepResize self.focusFollowsMouse = config.focusFollowsMouse self.splitPreserveZoom = config.splitPreserveZoom + self.autoEqualizeSplits = config.autoEqualizeSplits } } } diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 9b094f21c0b..c61b8aa12f7 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -181,6 +181,14 @@ extension Ghostty { return .milliseconds(v) } + var autoEqualizeSplits: Bool { + guard let config = self.config else { return false } + var v = false + let key = "auto-equalize-splits" + _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) + return v + } + var splitPreserveZoom: SplitPreserveZoom { guard let config = self.config else { return .init() } var v: CUnsignedInt = 0 diff --git a/src/apprt/gtk/class/split_tree.zig b/src/apprt/gtk/class/split_tree.zig index 311fbd8a6cf..1cd1f5468ad 100644 --- a/src/apprt/gtk/class/split_tree.zig +++ b/src/apprt/gtk/class/split_tree.zig @@ -273,6 +273,10 @@ pub const SplitTree = extern struct { .{ handle, direction, old_tree, &new_tree }, ); + // Auto-equalize the whole tree if the user opted in. We do this + // before publishing the tree so the resize is invisible to listeners. + autoEqualize(&new_tree, alloc); + // Replace our tree self.setTree(&new_tree); } @@ -646,6 +650,25 @@ pub const SplitTree = extern struct { self.setTree(&new_tree); } + /// If `auto-equalize-splits` is enabled, replace `tree` in place with an + /// equalized version. On allocation failure we log and leave `tree` + /// untouched so the create/close operation still succeeds with the + /// non-equalized layout. + fn autoEqualize(tree: *Surface.Tree, alloc: Allocator) void { + const app = Application.default(); + const config_obj = app.getConfig(); + defer config_obj.unref(); + const config = config_obj.get(); + if (!config.@"auto-equalize-splits") return; + + const equalized = tree.equalize(alloc) catch |err| { + log.warn("unable to auto-equalize tree: {}", .{err}); + return; + }; + tree.deinit(); + tree.* = equalized; + } + pub fn actionZoom( _: *gio.SimpleAction, _: ?*glib.Variant, @@ -744,6 +767,10 @@ pub const SplitTree = extern struct { return; }; defer new_tree.deinit(); + + // Auto-equalize the surviving splits if the user opted in. + autoEqualize(&new_tree, Application.default().allocator()); + self.setTree(&new_tree); // Grab focus. We have to set this on the "last focused" because our diff --git a/src/config/Config.zig b/src/config/Config.zig index 5b1d73deb6c..f0354843b91 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2027,6 +2027,20 @@ keybind: Keybinds = .{}, /// working directory will be used (the `working-directory` option). @"split-inherit-working-directory": bool = true, +/// If true, all splits in the current window are automatically resized to +/// be equal whenever a split is created or closed. This is equivalent to +/// invoking the `equalize_splits` action after every split create/close. +/// +/// Because the equalization runs over the entire split tree of the window, +/// any prior manual resizes (e.g., via `resize_split`) within that tree are +/// reset by this option whenever any split in the same window is created or +/// closed. +/// +/// Default is `false` to preserve existing behavior. Users who want every +/// split to always be evenly sized (similar to iTerm2's default) should set +/// this to `true`. +@"auto-equalize-splits": bool = false, + /// If true, new windows and tabs will inherit the font size of the previously /// focused window. If no window was previously focused, the default font size /// will be used. If this is false, the default font size specified in the