diff --git a/src/App.zig b/src/App.zig index f933b7126..4b9c2673e 100644 --- a/src/App.zig +++ b/src/App.zig @@ -262,6 +262,58 @@ pub fn setQuit(self: *App) !void { self.quit = true; } +/// Perform a binding action. This only accepts actions that are scoped +/// to the app. Callers can use performAllAction to perform any action +/// and any non-app-scoped actions will be performed on all surfaces. +pub fn performAction( + self: *App, + rt_app: *apprt.App, + action: input.Binding.Action.Scoped(.app), +) !void { + switch (action) { + .unbind => unreachable, + .ignore => {}, + .quit => try self.setQuit(), + .open_config => try self.openConfig(rt_app), + .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", .{}); + }, + } +} + +/// Perform an app-wide binding action. If the action is surface-specific +/// then it will be performed on all surfaces. To perform only app-scoped +/// actions, use performAction. +pub fn performAllAction( + self: *App, + rt_app: *apprt.App, + action: input.Binding.Action, +) !void { + switch (action.scope()) { + // App-scoped actions are handled by the app so that they aren't + // repeated for each surface (since each surface forwards + // app-scoped actions back up). + .app => try self.performAction( + rt_app, + action.scoped(.app).?, // asserted through the scope match + ), + + // Surface-scoped actions are performed on all surfaces. Errors + // are logged but processing continues. + .surface => for (self.surfaces.items) |surface| { + _ = surface.core_surface.performBindingAction(action) catch |err| { + log.warn("error performing binding action on surface ptr={X} err={}", .{ + @intFromPtr(surface), + err, + }); + }; + }, + } +} + /// Handle a window message fn surfaceMessage(self: *App, surface: *Surface, msg: apprt.surface.Message) !void { // We want to ensure our window is still active. Window messages diff --git a/src/Surface.zig b/src/Surface.zig index aa37b462b..9e25ef0ad 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3400,14 +3400,22 @@ fn showMouse(self: *Surface) void { /// will ever return false. We can expand this in the future if it becomes /// useful. We did previous/next tab so we could implement #498. pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool { - switch (action) { - .unbind => unreachable, - .ignore => {}, + // Handle app-scoped bindings by sending it to the app. + switch (action.scope()) { + .app => { + try self.app.performAction( + self.rt_app, + action.scoped(.app).?, + ); - .open_config => try self.app.openConfig(self.rt_app), + return true; + }, - .reload_config => try self.app.reloadConfig(self.rt_app), + // Surface fallthrough and handle + .surface => {}, + } + switch (action.scoped(.surface).?) { .csi, .esc => |data| { // We need to send the CSI/ESC sequence as a single write request. // If you split it across two then the shell can interpret it @@ -3757,14 +3765,6 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .close_window => try self.app.closeSurface(self), - .close_all_windows => { - if (@hasDecl(apprt.Surface, "closeAllWindows")) { - self.rt_surface.closeAllWindows(); - } else log.warn("runtime doesn't implement closeAllWindows", .{}); - }, - - .quit => try self.app.setQuit(), - .crash => |location| switch (location) { .main => @panic("crash binding action, crashing intentionally"), diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 97798463f..ba7b62af2 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -539,6 +539,138 @@ pub const Action = union(enum) { return Error.InvalidAction; } + /// The scope of an action. The scope is the context in which an action + /// must be executed. + pub const Scope = enum { + app, + surface, + }; + + /// Returns the scope of an action. + pub fn scope(self: Action) Scope { + return switch (self) { + // Doesn't really matter, so we'll see app. + .ignore, + .unbind, + => .app, + + // Obviously app actions. + .open_config, + .reload_config, + .close_all_windows, + .quit, + => .app, + + // Obviously surface actions. + .csi, + .esc, + .text, + .cursor_key, + .reset, + .copy_to_clipboard, + .paste_from_clipboard, + .paste_from_selection, + .increase_font_size, + .decrease_font_size, + .reset_font_size, + .clear_screen, + .select_all, + .scroll_to_top, + .scroll_to_bottom, + .scroll_page_up, + .scroll_page_down, + .scroll_page_fractional, + .scroll_page_lines, + .adjust_selection, + .jump_to_prompt, + .write_scrollback_file, + .write_screen_file, + .write_selection_file, + .close_surface, + .close_window, + .toggle_fullscreen, + .toggle_window_decorations, + .toggle_secure_input, + .crash, + + // These are less obvious surface actions. They're surface + // actions because they are relevant to the surface they + // come from. For example `new_window` needs to be sourced to + // a surface so inheritance can be done correctly. + .new_window, + .new_tab, + .previous_tab, + .next_tab, + .last_tab, + .goto_tab, + .new_split, + .goto_split, + .toggle_split_zoom, + .resize_split, + .equalize_splits, + .inspector, + => .surface, + }; + } + + /// Returns a union type that only contains actions that are scoped to + /// the given scope. + pub fn Scoped(comptime s: Scope) type { + const all_fields = @typeInfo(Action).Union.fields; + + // Find all fields that are app-scoped + var i: usize = 0; + var union_fields: [all_fields.len]std.builtin.Type.UnionField = undefined; + var enum_fields: [all_fields.len]std.builtin.Type.EnumField = undefined; + for (all_fields) |field| { + const action = @unionInit(Action, field.name, undefined); + if (action.scope() == s) { + union_fields[i] = field; + enum_fields[i] = .{ .name = field.name, .value = i }; + i += 1; + } + } + + // Build our union + return @Type(.{ .Union = .{ + .layout = .auto, + .tag_type = @Type(.{ .Enum = .{ + .tag_type = std.math.IntFittingRange(0, i), + .fields = enum_fields[0..i], + .decls = &.{}, + .is_exhaustive = true, + } }), + .fields = union_fields[0..i], + .decls = &.{}, + } }); + } + + /// Returns the scoped version of this action. If the action is not + /// scoped to the given scope then this returns null. + /// + /// The benefit of this function is that it allows us to use Zig's + /// exhaustive switch safety to ensure we always properly handle certain + /// scoped actions. + pub fn scoped(self: Action, comptime s: Scope) ?Scoped(s) { + switch (self) { + inline else => |v, tag| { + // Use comptime to prune out non-app actions + if (comptime @unionInit( + Action, + @tagName(tag), + undefined, + ).scope() != s) return null; + + // Initialize our app action + return @unionInit( + Scoped(s), + @tagName(tag), + v, + ); + }, + } + } + /// Implements the formatter for the fmt package. This encodes the /// action back into the format used by parse. pub fn format(