From 1c1ef99fb1d7cdab5af3d058fc2ff51867eab26a Mon Sep 17 00:00:00 2001 From: Max Bretschneider Date: Tue, 21 Oct 2025 22:13:42 +0200 Subject: [PATCH] Window switching initial --- include/ghostty.h | 6 +++++ src/Surface.zig | 11 +++++++++ src/apprt/action.zig | 11 +++++++++ src/apprt/gtk/class/application.zig | 36 +++++++++++++++++++++++++++++ src/input/Binding.zig | 10 ++++++++ src/input/command.zig | 14 +++++++++++ 6 files changed, 88 insertions(+) diff --git a/include/ghostty.h b/include/ghostty.h index a75fdc245..82ac392e2 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -512,6 +512,12 @@ typedef enum { GHOSTTY_GOTO_SPLIT_RIGHT, } ghostty_action_goto_split_e; +// apprt.action.GotoWindow +typedef enum { + GHOSTTY_GOTO_WINDOW_PREVIOUS, + GHOSTTY_GOTO_WINDOW_NEXT, +} ghostty_action_goto_window_e; + // apprt.action.ResizeSplit.Direction typedef enum { GHOSTTY_RESIZE_SPLIT_UP, diff --git a/src/Surface.zig b/src/Surface.zig index a3b306fef..69a390c2d 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -5390,6 +5390,17 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool }, ), + .goto_window => |direction| return try self.rt_app.performAction( + .{ .surface = self }, + .goto_window, + switch (direction) { + inline else => |tag| @field( + apprt.action.GotoWindow, + @tagName(tag), + ), + }, + ), + .resize_split => |value| return try self.rt_app.performAction( .{ .surface = self }, .resize_split, diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 608081a46..4bb590eee 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -129,6 +129,9 @@ pub const Action = union(Key) { /// Jump to a specific split. goto_split: GotoSplit, + /// Jump to next/previous window. + goto_window: GotoWindow, + /// Resize the split in the given direction. resize_split: ResizeSplit, @@ -335,6 +338,7 @@ pub const Action = union(Key) { move_tab, goto_tab, goto_split, + goto_window, resize_split, equalize_splits, toggle_split_zoom, @@ -474,6 +478,13 @@ pub const GotoSplit = enum(c_int) { right, }; +// This is made extern (c_int) to make interop easier with our embedded +// runtime. The small size cost doesn't make a difference in our union. +pub const GotoWindow = enum(c_int) { + previous, + next, +}; + /// The amount to resize the split by and the direction to resize it in. pub const ResizeSplit = extern struct { amount: u16, diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index efca498b4..e53201c96 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -659,6 +659,8 @@ pub const Application = extern struct { .goto_split => return Action.gotoSplit(target, value), + .goto_window => return Action.gotoWindow(value), + .goto_tab => return Action.gotoTab(target, value), .initial_size => return Action.initialSize(target, value), @@ -2014,6 +2016,40 @@ const Action = struct { } } + pub fn gotoWindow( + direction: apprt.action.GotoWindow, + ) bool { + const glist = gtk.Window.listToplevels(); + defer glist.free(); + + const node = glist.findCustom(null, findActiveWindow); + + // Check based on direction if we are at beginning or end of window list to loop around + // else just go to next/previous window + switch(direction) { + .next => { + const next_node = node.f_next orelse glist; + + const window: *gtk.Window = @ptrCast(@alignCast(next_node.f_data orelse return false)); + gtk.Window.present(window); + return true; + }, + .previous => { + const prev_node = node.f_prev orelse last: { + var current = glist; + while (current.f_next) |next| { + current = next; + } + break :last current; + }; + const window: *gtk.Window = @ptrCast(@alignCast(prev_node.f_data orelse return false)); + gtk.Window.present(window); + return true; + }, + } + return false; + } + pub fn initialSize( target: apprt.Target, value: apprt.action.InitialSize, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index d368c48b2..0a927b85f 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -545,6 +545,10 @@ pub const Action = union(enum) { /// (`previous` and `next`). goto_split: SplitFocusDirection, + + /// Focus on either the previous window or the next one ('previous', 'next') + goto_window: WindowDirection, + /// Zoom in or out of the current split. /// /// When a split is zoomed into, it will take up the entire space in @@ -931,6 +935,11 @@ pub const Action = union(enum) { right, }; + pub const WindowDirection = enum { + previous, + next, + }; + pub const SplitResizeParameter = struct { SplitResizeDirection, u16, @@ -1250,6 +1259,7 @@ pub const Action = union(enum) { .toggle_tab_overview, .new_split, .goto_split, + .goto_window, .toggle_split_zoom, .toggle_readonly, .resize_split, diff --git a/src/input/command.zig b/src/input/command.zig index ce218718f..037b5317c 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -479,6 +479,20 @@ fn actionCommands(action: Action.Key) []const Command { }, }, + .goto_window => comptime &.{ + .{ + .action = .{ .goto_window = .previous }, + .title = "Focus Window: Previous", + .description = "Focus the previous window, if any.", + }, + .{ + .action = .{ .goto_window = .previous }, + .title = "Focus Window: Next", + .description = "Focus the next window, if any.", + }, + }, + + .toggle_split_zoom => comptime &.{.{ .action = .toggle_split_zoom, .title = "Toggle Split Zoom",