core: handle app bindings in the App struct

This commit is contained in:
Mitchell Hashimoto
2024-09-23 15:05:36 -07:00
parent 070cc22172
commit 7f8c1a37ff
3 changed files with 197 additions and 13 deletions

View File

@ -262,6 +262,58 @@ pub fn setQuit(self: *App) !void {
self.quit = true; 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 /// Handle a window message
fn surfaceMessage(self: *App, surface: *Surface, msg: apprt.surface.Message) !void { fn surfaceMessage(self: *App, surface: *Surface, msg: apprt.surface.Message) !void {
// We want to ensure our window is still active. Window messages // We want to ensure our window is still active. Window messages

View File

@ -3400,14 +3400,22 @@ fn showMouse(self: *Surface) void {
/// will ever return false. We can expand this in the future if it becomes /// 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. /// useful. We did previous/next tab so we could implement #498.
pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool { pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool {
switch (action) { // Handle app-scoped bindings by sending it to the app.
.unbind => unreachable, switch (action.scope()) {
.ignore => {}, .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| { .csi, .esc => |data| {
// We need to send the CSI/ESC sequence as a single write request. // 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 // 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_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) { .crash => |location| switch (location) {
.main => @panic("crash binding action, crashing intentionally"), .main => @panic("crash binding action, crashing intentionally"),

View File

@ -539,6 +539,138 @@ pub const Action = union(enum) {
return Error.InvalidAction; 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 /// Implements the formatter for the fmt package. This encodes the
/// action back into the format used by parse. /// action back into the format used by parse.
pub fn format( pub fn format(