diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 30efb289e..3b3dd9626 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -549,6 +549,9 @@ extension Ghostty { } static func setPasswordInput(_ userdata: UnsafeMutableRawPointer?, value: Bool) { + // We don't currently allow global password input being set from this. + guard let userdata else { return } + let surfaceView = self.surfaceUserdata(from: userdata) guard let appState = self.appState(fromView: surfaceView) else { return } guard appState.config.autoSecureInput else { return } diff --git a/src/App.zig b/src/App.zig index d93e00a2a..4462c7c83 100644 --- a/src/App.zig +++ b/src/App.zig @@ -138,7 +138,13 @@ pub fn addSurface(self: *App, rt_surface: *apprt.Surface) !void { // Since we have non-zero surfaces, we can cancel the quit timer. // It is up to the apprt if there is a quit timer at all and if it // should be canceled. - if (@hasDecl(apprt.App, "cancelQuitTimer")) rt_surface.app.cancelQuitTimer(); + rt_surface.app.performAction( + .{ .surface = &rt_surface.core_surface }, + .quit_timer, + .stop, + ) catch |err| { + log.warn("error stopping quit timer err={}", .{err}); + }; } /// Delete the surface from the known surface list. This will NOT call the @@ -166,8 +172,13 @@ pub fn deleteSurface(self: *App, rt_surface: *apprt.Surface) void { // If we have no surfaces, we can start the quit timer. It is up to the // apprt to determine if this is necessary. - if (@hasDecl(apprt.App, "startQuitTimer") and - self.surfaces.items.len == 0) rt_surface.app.startQuitTimer(); + if (self.surfaces.items.len == 0) rt_surface.app.performAction( + .{ .surface = &rt_surface.core_surface }, + .quit_timer, + .start, + ) catch |err| { + log.warn("error starting quit timer err={}", .{err}); + }; } /// The last focused surface. This is only valid while on the main thread @@ -194,7 +205,7 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void { log.debug("mailbox message={s}", .{@tagName(message)}); switch (message) { .reload_config => try self.reloadConfig(rt_app), - .open_config => try self.openConfig(rt_app), + .open_config => try self.performAction(rt_app, .open_config), .new_window => |msg| try self.newWindow(rt_app, msg), .close => |surface| try self.closeSurface(surface), .quit => try self.setQuit(), @@ -205,12 +216,6 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void { } } -pub fn openConfig(self: *App, rt_app: *apprt.App) !void { - _ = self; - log.debug("opening configuration", .{}); - try rt_app.openConfig(); -} - pub fn reloadConfig(self: *App, rt_app: *apprt.App) !void { log.debug("reloading configuration", .{}); if (try rt_app.reloadConfig()) |new| { @@ -241,19 +246,17 @@ fn redrawInspector(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) !voi /// Create a new window pub fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void { - if (!@hasDecl(apprt.App, "newWindow")) { - log.warn("newWindow is not supported by this runtime", .{}); - return; - } + const target: apprt.Target = target: { + const parent = msg.parent orelse break :target .app; + if (self.hasSurface(parent)) break :target .{ .surface = parent }; + break :target .app; + }; - const parent = if (msg.parent) |parent| parent: { - break :parent if (self.hasSurface(parent)) - parent - else - null; - } else null; - - try rt_app.newWindow(parent); + try rt_app.performAction( + target, + .new_window, + {}, + ); } /// Start quitting @@ -318,13 +321,9 @@ pub fn performAction( .ignore => {}, .quit => try self.setQuit(), .new_window => try self.newWindow(rt_app, .{ .parent = null }), - .open_config => try self.openConfig(rt_app), + .open_config => try rt_app.performAction(.app, .open_config, {}), .reload_config => try self.reloadConfig(rt_app), - .close_all_windows => { - if (@hasDecl(apprt.App, "closeAllWindows")) { - rt_app.closeAllWindows(); - } else log.warn("runtime doesn't implement closeAllWindows", .{}); - }, + .close_all_windows => try rt_app.performAction(.app, .close_all_windows, {}), } } diff --git a/src/Surface.zig b/src/Surface.zig index 85a5face0..ed83b2af8 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -747,11 +747,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { }, .report_title => |style| { - const title: ?[:0]const u8 = title: { - if (!@hasDecl(apprt.runtime.Surface, "getTitle")) break :title null; - break :title self.rt_surface.getTitle(); - }; - + const title: ?[:0]const u8 = self.rt_surface.getTitle(); const data = switch (style) { .csi_21_t => try std.fmt.allocPrint( self.alloc, @@ -838,9 +834,16 @@ fn passwordInput(self: *Surface, v: bool) !void { } // Notify our apprt so it can do whatever it wants. - if (@hasDecl(apprt.Surface, "setPasswordInput")) { - self.rt_surface.setPasswordInput(v); - } + self.rt_app.performAction( + .{ .surface = self }, + .secure_input, + 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 + // the secure input state. + log.warn("apprt failed to set secure input state err={}", .{err}); + }; try self.queueRender(); } @@ -894,7 +897,6 @@ fn modsChanged(self: *Surface, mods: input.Mods) void { /// Called when our renderer health state changes. fn updateRendererHealth(self: *Surface, health: renderer.Health) void { log.warn("renderer health status change status={}", .{health}); - if (!@hasDecl(apprt.runtime.Surface, "updateRendererHealth")) return; self.rt_surface.updateRendererHealth(health); } @@ -1151,10 +1153,8 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) !void { // Check if our runtime supports the selection clipboard at all. // We can save a lot of work if it doesn't. - if (@hasDecl(apprt.runtime.Surface, "supportsClipboard")) { - if (!self.rt_surface.supportsClipboard(clipboard)) { - return; - } + if (!self.rt_surface.supportsClipboard(clipboard)) { + return; } const buf = self.io.terminal.screen.selectionString(self.alloc, .{ @@ -3656,113 +3656,99 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool v, ), - .new_tab => { - if (@hasDecl(apprt.Surface, "newTab")) { - try self.rt_surface.newTab(); - } else log.warn("runtime doesn't implement newTab", .{}); - }, + .new_tab => try self.rt_app.performAction( + .{ .surface = self }, + .new_tab, + {}, + ), - .previous_tab => { - if (@hasDecl(apprt.Surface, "hasTabs")) { - if (!self.rt_surface.hasTabs()) { - log.debug("surface has no tabs, ignoring previous_tab binding", .{}); - return false; - } - } + inline .previous_tab, + .next_tab, + .last_tab, + .goto_tab, + => |v, tag| try self.rt_app.performAction( + .{ .surface = self }, + .goto_tab, + switch (tag) { + .previous_tab => .previous, + .next_tab => .next, + .last_tab => .last, + .goto_tab => @enumFromInt(v), + else => comptime unreachable, + }, + ), - if (@hasDecl(apprt.Surface, "gotoTab")) { - self.rt_surface.gotoTab(.previous); - } else log.warn("runtime doesn't implement gotoTab", .{}); - }, + .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, + }, + ), - .next_tab => { - if (@hasDecl(apprt.Surface, "hasTabs")) { - if (!self.rt_surface.hasTabs()) { - log.debug("surface has no tabs, ignoring next_tab binding", .{}); - return false; - } - } + .goto_split => |direction| try self.rt_app.performAction( + .{ .surface = self }, + .goto_split, + switch (direction) { + inline else => |tag| @field( + apprt.action.GotoSplit, + @tagName(tag), + ), + }, + ), - if (@hasDecl(apprt.Surface, "gotoTab")) { - self.rt_surface.gotoTab(.next); - } else log.warn("runtime doesn't implement gotoTab", .{}); - }, + .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), + ), + }, + }, + ), - .last_tab => { - if (@hasDecl(apprt.Surface, "hasTabs")) { - if (!self.rt_surface.hasTabs()) { - log.debug("surface has no tabs, ignoring last_tab binding", .{}); - return false; - } - } + .equalize_splits => try self.rt_app.performAction( + .{ .surface = self }, + .equalize_splits, + {}, + ), - if (@hasDecl(apprt.Surface, "gotoTab")) { - self.rt_surface.gotoTab(.last); - } else log.warn("runtime doesn't implement gotoTab", .{}); - }, + .toggle_split_zoom => try self.rt_app.performAction( + .{ .surface = self }, + .toggle_split_zoom, + {}, + ), - .goto_tab => |n| { - if (@hasDecl(apprt.Surface, "gotoTab")) { - self.rt_surface.gotoTab(@enumFromInt(n)); - } else log.warn("runtime doesn't implement gotoTab", .{}); - }, + .toggle_fullscreen => try self.rt_app.performAction( + .{ .surface = self }, + .toggle_fullscreen, + switch (self.config.macos_non_native_fullscreen) { + .false => .native, + .true => .macos_non_native, + .@"visible-menu" => .macos_non_native_visible_menu, + }, + ), - .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", .{}); - }, + .toggle_window_decorations => try self.rt_app.performAction( + .{ .surface = self }, + .toggle_window_decorations, + {}, + ), - .goto_split => |direction| { - if (@hasDecl(apprt.Surface, "gotoSplit")) { - self.rt_surface.gotoSplit(direction); - } else log.warn("runtime doesn't implement gotoSplit", .{}); - }, - - .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", .{}); - }, - - .equalize_splits => { - if (@hasDecl(apprt.Surface, "equalizeSplits")) { - self.rt_surface.equalizeSplits(); - } else log.warn("runtime doesn't implement equalizeSplits", .{}); - }, - - .toggle_split_zoom => { - if (@hasDecl(apprt.Surface, "toggleSplitZoom")) { - self.rt_surface.toggleSplitZoom(); - } else log.warn("runtime doesn't implement toggleSplitZoom", .{}); - }, - - .toggle_fullscreen => { - if (@hasDecl(apprt.Surface, "toggleFullscreen")) { - self.rt_surface.toggleFullscreen(self.config.macos_non_native_fullscreen); - } 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_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(); @@ -3772,11 +3758,16 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool } }, - .inspector => |mode| { - if (@hasDecl(apprt.Surface, "controlInspector")) { - self.rt_surface.controlInspector(mode); - } else log.warn("runtime doesn't implement controlInspector", .{}); - }, + .inspector => |mode| try self.rt_app.performAction( + .{ .surface = self }, + .inspector, + switch (mode) { + inline else => |tag| @field( + apprt.action.Inspector, + @tagName(tag), + ), + }, + ), .close_surface => self.close(), @@ -4163,11 +4154,6 @@ fn completeClipboardReadOSC52( } fn showDesktopNotification(self: *Surface, title: [:0]const u8, body: [:0]const u8) !void { - if (comptime !@hasDecl(apprt.Surface, "showDesktopNotification")) { - log.warn("runtime doesn't support desktop notifications", .{}); - return; - } - // Wyhash is used to hash the contents of the desktop notification to limit // how fast identical notifications can be sent sequentially. const hash_algorithm = std.hash.Wyhash; @@ -4203,7 +4189,14 @@ fn showDesktopNotification(self: *Surface, title: [:0]const u8, body: [:0]const self.app.last_notification_time = now; self.app.last_notification_digest = new_digest; - try self.rt_surface.showDesktopNotification(title, body); + try self.rt_app.performAction( + .{ .surface = self }, + .desktop_notification, + .{ + .title = title, + .body = body, + }, + ); } fn crashThreadState(self: *Surface) crash.sentry.ThreadState { @@ -4216,9 +4209,11 @@ fn crashThreadState(self: *Surface) crash.sentry.ThreadState { /// Tell the surface to present itself to the user. This may involve raising the /// window and switching tabs. fn presentSurface(self: *Surface) !void { - if (@hasDecl(apprt.Surface, "presentSurface")) { - self.rt_surface.presentSurface(); - } else log.warn("runtime doesn't support presentSurface", .{}); + try self.rt_app.performAction( + .{ .surface = self }, + .present_terminal, + {}, + ); } pub const face_ttf = @embedFile("font/res/JetBrainsMono-Regular.ttf"); diff --git a/src/apprt.zig b/src/apprt.zig index 491f1b8b5..7651ace9b 100644 --- a/src/apprt.zig +++ b/src/apprt.zig @@ -14,6 +14,7 @@ const build_config = @import("build_config.zig"); const structs = @import("apprt/structs.zig"); +pub const action = @import("apprt/action.zig"); pub const glfw = @import("apprt/glfw.zig"); pub const gtk = @import("apprt/gtk.zig"); pub const none = @import("apprt/none.zig"); @@ -21,6 +22,9 @@ pub const browser = @import("apprt/browser.zig"); pub const embedded = @import("apprt/embedded.zig"); pub const surface = @import("apprt/surface.zig"); +pub const Action = action.Action; +pub const Target = action.Target; + pub const ContentScale = structs.ContentScale; pub const Clipboard = structs.Clipboard; pub const ClipboardRequest = structs.ClipboardRequest; @@ -28,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 @@ -84,4 +86,6 @@ pub const Runtime = enum { test { _ = Runtime; _ = runtime; + _ = action; + _ = structs; } diff --git a/src/apprt/action.zig b/src/apprt/action.zig new file mode 100644 index 000000000..edeb02d7a --- /dev/null +++ b/src/apprt/action.zig @@ -0,0 +1,177 @@ +const std = @import("std"); +const assert = std.debug.assert; +const CoreSurface = @import("../Surface.zig"); + +/// The target for an action. This is generally the thing that had focus +/// while the action was made but the concept of "focus" is not guaranteed +/// since actions can also be triggered by timers, scripts, etc. +pub const Target = union(enum) { + app, + surface: *CoreSurface, +}; + +/// The possible actions an apprt has to react to. Actions are one-way +/// messages that are sent to the app runtime to trigger some behavior. +/// +/// Actions are very often key binding actions but can also be triggered +/// by lifecycle events. For example, the `quit_timer` action is not bindable. +/// +/// Importantly, actions are generally OPTIONAL to implement by an apprt. +/// Required functionality is called directly on the runtime structure so +/// there is a compiler error if an action is not implemented. +pub const Action = union(enum) { + /// Open a new window. The target determines whether properties such + /// as font size should be inherited. + new_window, + + /// Open a new tab. If the target is a surface it should be opened in + /// the same window as the surface. If the target is the app then + /// 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 fullscreen mode. + toggle_fullscreen: Fullscreen, + + /// 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, + + /// 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, + + /// Present the target terminal whether its a tab, split, or window. + present_terminal, + + /// Control whether the inspector is shown or hidden. + inspector: Inspector, + + /// Show a desktop notification. + desktop_notification: DesktopNotification, + + /// Open the Ghostty configuration. This is platform-specific about + /// what it means; it can mean opening a dedicated UI or just opening + /// a file in a text editor. + open_config, + + /// Called when there are no more surfaces and the app should quit + /// after the configured delay. This can be cancelled by sending + /// another quit_timer action with "stop". Multiple "starts" shouldn't + /// happen and can be ignored or cause a restart it isn't that important. + quit_timer: QuitTimer, + + /// Set the secure input functionality on or off. "Secure input" means + /// that the user is currently at some sort of prompt where they may be + /// 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: SecureInput, + + /// The enum of keys in the tagged union. + pub const Key = @typeInfo(Action).Union.tag_type.?; + + /// Returns the value type for the given key. + pub fn Value(comptime key: Key) type { + inline for (@typeInfo(Action).Union.fields) |field| { + const field_key = @field(Key, field.name); + if (field_key == key) return field.type; + } + + unreachable; + } +}; + +// 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. +pub const GotoTab = enum(c_int) { + previous = -1, + next = -2, + last = -3, + _, +}; + +/// The fullscreen mode to toggle to if we're moving to fullscreen. +pub const Fullscreen = enum(c_int) { + native, + + /// macOS has a non-native fullscreen mode that is more like a maximized + /// window. This is much faster to enter and exit than the native mode. + macos_non_native, + macos_non_native_visible_menu, +}; + +pub const SecureInput = enum(c_int) { + on, + off, + toggle, +}; + +/// The inspector mode to toggle to if we're toggling the inspector. +pub const Inspector = enum(c_int) { + toggle, + show, + hide, +}; + +pub const QuitTimer = enum(c_int) { + start, + stop, +}; + +/// The desktop notification to show. +pub const DesktopNotification = struct { + title: [:0]const u8, + body: [:0]const u8, +}; diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index c540694d0..ca53f137e 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -81,9 +81,10 @@ 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. + /// 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. new_tab: ?*const fn (SurfaceUD, apprt.Surface.Options) callconv(.C) void = null, /// New window with options. The surface may be null if there is no @@ -91,16 +92,16 @@ pub const App = struct { new_window: ?*const fn (SurfaceUD, apprt.Surface.Options) callconv(.C) void = null, /// Control the inspector visibility - control_inspector: ?*const fn (SurfaceUD, input.InspectorMode) callconv(.C) void = null, + control_inspector: ?*const fn (SurfaceUD, apprt.action.Inspector) callconv(.C) void = null, /// Close the current surface given by this function. 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, @@ -109,7 +110,7 @@ pub const App = struct { toggle_split_zoom: ?*const fn (SurfaceUD) callconv(.C) void = null, /// Goto tab - goto_tab: ?*const fn (SurfaceUD, apprt.GotoTab) callconv(.C) void = null, + goto_tab: ?*const fn (SurfaceUD, apprt.action.GotoTab) callconv(.C) void = null, /// Toggle fullscreen for current window. toggle_fullscreen: ?*const fn (SurfaceUD, configpkg.NonNativeFullscreen) callconv(.C) void = null, @@ -124,7 +125,8 @@ pub const App = struct { /// Called when the cell size changes. set_cell_size: ?*const fn (SurfaceUD, u32, u32) callconv(.C) void = null, - /// Show a desktop notification to the user. + /// Show a desktop notification to the user. The surface may be null + /// if the notification is global. show_desktop_notification: ?*const fn (SurfaceUD, [*:0]const u8, [*:0]const u8) void = null, /// Called when the health of the renderer changes. @@ -138,6 +140,8 @@ pub const App = struct { /// Notifies that a password input has been started for the given /// surface. The apprt can use this to modify UI, enable features /// such as macOS secure input, etc. + /// + /// The surface userdata will be null if a surface isn't focused. set_password_input: ?*const fn (SurfaceUD, bool) callconv(.C) void = null, /// Toggle secure input for the application. @@ -440,10 +444,6 @@ pub const App = struct { } } - pub fn openConfig(self: *App) !void { - try configpkg.edit.open(self.core_app.alloc); - } - pub fn reloadConfig(self: *App) !?*const Config { // Reload if (self.opts.reload_config(self.opts.userdata)) |new| { @@ -492,7 +492,7 @@ pub const App = struct { surface.queueInspectorRender(); } - pub fn newWindow(self: *App, parent: ?*CoreSurface) !void { + fn newWindow(self: *App, parent: ?*CoreSurface) !void { // If we have a parent, the surface logic handles it. if (parent) |surface| { try surface.rt_surface.newWindow(); @@ -507,6 +507,232 @@ pub const App = struct { func(null, .{}); } + + fn toggleFullscreen( + self: *App, + target: apprt.Target, + fullscreen: apprt.action.Fullscreen, + ) void { + const func = self.opts.toggle_fullscreen orelse { + log.info("runtime embedder does not toggle_fullscreen", .{}); + return; + }; + + switch (target) { + .app => {}, + .surface => |v| func( + v.rt_surface.userdata, + switch (fullscreen) { + .native => .false, + .macos_non_native => .true, + .macos_non_native_visible_menu => .@"visible-menu", + }, + ), + } + } + + fn newTab(self: *const App, target: apprt.Target) void { + const func = self.opts.new_tab orelse { + log.info("runtime embedder does not support new_tab", .{}); + return; + }; + + switch (target) { + .app => func(null, .{}), + .surface => |v| func( + v.rt_surface.userdata, + v.rt_surface.newSurfaceOptions(), + ), + } + } + + fn gotoTab(self: *App, target: apprt.Target, tab: apprt.action.GotoTab) void { + const func = self.opts.goto_tab orelse { + log.info("runtime embedder does not support goto_tab", .{}); + return; + }; + + switch (target) { + .app => {}, + .surface => |v| func(v.rt_surface.userdata, tab), + } + } + + 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; + }; + + 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 controlInspector( + self: *const App, + target: apprt.Target, + value: apprt.action.Inspector, + ) void { + const func = self.opts.control_inspector orelse { + log.info("runtime embedder does not support the terminal inspector", .{}); + return; + }; + + switch (target) { + .app => {}, + .surface => |v| func(v.rt_surface.userdata, value), + } + } + + fn showDesktopNotification( + self: *const App, + target: apprt.Target, + notification: apprt.action.DesktopNotification, + ) void { + const func = self.opts.show_desktop_notification orelse { + log.info("runtime embedder does not support show_desktop_notification", .{}); + return; + }; + + func(switch (target) { + .app => null, + .surface => |v| v.rt_surface.userdata, + }, notification.title, notification.body); + } + + 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. + pub fn performAction( + self: *App, + target: apprt.Target, + comptime action: apprt.Action.Key, + value: apprt.Action.Value(action), + ) !void { + switch (action) { + .new_window => _ = try self.newWindow(switch (target) { + .app => null, + .surface => |v| v, + }), + .toggle_fullscreen => self.toggleFullscreen(target, value), + + .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), + .inspector => self.controlInspector(target, value), + .desktop_notification => self.showDesktopNotification(target, value), + .secure_input => self.setPasswordInput(target, value), + + // Unimplemented + .present_terminal, + .close_all_windows, + .toggle_window_decorations, + .quit_timer, + => log.warn("unimplemented action={}", .{action}), + } + } }; /// Platform-specific configuration for libghostty. @@ -723,25 +949,6 @@ pub const Surface = struct { } } - pub fn controlInspector(self: *const Surface, mode: input.InspectorMode) void { - const func = self.app.opts.control_inspector orelse { - log.info("runtime embedder does not support the terminal inspector", .{}); - return; - }; - - 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", .{}); @@ -751,42 +958,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; } @@ -1065,53 +1236,7 @@ pub const Surface = struct { }; } - pub fn gotoTab(self: *Surface, tab: apprt.GotoTab) void { - const func = self.app.opts.goto_tab orelse { - log.info("runtime embedder does not goto_tab", .{}); - return; - }; - - func(self.userdata, tab); - } - - pub fn toggleFullscreen(self: *Surface, nonNativeFullscreen: configpkg.NonNativeFullscreen) void { - const func = self.app.opts.toggle_fullscreen orelse { - log.info("runtime embedder does not toggle_fullscreen", .{}); - return; - }; - - 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(); - } - - pub fn setPasswordInput(self: *Surface, v: bool) void { - const func = self.app.opts.set_password_input orelse { - log.info("runtime embedder does not set_password_input", .{}); - return; - }; - - func(self.userdata, v); - } - - pub fn newTab(self: *const Surface) !void { - const func = self.app.opts.new_tab orelse { - log.info("runtime embedder does not support new_tab", .{}); - return; - }; - - const options = self.newSurfaceOptions(); - func(self.userdata, options); - } - - pub fn newWindow(self: *const Surface) !void { + fn newWindow(self: *const Surface) !void { const func = self.app.opts.new_window orelse { log.info("runtime embedder does not support new_window", .{}); return; @@ -1166,20 +1291,6 @@ pub const Surface = struct { return .{ .x = pos.x * scale.x, .y = pos.y * scale.y }; } - /// Show a desktop notification. - pub fn showDesktopNotification( - self: *const Surface, - title: [:0]const u8, - body: [:0]const u8, - ) !void { - const func = self.app.opts.show_desktop_notification orelse { - log.info("runtime embedder does not support show_desktop_notification", .{}); - return; - }; - - func(self.userdata, title, body); - } - /// Update the health of the renderer. pub fn updateRendererHealth(self: *const Surface, health: renderer.Health) void { const func = self.app.opts.update_renderer_health orelse { @@ -1573,7 +1684,7 @@ pub const CAPI = struct { /// Open the configuration. export fn ghostty_app_open_config(v: *App) void { - _ = v.core_app.openConfig(v) catch |err| { + v.performAction(.app, .open_config, {}) catch |err| { log.err("error reloading config err={}", .{err}); return; }; @@ -1864,26 +1975,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 57cb257b4..b73aefced 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -127,9 +127,46 @@ pub const App = struct { glfw.postEmptyEvent(); } - /// Open the configuration in the system editor. - pub fn openConfig(self: *App) !void { - try configpkg.edit.open(self.app.alloc); + /// Perform a given action. + pub fn performAction( + self: *App, + target: apprt.Target, + comptime action: apprt.Action.Key, + value: apprt.Action.Value(action), + ) !void { + _ = value; + + switch (action) { + .new_window => _ = try self.newSurface(switch (target) { + .app => null, + .surface => |v| v, + }), + + .new_tab => try self.newTab(switch (target) { + .app => null, + .surface => |v| v, + }), + + .toggle_fullscreen => self.toggleFullscreen(target), + + .open_config => try configpkg.edit.open(self.app.alloc), + + // Unimplemented + .new_split, + .goto_split, + .resize_split, + .equalize_splits, + .toggle_split_zoom, + .present_terminal, + .close_all_windows, + .toggle_window_decorations, + .goto_tab, + .inspector, + .quit_timer, + .secure_input, + .desktop_notification, + => log.info("unimplemented action={}", .{action}), + } } /// Reload the configuration. This should return the new configuration. @@ -150,8 +187,12 @@ pub const App = struct { } /// Toggle the window to fullscreen mode. - pub fn toggleFullscreen(self: *App, surface: *Surface) void { + fn toggleFullscreen(self: *App, target: apprt.Target) void { _ = self; + const surface: *Surface = switch (target) { + .app => return, + .surface => |v| v.rt_surface, + }; const win = surface.window; if (surface.isFullscreen()) { @@ -195,18 +236,18 @@ pub const App = struct { win.setMonitor(monitor, 0, 0, video_mode.getWidth(), video_mode.getHeight(), 0); } - /// Create a new window for the app. - pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void { - _ = try self.newSurface(parent_); - } - /// Create a new tab in the parent surface. - fn newTab(self: *App, parent: *CoreSurface) !void { + fn newTab(self: *App, parent_: ?*CoreSurface) !void { if (!Darwin.enabled) { log.warn("tabbing is not supported on this platform", .{}); return; } + const parent = parent_ orelse { + _ = try self.newSurface(null); + return; + }; + // Create the new window const window = try self.newSurface(parent); @@ -370,7 +411,6 @@ pub const Surface = struct { /// Initialize the surface into the given self pointer. This gives a /// stable pointer to the destination that can be used for callbacks. pub fn init(self: *Surface, app: *App) !void { - // Create our window const win = glfw.Window.create( 640, @@ -525,20 +565,11 @@ pub const Surface = struct { } } - /// Create a new tab in the window containing this surface. - pub fn newTab(self: *Surface) !void { - try self.app.newTab(&self.core_surface); - } - /// Checks if the glfw window is in fullscreen. pub fn isFullscreen(self: *Surface) bool { return self.window.getMonitor() != null; } - pub fn toggleFullscreen(self: *Surface, _: Config.NonNativeFullscreen) void { - self.app.toggleFullscreen(self); - } - /// Close this surface. pub fn close(self: *Surface, processActive: bool) void { _ = processActive; @@ -683,6 +714,23 @@ pub const Surface = struct { self.window.setInputModeCursor(if (visible) .normal else .hidden); } + pub fn updateRendererHealth(self: *const Surface, health: renderer.Health) void { + // We don't support this in GLFW. + _ = self; + _ = health; + } + + pub fn supportsClipboard( + self: *const Surface, + clipboard_type: apprt.Clipboard, + ) bool { + _ = self; + return switch (clipboard_type) { + .standard => true, + .selection, .primary => comptime builtin.os.tag == .linux, + }; + } + /// Start an async clipboard request. pub fn clipboardRequest( self: *Surface, diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 73c25ec87..1d97731fa 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -27,6 +27,7 @@ const Surface = @import("Surface.zig"); const Window = @import("Window.zig"); const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig"); const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig"); +const Split = @import("Split.zig"); const c = @import("c.zig").c; const inspector = @import("inspector.zig"); const key = @import("key.zig"); @@ -341,9 +342,249 @@ pub fn terminate(self: *App) void { self.config.deinit(); } -/// Open the configuration in the system editor. -pub fn openConfig(self: *App) !void { - try configpkg.edit.open(self.core_app.alloc); +/// Perform a given action. +pub fn performAction( + self: *App, + target: apprt.Target, + comptime action: apprt.Action.Key, + value: apprt.Action.Value(action), +) !void { + switch (action) { + .new_window => _ = try self.newWindow(switch (target) { + .app => null, + .surface => |v| v, + }), + .toggle_fullscreen => self.toggleFullscreen(target, value), + + .new_tab => try self.newTab(target), + .goto_tab => self.gotoTab(target, value), + .new_split => try self.newSplit(target, value), + .resize_split => self.resizeSplit(target, value), + .equalize_splits => self.equalizeSplits(target), + .goto_split => self.gotoSplit(target, value), + .open_config => try configpkg.edit.open(self.core_app.alloc), + .inspector => self.controlInspector(target, value), + .desktop_notification => self.showDesktopNotification(target, value), + .present_terminal => self.presentTerminal(target), + .toggle_window_decorations => self.toggleWindowDecorations(target), + .quit_timer => self.quitTimer(value), + + // Unimplemented + .close_all_windows, + .toggle_split_zoom, + .secure_input, + => log.warn("unimplemented action={}", .{action}), + } +} + +fn newTab(_: *App, target: apprt.Target) !void { + switch (target) { + .app => {}, + .surface => |v| { + const window = v.rt_surface.container.window() orelse { + log.info( + "new_tab invalid for container={s}", + .{@tagName(v.rt_surface.container)}, + ); + return; + }; + + try window.newTab(v); + }, + } +} + +fn gotoTab(_: *App, target: apprt.Target, tab: apprt.action.GotoTab) void { + switch (target) { + .app => {}, + .surface => |v| { + const window = v.rt_surface.container.window() orelse { + log.info( + "gotoTab invalid for container={s}", + .{@tagName(v.rt_surface.container)}, + ); + return; + }; + + switch (tab) { + .previous => window.gotoPreviousTab(v.rt_surface), + .next => window.gotoNextTab(v.rt_surface), + .last => window.gotoLastTab(), + else => window.gotoTab(@intCast(@intFromEnum(tab))), + } + }, + } +} + +fn newSplit( + self: *App, + target: apprt.Target, + direction: apprt.action.SplitDirection, +) !void { + switch (target) { + .app => {}, + .surface => |v| { + const alloc = self.core_app.alloc; + _ = try Split.create(alloc, v.rt_surface, direction); + }, + } +} + +fn equalizeSplits(_: *App, target: apprt.Target) void { + switch (target) { + .app => {}, + .surface => |v| { + const tab = v.rt_surface.container.tab() orelse return; + const top_split = switch (tab.elem) { + .split => |s| s, + else => return, + }; + _ = top_split.equalize(); + }, + } +} + +fn gotoSplit( + _: *const App, + target: apprt.Target, + direction: apprt.action.GotoSplit, +) void { + switch (target) { + .app => {}, + .surface => |v| { + const s = v.rt_surface.container.split() orelse return; + const map = s.directionMap(switch (v.rt_surface.container) { + .split_tl => .top_left, + .split_br => .bottom_right, + .none, .tab_ => unreachable, + }); + const surface_ = map.get(direction) orelse return; + if (surface_) |surface| surface.grabFocus(); + }, + } +} + +fn resizeSplit( + _: *const App, + target: apprt.Target, + resize: apprt.action.ResizeSplit, +) void { + switch (target) { + .app => {}, + .surface => |v| { + const s = v.rt_surface.container.firstSplitWithOrientation( + Split.Orientation.fromResizeDirection(resize.direction), + ) orelse return; + s.moveDivider(resize.direction, resize.amount); + }, + } +} + +fn presentTerminal( + _: *const App, + target: apprt.Target, +) void { + switch (target) { + .app => {}, + .surface => |v| v.rt_surface.present(), + } +} + +fn controlInspector( + _: *const App, + target: apprt.Target, + mode: apprt.action.Inspector, +) void { + const surface: *Surface = switch (target) { + .app => return, + .surface => |v| v.rt_surface, + }; + + surface.controlInspector(mode); +} + +fn toggleFullscreen( + _: *App, + target: apprt.Target, + _: apprt.action.Fullscreen, +) void { + switch (target) { + .app => {}, + .surface => |v| { + const window = v.rt_surface.container.window() orelse { + log.info( + "toggleFullscreen invalid for container={s}", + .{@tagName(v.rt_surface.container)}, + ); + return; + }; + + window.toggleFullscreen(); + }, + } +} + +fn toggleWindowDecorations( + _: *App, + target: apprt.Target, +) void { + switch (target) { + .app => {}, + .surface => |v| { + const window = v.rt_surface.container.window() orelse { + log.info( + "toggleFullscreen invalid for container={s}", + .{@tagName(v.rt_surface.container)}, + ); + return; + }; + + window.toggleWindowDecorations(); + }, + } +} + +fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void { + switch (mode) { + .start => self.startQuitTimer(), + .stop => self.stopQuitTimer(), + } +} + +fn showDesktopNotification( + self: *App, + target: apprt.Target, + n: apprt.action.DesktopNotification, +) void { + // Set a default title if we don't already have one + const t = switch (n.title.len) { + 0 => "Ghostty", + else => n.title, + }; + + const notification = c.g_notification_new(t.ptr); + defer c.g_object_unref(notification); + c.g_notification_set_body(notification, n.body.ptr); + + const icon = c.g_themed_icon_new("com.mitchellh.ghostty"); + defer c.g_object_unref(icon); + c.g_notification_set_icon(notification, icon); + + const pointer = c.g_variant_new_uint64(switch (target) { + .app => 0, + .surface => |v| @intFromPtr(v), + }); + c.g_notification_set_default_action_and_target_value( + notification, + "app.present-surface", + pointer, + ); + + const g_app: *c.GApplication = @ptrCast(self.app); + + // We set the notification ID to the body content. If the content is the + // same, this notification may replace a previous notification + c.g_application_send_notification(g_app, n.body.ptr, notification); } /// Reload the configuration. This should return the new configuration. @@ -565,9 +806,9 @@ pub fn gtkQuitTimerExpired(ud: ?*anyopaque) callconv(.C) c.gboolean { } /// This will get called when there are no more open surfaces. -pub fn startQuitTimer(self: *App) void { +fn startQuitTimer(self: *App) void { // Cancel any previous timer. - self.cancelQuitTimer(); + self.stopQuitTimer(); // This is a no-op unless we are configured to quit after last window is closed. if (!self.config.@"quit-after-last-window-closed") return; @@ -582,7 +823,7 @@ pub fn startQuitTimer(self: *App) void { } /// This will get called when a new surface gets opened. -pub fn cancelQuitTimer(self: *App) void { +fn stopQuitTimer(self: *App) void { switch (self.quit_timer) { .off => {}, .expired => self.quit_timer = .{ .off = {} }, @@ -608,7 +849,7 @@ pub fn redrawInspector(self: *App, surface: *Surface) void { } /// Called by CoreApp to create a new window with a new surface. -pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void { +fn newWindow(self: *App, parent_: ?*CoreSurface) !void { const alloc = self.core_app.alloc; // Allocate a fixed pointer for our window. We try to minimize @@ -857,8 +1098,12 @@ fn gtkActionPresentSurface( return; } - // Convert that u64 to pointer to a core surface. - const surface: *CoreSurface = @ptrFromInt(c.g_variant_get_uint64(parameter)); + // Convert that u64 to pointer to a core surface. A value of zero + // means that there was no target surface for the notification so + // we dont' focus any surface. + const ptr_int: u64 = c.g_variant_get_uint64(parameter); + if (ptr_int == 0) return; + const surface: *CoreSurface = @ptrFromInt(ptr_int); // Send a message through the core app mailbox rather than presenting the // surface directly so that it can validate that the surface pointer is diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index 105646c7c..7a3645d1b 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -7,7 +7,6 @@ const Allocator = std.mem.Allocator; const assert = std.debug.assert; const apprt = @import("../../apprt.zig"); const font = @import("../../font/main.zig"); -const input = @import("../../input.zig"); const CoreSurface = @import("../../Surface.zig"); const Surface = @import("Surface.zig"); @@ -21,14 +20,14 @@ pub const Orientation = enum { horizontal, vertical, - pub fn fromDirection(direction: apprt.SplitDirection) Orientation { + pub fn fromDirection(direction: apprt.action.SplitDirection) Orientation { return switch (direction) { .right => .horizontal, .down => .vertical, }; } - pub fn fromResizeDirection(direction: input.SplitResizeDirection) Orientation { + pub fn fromResizeDirection(direction: apprt.action.ResizeSplit.Direction) Orientation { return switch (direction) { .up, .down => .vertical, .left, .right => .horizontal, @@ -58,7 +57,7 @@ bottom_right: Surface.Container.Elem, pub fn create( alloc: Allocator, sibling: *Surface, - direction: apprt.SplitDirection, + direction: apprt.action.SplitDirection, ) !*Split { var split = try alloc.create(Split); errdefer alloc.destroy(split); @@ -69,7 +68,7 @@ pub fn create( pub fn init( self: *Split, sibling: *Surface, - direction: apprt.SplitDirection, + direction: apprt.action.SplitDirection, ) !void { // Create the new child surface for the other direction. const alloc = sibling.app.core_app.alloc; @@ -164,7 +163,11 @@ fn removeChild( } /// Move the divider in the given direction by the given amount. -pub fn moveDivider(self: *Split, direction: input.SplitResizeDirection, amount: u16) void { +pub fn moveDivider( + self: *Split, + direction: apprt.action.ResizeSplit.Direction, + amount: u16, +) void { const min_pos = 10; const pos = c.gtk_paned_get_position(self.paned); @@ -263,7 +266,7 @@ fn updateChildren(self: *const Split) void { /// A mapping of direction to the element (if any) in that direction. pub const DirectionMap = std.EnumMap( - input.SplitFocusDirection, + apprt.action.GotoSplit, ?*Surface, ); diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index c1146d348..054eb675d 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -9,6 +9,7 @@ const configpkg = @import("../../config.zig"); const apprt = @import("../../apprt.zig"); const font = @import("../../font/main.zig"); const input = @import("../../input.zig"); +const renderer = @import("../../renderer.zig"); const terminal = @import("../../terminal/main.zig"); const CoreSurface = @import("../../Surface.zig"); const internal_os = @import("../../os/main.zig"); @@ -688,7 +689,10 @@ pub fn close(self: *Surface, processActive: bool) void { c.gtk_widget_show(alert); } -pub fn controlInspector(self: *Surface, mode: input.InspectorMode) void { +pub fn controlInspector( + self: *Surface, + mode: apprt.action.Inspector, +) void { const show = switch (mode) { .toggle => self.inspector == null, .show => true, @@ -715,30 +719,6 @@ pub fn controlInspector(self: *Surface, mode: input.InspectorMode) void { }; } -pub fn toggleFullscreen(self: *Surface, mac_non_native: configpkg.NonNativeFullscreen) void { - const window = self.container.window() orelse { - log.info( - "toggleFullscreen invalid for container={s}", - .{@tagName(self.container)}, - ); - return; - }; - - window.toggleFullscreen(mac_non_native); -} - -pub fn toggleWindowDecorations(self: *Surface) void { - const window = self.container.window() orelse { - log.info( - "toggleWindowDecorations invalid for container={s}", - .{@tagName(self.container)}, - ); - return; - }; - - window.toggleWindowDecorations(); -} - pub fn getTitleLabel(self: *Surface) ?*c.GtkWidget { switch (self.title) { .none => return null, @@ -749,69 +729,6 @@ pub fn getTitleLabel(self: *Surface) ?*c.GtkWidget { } } -pub fn newSplit(self: *Surface, direction: apprt.SplitDirection) !void { - const alloc = self.app.core_app.alloc; - _ = try Split.create(alloc, self, direction); -} - -pub fn gotoSplit(self: *const Surface, direction: input.SplitFocusDirection) void { - const s = self.container.split() orelse return; - const map = s.directionMap(switch (self.container) { - .split_tl => .top_left, - .split_br => .bottom_right, - .none, .tab_ => unreachable, - }); - const surface_ = map.get(direction) orelse return; - if (surface_) |surface| surface.grabFocus(); -} - -pub fn resizeSplit(self: *const Surface, direction: input.SplitResizeDirection, amount: u16) void { - const s = self.container.firstSplitWithOrientation( - Split.Orientation.fromResizeDirection(direction), - ) orelse return; - s.moveDivider(direction, amount); -} - -pub fn equalizeSplits(self: *const Surface) void { - const tab = self.container.tab() orelse return; - const top_split = switch (tab.elem) { - .split => |s| s, - else => return, - }; - _ = top_split.equalize(); -} - -pub fn newTab(self: *Surface) !void { - const window = self.container.window() orelse { - log.info("surface cannot create new tab when not attached to a window", .{}); - return; - }; - - try window.newTab(&self.core_surface); -} - -pub fn hasTabs(self: *const Surface) bool { - const window = self.container.window() orelse return false; - return window.hasTabs(); -} - -pub fn gotoTab(self: *Surface, tab: apprt.GotoTab) void { - const window = self.container.window() orelse { - log.info( - "gotoTab invalid for container={s}", - .{@tagName(self.container)}, - ); - return; - }; - - switch (tab) { - .previous => window.gotoPreviousTab(self), - .next => window.gotoNextTab(self), - .last => window.gotoLastTab(), - else => window.gotoTab(@intCast(@intFromEnum(tab))), - } -} - pub fn setShouldClose(self: *Surface) void { _ = self; } @@ -1026,6 +943,19 @@ pub fn mouseOverLink(self: *Surface, uri_: ?[]const u8) void { self.url_widget = URLWidget.init(self, uriZ); } +pub fn supportsClipboard( + self: *const Surface, + clipboard_type: apprt.Clipboard, +) bool { + _ = self; + return switch (clipboard_type) { + .standard, + .selection, + .primary, + => true, + }; +} + pub fn clipboardRequest( self: *Surface, clipboard_type: apprt.Clipboard, @@ -1980,7 +1910,7 @@ fn translateMods(state: c.GdkModifierType) input.Mods { return mods; } -pub fn presentSurface(self: *Surface) void { +pub fn present(self: *Surface) void { if (self.container.window()) |window| { if (self.container.tab()) |tab| { if (window.notebook.getTabPosition(tab)) |position| @@ -1988,5 +1918,12 @@ pub fn presentSurface(self: *Surface) void { } c.gtk_window_present(window.window); } + self.grabFocus(); } + +pub fn updateRendererHealth(self: *const Surface, health: renderer.Health) void { + // We don't support this in GTK. + _ = self; + _ = health; +} diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index efb0d2ea4..80bbd0944 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -421,11 +421,6 @@ pub fn closeTab(self: *Window, tab: *Tab) void { self.notebook.closeTab(tab); } -/// Returns true if this window has any tabs. -pub fn hasTabs(self: *const Window) bool { - return self.notebook.nPages() > 0; -} - /// Go to the previous tab for a surface. pub fn gotoPreviousTab(self: *Window, surface: *Surface) void { const tab = surface.container.tab() orelse { @@ -464,7 +459,7 @@ pub fn gotoTab(self: *Window, n: usize) void { } /// Toggle fullscreen for this window. -pub fn toggleFullscreen(self: *Window, _: configpkg.NonNativeFullscreen) void { +pub fn toggleFullscreen(self: *Window) void { const is_fullscreen = c.gtk_window_is_fullscreen(self.window); if (is_fullscreen == 0) { c.gtk_window_fullscreen(self.window); diff --git a/src/apprt/structs.zig b/src/apprt/structs.zig index 1e14b1b7c..f8bcd72cf 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -62,23 +62,6 @@ pub const DesktopNotification = struct { body: []const u8, }; -/// 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. -pub const GotoTab = enum(c_int) { - previous = -1, - next = -2, - last = -3, - _, -}; - -// 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..45ec24126 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, @@ -440,7 +438,7 @@ pub const Action = union(enum) { }; // Extern because it is used in the embedded runtime ABI. - pub const InspectorMode = enum(c_int) { + pub const InspectorMode = enum { toggle, show, hide,