ghostty/src/apprt/action.zig
2024-11-22 11:52:34 -08:00

540 lines
15 KiB
Zig

const std = @import("std");
const assert = std.debug.assert;
const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig");
const input = @import("../input.zig");
const renderer = @import("../renderer.zig");
const terminal = @import("../terminal/main.zig");
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(Key) {
app,
surface: *CoreSurface,
// Sync with: ghostty_target_tag_e
pub const Key = enum(c_int) {
app,
surface,
};
// Sync with: ghostty_target_u
pub const CValue = extern union {
app: void,
surface: *apprt.Surface,
};
// Sync with: ghostty_target_s
pub const C = extern struct {
key: Key,
value: CValue,
};
/// Convert to ghostty_target_s.
pub fn cval(self: Target) C {
return .{
.key = @as(Key, self),
.value = switch (self) {
.app => .{ .app = {} },
.surface => |v| .{ .surface = v.rt_surface },
},
};
}
};
/// 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(Key) {
// A GUIDE TO ADDING NEW ACTIONS:
//
// 1. Add the action to the `Key` enum. The order of the enum matters
// because it maps directly to the libghostty C enum. For ABI
// compatibility, new actions should be added to the end of the enum.
//
// 2. Add the action and optional value to the Action union.
//
// 3. If the value type is not void, ensure the value is C ABI
// compatible (extern). If it is not, add a `C` decl to the value
// and a `cval` function to convert to the C ABI compatible value.
//
// 4. Update `include/ghostty.h`: add the new key, value, and union
// entry. If the value type is void then only the key needs to be
// added. Ensure the order matches exactly with the Zig code.
/// 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 tab overview.
toggle_tab_overview,
/// Toggle whether window directions are shown.
toggle_window_decorations,
/// Toggle the quick terminal in or out.
toggle_quick_terminal,
/// Toggle the visibility of all Ghostty terminal windows.
toggle_visibility,
/// Moves a tab by a relative offset.
///
/// Adjusts the tab position based on `offset` (e.g., -1 for left, +1
/// for right). If the new position is out of bounds, it wraps around
/// cyclically within the tab range.
move_tab: MoveTab,
/// 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,
/// Sets a size limit (in pixels) for the target terminal.
size_limit: SizeLimit,
/// Specifies the initial size of the target terminal. This will be
/// sent only during the initialization of a surface. If it is received
/// after the surface is initialized it should be ignored.
initial_size: InitialSize,
/// The cell size has changed to the given dimensions in pixels.
cell_size: CellSize,
/// Control whether the inspector is shown or hidden.
inspector: Inspector,
/// The inspector for the given target has changes and should be
/// rendered at the next opportunity.
render_inspector,
/// Show a desktop notification.
desktop_notification: DesktopNotification,
/// Set the title of the target.
set_title: SetTitle,
/// The current working directory has changed for the target terminal.
pwd: Pwd,
/// Set the mouse cursor shape.
mouse_shape: terminal.MouseShape,
/// Set whether the mouse cursor is visible or not.
mouse_visibility: MouseVisibility,
/// Called when the mouse is over or recently left a link.
mouse_over_link: MouseOverLink,
/// The health of the renderer has changed.
renderer_health: renderer.Health,
/// 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,
/// A sequenced key binding has started, continued, or stopped.
/// The UI should show some indication that the user is in a sequenced
/// key mode because other input may be ignored.
key_sequence: KeySequence,
/// A terminal color was changed programmatically through things
/// such as OSC 10/11.
color_change: ColorChange,
/// A request to reload the configuration. The reload request can be
/// from a user or for some internal reason. The reload request may
/// request it is a soft reload or a full reload. See the struct for
/// more documentation.
///
/// The configuration should be passed to updateConfig either at the
/// app or surface level depending on the target.
reload_config: ReloadConfig,
/// The configuration has changed. The value is a pointer to the new
/// configuration. The pointer is only valid for the duration of the
/// action and should not be stored.
///
/// This should be used by apprts to update any internal state that
/// depends on configuration for the given target (i.e. headerbar colors).
/// The apprt should copy any data it needs since the memory lifetime
/// is only valid for the duration of the action.
///
/// This allows an apprt to have config-dependent state reactively
/// change without having to store the entire configuration or poll
/// for changes.
config_change: ConfigChange,
/// Sync with: ghostty_action_tag_e
pub const Key = enum(c_int) {
new_window,
new_tab,
new_split,
close_all_windows,
toggle_fullscreen,
toggle_tab_overview,
toggle_window_decorations,
toggle_quick_terminal,
toggle_visibility,
move_tab,
goto_tab,
goto_split,
resize_split,
equalize_splits,
toggle_split_zoom,
present_terminal,
size_limit,
initial_size,
cell_size,
inspector,
render_inspector,
desktop_notification,
set_title,
pwd,
mouse_shape,
mouse_visibility,
mouse_over_link,
renderer_health,
open_config,
quit_timer,
secure_input,
key_sequence,
color_change,
reload_config,
config_change,
};
/// Sync with: ghostty_action_u
pub const CValue = cvalue: {
const key_fields = @typeInfo(Key).Enum.fields;
var union_fields: [key_fields.len]std.builtin.Type.UnionField = undefined;
for (key_fields, 0..) |field, i| {
const action = @unionInit(Action, field.name, undefined);
const Type = t: {
const Type = @TypeOf(@field(action, field.name));
// Types can provide custom types for their CValue.
if (Type != void and @hasDecl(Type, "C")) break :t Type.C;
break :t Type;
};
union_fields[i] = .{
.name = field.name,
.type = Type,
.alignment = @alignOf(Type),
};
}
break :cvalue @Type(.{ .Union = .{
.layout = .@"extern",
.tag_type = Key,
.fields = &union_fields,
.decls = &.{},
} });
};
/// Sync with: ghostty_action_s
pub const C = extern struct {
key: Key,
value: CValue,
};
/// 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;
}
/// Convert to ghostty_action_s.
pub fn cval(self: Action) C {
const value: CValue = switch (self) {
inline else => |v, tag| @unionInit(
CValue,
@tagName(tag),
if (@TypeOf(v) != void and @hasDecl(@TypeOf(v), "cval")) v.cval() else v,
),
};
return .{
.key = @as(Key, self),
.value = value,
};
}
};
// 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,
left,
up,
};
// 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 = extern struct {
amount: u16,
direction: Direction,
pub const Direction = enum(c_int) {
up,
down,
left,
right,
};
};
pub const MoveTab = extern struct {
amount: isize,
};
/// 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,
};
pub const MouseVisibility = enum(c_int) {
visible,
hidden,
};
pub const MouseOverLink = struct {
url: []const u8,
// Sync with: ghostty_action_mouse_over_link_s
pub const C = extern struct {
url: [*]const u8,
len: usize,
};
pub fn cval(self: MouseOverLink) C {
return .{
.url = self.url.ptr,
.len = self.url.len,
};
}
};
pub const SizeLimit = extern struct {
min_width: u32,
min_height: u32,
max_width: u32,
max_height: u32,
};
pub const InitialSize = extern struct {
width: u32,
height: u32,
};
pub const CellSize = extern struct {
width: u32,
height: u32,
};
pub const SetTitle = struct {
title: [:0]const u8,
// Sync with: ghostty_action_set_title_s
pub const C = extern struct {
title: [*:0]const u8,
};
pub fn cval(self: SetTitle) C {
return .{
.title = self.title.ptr,
};
}
};
pub const Pwd = struct {
pwd: [:0]const u8,
// Sync with: ghostty_action_set_pwd_s
pub const C = extern struct {
pwd: [*:0]const u8,
};
pub fn cval(self: Pwd) C {
return .{
.pwd = self.pwd.ptr,
};
}
};
/// The desktop notification to show.
pub const DesktopNotification = struct {
title: [:0]const u8,
body: [:0]const u8,
// Sync with: ghostty_action_desktop_notification_s
pub const C = extern struct {
title: [*:0]const u8,
body: [*:0]const u8,
};
pub fn cval(self: DesktopNotification) C {
return .{
.title = self.title.ptr,
.body = self.body.ptr,
};
}
};
pub const KeySequence = union(enum) {
trigger: input.Trigger,
end,
// Sync with: ghostty_action_key_sequence_s
pub const C = extern struct {
active: bool,
trigger: input.Trigger.C,
};
pub fn cval(self: KeySequence) C {
return switch (self) {
.trigger => |t| .{ .active = true, .trigger = t.cval() },
.end => .{ .active = false, .trigger = .{} },
};
}
};
pub const ColorChange = extern struct {
kind: ColorKind,
r: u8,
g: u8,
b: u8,
};
pub const ColorKind = enum(c_int) {
// Negative numbers indicate some named kind
foreground = -1,
background = -2,
cursor = -3,
// 0+ values indicate a palette index
_,
};
pub const ReloadConfig = extern struct {
/// A soft reload means that the configuration doesn't need to be
/// read off disk, but libghostty needs the full config again so call
/// updateConfig with it.
soft: bool = false,
};
pub const ConfigChange = struct {
config: *const configpkg.Config,
// Sync with: ghostty_action_config_change_s
pub const C = extern struct {
config: *const configpkg.Config,
};
pub fn cval(self: ConfigChange) C {
return .{
.config = self.config,
};
}
};