mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-04-20 00:18:53 +03:00
core: handle app bindings in the App struct
This commit is contained in:
52
src/App.zig
52
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
|
||||
|
@ -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"),
|
||||
|
||||
|
@ -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(
|
||||
|
Reference in New Issue
Block a user