diff --git a/src/Surface.zig b/src/Surface.zig index 309294e61..029a1b221 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -841,7 +841,7 @@ fn passwordInput(self: *Surface, v: bool) !void { self.rt_app.performAction( .{ .surface = self }, .secure_input, - v, + if (v) .on else .off, ) catch |err| { // We ignore this error because we don't want to fail this // entire operation just because the apprt failed to set @@ -3685,44 +3685,55 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool }, ), - .new_split => |direction| { - if (@hasDecl(apprt.Surface, "newSplit")) { - try self.rt_surface.newSplit(switch (direction) { - .right => .right, - .down => .down, - .auto => if (self.screen_size.width > self.screen_size.height) - .right - else - .down, - }); - } else log.warn("runtime doesn't implement newSplit", .{}); - }, + .new_split => |direction| try self.rt_app.performAction( + .{ .surface = self }, + .new_split, + switch (direction) { + .right => .right, + .down => .down, + .auto => if (self.screen_size.width > self.screen_size.height) + .right + else + .down, + }, + ), - .goto_split => |direction| { - if (@hasDecl(apprt.Surface, "gotoSplit")) { - self.rt_surface.gotoSplit(direction); - } else log.warn("runtime doesn't implement gotoSplit", .{}); - }, + .goto_split => |direction| try self.rt_app.performAction( + .{ .surface = self }, + .goto_split, + switch (direction) { + inline else => |tag| @field( + apprt.action.GotoSplit, + @tagName(tag), + ), + }, + ), - .resize_split => |param| { - if (@hasDecl(apprt.Surface, "resizeSplit")) { - const direction = param[0]; - const amount = param[1]; - self.rt_surface.resizeSplit(direction, amount); - } else log.warn("runtime doesn't implement resizeSplit", .{}); - }, + .resize_split => |value| try self.rt_app.performAction( + .{ .surface = self }, + .resize_split, + .{ + .amount = value[1], + .direction = switch (value[0]) { + inline else => |tag| @field( + apprt.action.ResizeSplit.Direction, + @tagName(tag), + ), + }, + }, + ), - .equalize_splits => { - if (@hasDecl(apprt.Surface, "equalizeSplits")) { - self.rt_surface.equalizeSplits(); - } else log.warn("runtime doesn't implement equalizeSplits", .{}); - }, + .equalize_splits => try self.rt_app.performAction( + .{ .surface = self }, + .equalize_splits, + {}, + ), - .toggle_split_zoom => { - if (@hasDecl(apprt.Surface, "toggleSplitZoom")) { - self.rt_surface.toggleSplitZoom(); - } else log.warn("runtime doesn't implement toggleSplitZoom", .{}); - }, + .toggle_split_zoom => try self.rt_app.performAction( + .{ .surface = self }, + .toggle_split_zoom, + {}, + ), .toggle_fullscreen => { if (@hasDecl(apprt.Surface, "toggleFullscreen")) { @@ -3730,17 +3741,17 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool } else log.warn("runtime doesn't implement toggleFullscreen", .{}); }, - .toggle_window_decorations => { - if (@hasDecl(apprt.Surface, "toggleWindowDecorations")) { - self.rt_surface.toggleWindowDecorations(); - } else log.warn("runtime doesn't implement toggleWindowDecorations", .{}); - }, + .toggle_window_decorations => try self.rt_app.performAction( + .{ .surface = self }, + .toggle_window_decorations, + {}, + ), - .toggle_secure_input => { - if (@hasDecl(apprt.Surface, "toggleSecureInput")) { - self.rt_surface.toggleSecureInput(); - } else log.warn("runtime doesn't implement toggleSecureInput", .{}); - }, + .toggle_secure_input => try self.rt_app.performAction( + .{ .surface = self }, + .secure_input, + .toggle, + ), .select_all => { const sel = self.io.terminal.screen.selectAll(); diff --git a/src/apprt.zig b/src/apprt.zig index 21a0e7805..7651ace9b 100644 --- a/src/apprt.zig +++ b/src/apprt.zig @@ -32,10 +32,8 @@ pub const ClipboardRequestType = structs.ClipboardRequestType; pub const ColorScheme = structs.ColorScheme; pub const CursorPos = structs.CursorPos; pub const DesktopNotification = structs.DesktopNotification; -pub const GotoTab = structs.GotoTab; pub const IMEPos = structs.IMEPos; pub const Selection = structs.Selection; -pub const SplitDirection = structs.SplitDirection; pub const SurfaceSize = structs.SurfaceSize; /// The implementation to use for the app runtime. This is comptime chosen diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 3f34ae8d8..72f0aadd5 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -21,12 +21,32 @@ pub const Action = union(enum) { /// the tab should be opened in a new window. new_tab, + /// Create a new split. The value determines the location of the split + /// relative to the target. + new_split: SplitDirection, + + /// Close all open windows. + close_all_windows, + + /// Toggle whether window directions are shown. + toggle_window_decorations, + /// Jump to a specific tab. Must handle the scenario that the tab /// value is invalid. goto_tab: GotoTab, - /// Close all open windows. - close_all_windows, + /// Jump to a specific split. + goto_split: GotoSplit, + + /// Resize the split in the given direction. + resize_split: ResizeSplit, + + /// Equalize all the splits in the target window. + equalize_splits, + + /// Toggle whether a split is zoomed or not. A zoomed split is resized + /// to take up the entire window. + toggle_split_zoom, /// Open the Ghostty configuration. This is platform-specific about /// what it means; it can mean opening a dedicated UI or just opening @@ -44,7 +64,7 @@ pub const Action = union(enum) { /// entering a password or other sensitive information. This can be used /// by the app runtime to change the appearance of the cursor, setup /// system APIs to not log the input, etc. - secure_input: bool, + secure_input: SecureInput, /// The enum of keys in the tagged union. pub const Key = @typeInfo(Action).Union.tag_type.?; @@ -68,6 +88,38 @@ pub const Target = union(enum) { surface: *CoreSurface, }; +// 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 SplitDirection = enum(c_int) { + right, + down, +}; + +// 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 GotoSplit = enum(c_int) { + previous, + next, + + top, + left, + bottom, + right, +}; + +/// The amount to resize the split by and the direction to resize it in. +pub const ResizeSplit = struct { + amount: u16, + direction: Direction, + + pub const Direction = enum(c_int) { + up, + down, + left, + right, + }; +}; + /// The tab to jump to. This is non-exhaustive so that integer values represent /// the index (zero-based) of the tab to jump to. Negative values are special /// values. @@ -77,3 +129,9 @@ pub const GotoTab = enum(c_int) { last = -3, _, }; + +pub const SecureInput = enum(c_int) { + on, + off, + toggle, +}; diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 8c602b5b0..a0e4230be 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -81,7 +81,7 @@ pub const App = struct { /// Create a new split view. If the embedder doesn't support split /// views then this can be null. - new_split: ?*const fn (SurfaceUD, apprt.SplitDirection, apprt.Surface.Options) callconv(.C) void = null, + new_split: ?*const fn (SurfaceUD, apprt.action.SplitDirection, apprt.Surface.Options) callconv(.C) void = null, /// New tab with options. The surface may be null if there is no target /// surface in which case the apprt is expected to create a new window. @@ -98,10 +98,10 @@ pub const App = struct { close_surface: ?*const fn (SurfaceUD, bool) callconv(.C) void = null, /// Focus the previous/next split (if any). - focus_split: ?*const fn (SurfaceUD, input.SplitFocusDirection) callconv(.C) void = null, + focus_split: ?*const fn (SurfaceUD, apprt.action.GotoSplit) callconv(.C) void = null, /// Resize the current split. - resize_split: ?*const fn (SurfaceUD, input.SplitResizeDirection, u16) callconv(.C) void = null, + resize_split: ?*const fn (SurfaceUD, apprt.action.ResizeSplit.Direction, u16) callconv(.C) void = null, /// Equalize all splits in the current window equalize_splits: ?*const fn (SurfaceUD) callconv(.C) void = null, @@ -534,16 +534,113 @@ pub const App = struct { } } - fn setPasswordInput(self: *App, target: apprt.Target, v: bool) void { - const func = self.opts.set_password_input orelse { - log.info("runtime embedder does not support set_password_input", .{}); + fn newSplit( + self: *const App, + target: apprt.Target, + direction: apprt.action.SplitDirection, + ) void { + const func = self.opts.new_split orelse { + log.info("runtime embedder does not support splits", .{}); return; }; - func(switch (target) { - .app => null, - .surface => |surface| surface.rt_surface.userdata, - }, v); + switch (target) { + .app => func(null, direction, .{}), + .surface => |v| func( + v.rt_surface.userdata, + direction, + v.rt_surface.newSurfaceOptions(), + ), + } + } + + fn gotoSplit( + self: *const App, + target: apprt.Target, + direction: apprt.action.GotoSplit, + ) void { + const func = self.opts.focus_split orelse { + log.info("runtime embedder does not support focus split", .{}); + return; + }; + + switch (target) { + .app => {}, + .surface => |v| func(v.rt_surface.userdata, direction), + } + } + + fn resizeSplit( + self: *const App, + target: apprt.Target, + resize: apprt.action.ResizeSplit, + ) void { + const func = self.opts.resize_split orelse { + log.info("runtime embedder does not support resize split", .{}); + return; + }; + + switch (target) { + .app => {}, + .surface => |v| func( + v.rt_surface.userdata, + resize.direction, + resize.amount, + ), + } + } + + pub fn equalizeSplits(self: *const App, target: apprt.Target) void { + const func = self.opts.equalize_splits orelse { + log.info("runtime embedder does not support equalize splits", .{}); + return; + }; + + switch (target) { + .app => func(null), + .surface => |v| func(v.rt_surface.userdata), + } + } + + fn toggleSplitZoom(self: *const App, target: apprt.Target) void { + const func = self.opts.toggle_split_zoom orelse { + log.info("runtime embedder does not support split zoom", .{}); + return; + }; + + switch (target) { + .app => func(null), + .surface => |v| func(v.rt_surface.userdata), + } + } + + fn setPasswordInput(self: *App, target: apprt.Target, v: apprt.action.SecureInput) void { + switch (v) { + inline .on, .off => |tag| { + const func = self.opts.set_password_input orelse { + log.info("runtime embedder does not support set_password_input", .{}); + return; + }; + + func(switch (target) { + .app => null, + .surface => |surface| surface.rt_surface.userdata, + }, switch (tag) { + .on => true, + .off => false, + else => comptime unreachable, + }); + }, + + .toggle => { + const func = self.opts.toggle_secure_input orelse { + log.info("runtime embedder does not support toggle_secure_input", .{}); + return; + }; + + func(); + }, + } } /// Perform a given action. @@ -561,11 +658,17 @@ pub const App = struct { .new_tab => self.newTab(target), .goto_tab => self.gotoTab(target, value), + .new_split => self.newSplit(target, value), + .resize_split => self.resizeSplit(target, value), + .equalize_splits => self.equalizeSplits(target), + .toggle_split_zoom => self.toggleSplitZoom(target), + .goto_split => self.gotoSplit(target, value), .open_config => try configpkg.edit.open(self.core_app.alloc), .secure_input => self.setPasswordInput(target, value), // Unimplemented .close_all_windows, + .toggle_window_decorations, .quit_timer, => log.warn("unimplemented action={}", .{action}), } @@ -795,16 +898,6 @@ pub const Surface = struct { func(self.userdata, mode); } - pub fn newSplit(self: *const Surface, direction: apprt.SplitDirection) !void { - const func = self.app.opts.new_split orelse { - log.info("runtime embedder does not support splits", .{}); - return; - }; - - const options = self.newSurfaceOptions(); - func(self.userdata, direction, options); - } - pub fn close(self: *const Surface, process_alive: bool) void { const func = self.app.opts.close_surface orelse { log.info("runtime embedder does not support closing a surface", .{}); @@ -814,42 +907,6 @@ pub const Surface = struct { func(self.userdata, process_alive); } - pub fn gotoSplit(self: *const Surface, direction: input.SplitFocusDirection) void { - const func = self.app.opts.focus_split orelse { - log.info("runtime embedder does not support focus split", .{}); - return; - }; - - func(self.userdata, direction); - } - - pub fn resizeSplit(self: *const Surface, direction: input.SplitResizeDirection, amount: u16) void { - const func = self.app.opts.resize_split orelse { - log.info("runtime embedder does not support resize split", .{}); - return; - }; - - func(self.userdata, direction, amount); - } - - pub fn equalizeSplits(self: *const Surface) void { - const func = self.app.opts.equalize_splits orelse { - log.info("runtime embedder does not support equalize splits", .{}); - return; - }; - - func(self.userdata); - } - - pub fn toggleSplitZoom(self: *const Surface) void { - const func = self.app.opts.toggle_split_zoom orelse { - log.info("runtime embedder does not support split zoom", .{}); - return; - }; - - func(self.userdata); - } - pub fn getContentScale(self: *const Surface) !apprt.ContentScale { return self.content_scale; } @@ -1137,15 +1194,6 @@ pub const Surface = struct { func(self.userdata, nonNativeFullscreen); } - pub fn toggleSecureInput(self: *Surface) void { - const func = self.app.opts.toggle_secure_input orelse { - log.info("runtime embedder does not toggle_secure_input", .{}); - return; - }; - - func(); - } - fn newWindow(self: *const Surface) !void { const func = self.app.opts.new_window orelse { log.info("runtime embedder does not support new_window", .{}); @@ -1899,26 +1947,61 @@ pub const CAPI = struct { } /// Request that the surface split in the given direction. - export fn ghostty_surface_split(ptr: *Surface, direction: apprt.SplitDirection) void { - ptr.newSplit(direction) catch {}; + export fn ghostty_surface_split(ptr: *Surface, direction: apprt.action.SplitDirection) void { + ptr.app.performAction( + .{ .surface = &ptr.core_surface }, + .new_split, + direction, + ) catch |err| { + log.err("error creating new split err={}", .{err}); + return; + }; } /// Focus on the next split (if any). - export fn ghostty_surface_split_focus(ptr: *Surface, direction: input.SplitFocusDirection) void { - ptr.gotoSplit(direction); + export fn ghostty_surface_split_focus( + ptr: *Surface, + direction: apprt.action.GotoSplit, + ) void { + ptr.app.performAction( + .{ .surface = &ptr.core_surface }, + .goto_split, + direction, + ) catch |err| { + log.err("error creating new split err={}", .{err}); + return; + }; } /// Resize the current split by moving the split divider in the given /// direction. `direction` specifies which direction the split divider will /// move relative to the focused split. `amount` is a fractional value /// between 0 and 1 that specifies by how much the divider will move. - export fn ghostty_surface_split_resize(ptr: *Surface, direction: input.SplitResizeDirection, amount: u16) void { - ptr.resizeSplit(direction, amount); + export fn ghostty_surface_split_resize( + ptr: *Surface, + direction: apprt.action.ResizeSplit.Direction, + amount: u16, + ) void { + ptr.app.performAction( + .{ .surface = &ptr.core_surface }, + .resize_split, + .{ .direction = direction, .amount = amount }, + ) catch |err| { + log.err("error resizing split err={}", .{err}); + return; + }; } /// Equalize the size of all splits in the current window. export fn ghostty_surface_split_equalize(ptr: *Surface) void { - ptr.equalizeSplits(); + ptr.app.performAction( + .{ .surface = &ptr.core_surface }, + .equalize_splits, + {}, + ) catch |err| { + log.err("error equalizing splits err={}", .{err}); + return; + }; } /// Invoke an action on the surface. diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index a3854a173..dabf63c2b 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -150,7 +150,13 @@ pub const App = struct { .open_config => try configpkg.edit.open(self.app.alloc), // Unimplemented + .new_split, + .goto_split, + .resize_split, + .equalize_splits, + .toggle_split_zoom, .close_all_windows, + .toggle_window_decorations, .goto_tab, .quit_timer, .secure_input, diff --git a/src/apprt/structs.zig b/src/apprt/structs.zig index 1982cc497..f8bcd72cf 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -62,13 +62,6 @@ pub const DesktopNotification = struct { body: []const u8, }; -// 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 SplitDirection = enum(c_int) { - right, - down, -}; - /// The color scheme in use (light vs dark). pub const ColorScheme = enum(u2) { light = 0, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 8f129065d..f39510554 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -410,8 +410,7 @@ pub const Action = union(enum) { // Note: we don't support top or left yet }; - // Extern because it is used in the embedded runtime ABI. - pub const SplitFocusDirection = enum(c_int) { + pub const SplitFocusDirection = enum { previous, next, @@ -421,8 +420,7 @@ pub const Action = union(enum) { right, }; - // Extern because it is used in the embedded runtime ABI. - pub const SplitResizeDirection = enum(c_int) { + pub const SplitResizeDirection = enum { up, down, left,