mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-04-25 19:08:39 +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;
|
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
|
||||||
|
@ -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"),
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
Reference in New Issue
Block a user