mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-27 19:16:27 +00:00
Merge branch 'main' into fix_2271
This commit is contained in:
140
src/App.zig
140
src/App.zig
@@ -138,7 +138,13 @@ pub fn addSurface(self: *App, rt_surface: *apprt.Surface) !void {
|
||||
// Since we have non-zero surfaces, we can cancel the quit timer.
|
||||
// It is up to the apprt if there is a quit timer at all and if it
|
||||
// should be canceled.
|
||||
if (@hasDecl(apprt.App, "cancelQuitTimer")) rt_surface.app.cancelQuitTimer();
|
||||
rt_surface.app.performAction(
|
||||
.app,
|
||||
.quit_timer,
|
||||
.stop,
|
||||
) catch |err| {
|
||||
log.warn("error stopping quit timer err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// Delete the surface from the known surface list. This will NOT call the
|
||||
@@ -166,8 +172,13 @@ pub fn deleteSurface(self: *App, rt_surface: *apprt.Surface) void {
|
||||
|
||||
// If we have no surfaces, we can start the quit timer. It is up to the
|
||||
// apprt to determine if this is necessary.
|
||||
if (@hasDecl(apprt.App, "startQuitTimer") and
|
||||
self.surfaces.items.len == 0) rt_surface.app.startQuitTimer();
|
||||
if (self.surfaces.items.len == 0) rt_surface.app.performAction(
|
||||
.app,
|
||||
.quit_timer,
|
||||
.start,
|
||||
) catch |err| {
|
||||
log.warn("error starting quit timer err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// The last focused surface. This is only valid while on the main thread
|
||||
@@ -194,7 +205,7 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
||||
log.debug("mailbox message={s}", .{@tagName(message)});
|
||||
switch (message) {
|
||||
.reload_config => try self.reloadConfig(rt_app),
|
||||
.open_config => try self.openConfig(rt_app),
|
||||
.open_config => try self.performAction(rt_app, .open_config),
|
||||
.new_window => |msg| try self.newWindow(rt_app, msg),
|
||||
.close => |surface| try self.closeSurface(surface),
|
||||
.quit => try self.setQuit(),
|
||||
@@ -205,12 +216,6 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn openConfig(self: *App, rt_app: *apprt.App) !void {
|
||||
_ = self;
|
||||
log.debug("opening configuration", .{});
|
||||
try rt_app.openConfig();
|
||||
}
|
||||
|
||||
pub fn reloadConfig(self: *App, rt_app: *apprt.App) !void {
|
||||
log.debug("reloading configuration", .{});
|
||||
if (try rt_app.reloadConfig()) |new| {
|
||||
@@ -241,19 +246,17 @@ fn redrawInspector(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) !voi
|
||||
|
||||
/// Create a new window
|
||||
pub fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void {
|
||||
if (!@hasDecl(apprt.App, "newWindow")) {
|
||||
log.warn("newWindow is not supported by this runtime", .{});
|
||||
return;
|
||||
}
|
||||
const target: apprt.Target = target: {
|
||||
const parent = msg.parent orelse break :target .app;
|
||||
if (self.hasSurface(parent)) break :target .{ .surface = parent };
|
||||
break :target .app;
|
||||
};
|
||||
|
||||
const parent = if (msg.parent) |parent| parent: {
|
||||
break :parent if (self.hasSurface(parent))
|
||||
parent
|
||||
else
|
||||
null;
|
||||
} else null;
|
||||
|
||||
try rt_app.newWindow(parent);
|
||||
try rt_app.performAction(
|
||||
target,
|
||||
.new_window,
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
/// Start quitting
|
||||
@@ -262,6 +265,99 @@ pub fn setQuit(self: *App) !void {
|
||||
self.quit = true;
|
||||
}
|
||||
|
||||
/// Handle a key event at the app-scope. If this key event is used,
|
||||
/// this will return true and the caller shouldn't continue processing
|
||||
/// the event. If the event is not used, this will return false.
|
||||
pub fn keyEvent(
|
||||
self: *App,
|
||||
rt_app: *apprt.App,
|
||||
event: input.KeyEvent,
|
||||
) bool {
|
||||
switch (event.action) {
|
||||
// We don't care about key release events.
|
||||
.release => return false,
|
||||
|
||||
// Continue processing key press events.
|
||||
.press, .repeat => {},
|
||||
}
|
||||
|
||||
// Get the keybind entry for this event. We don't support key sequences
|
||||
// so we can look directly in the top-level set.
|
||||
const entry = rt_app.config.keybind.set.getEvent(event) orelse return false;
|
||||
const leaf: input.Binding.Set.Leaf = switch (entry) {
|
||||
// Sequences aren't supported. Our configuration parser verifies
|
||||
// this for global keybinds but we may still get an entry for
|
||||
// a non-global keybind.
|
||||
.leader => return false,
|
||||
|
||||
// Leaf entries are good
|
||||
.leaf => |leaf| leaf,
|
||||
};
|
||||
|
||||
// We only care about global keybinds
|
||||
if (!leaf.flags.global) return false;
|
||||
|
||||
// Perform the action
|
||||
self.performAllAction(rt_app, leaf.action) catch |err| {
|
||||
log.warn("error performing global keybind action action={s} err={}", .{
|
||||
@tagName(leaf.action),
|
||||
err,
|
||||
});
|
||||
};
|
||||
|
||||
return 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(),
|
||||
.new_window => try self.newWindow(rt_app, .{ .parent = null }),
|
||||
.open_config => try rt_app.performAction(.app, .open_config, {}),
|
||||
.reload_config => try self.reloadConfig(rt_app),
|
||||
.close_all_windows => try rt_app.performAction(.app, .close_all_windows, {}),
|
||||
.toggle_quick_terminal => try rt_app.performAction(.app, .toggle_quick_terminal, {}),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
494
src/Surface.zig
494
src/Surface.zig
@@ -515,14 +515,25 @@ pub fn init(
|
||||
errdefer self.io.deinit();
|
||||
|
||||
// Report initial cell size on surface creation
|
||||
try rt_surface.setCellSize(cell_size.width, cell_size.height);
|
||||
try rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.cell_size,
|
||||
.{ .width = cell_size.width, .height = cell_size.height },
|
||||
);
|
||||
|
||||
// Set a minimum size that is cols=10 h=4. This matches Mac's Terminal.app
|
||||
// but is otherwise somewhat arbitrary.
|
||||
try rt_surface.setSizeLimits(.{
|
||||
.width = cell_size.width * 10,
|
||||
.height = cell_size.height * 4,
|
||||
}, null);
|
||||
try rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.size_limit,
|
||||
.{
|
||||
.min_width = cell_size.width * 10,
|
||||
.min_height = cell_size.height * 4,
|
||||
// No max:
|
||||
.max_width = 0,
|
||||
.max_height = 0,
|
||||
},
|
||||
);
|
||||
|
||||
// Call our size callback which handles all our retina setup
|
||||
// Note: this shouldn't be necessary and when we clean up the surface
|
||||
@@ -576,13 +587,23 @@ pub fn init(
|
||||
padding.top +
|
||||
padding.bottom;
|
||||
|
||||
rt_surface.setInitialWindowSize(final_width, final_height) catch |err| {
|
||||
rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.initial_size,
|
||||
.{ .width = final_width, .height = final_height },
|
||||
) catch |err| {
|
||||
// We don't treat this as a fatal error because not setting
|
||||
// an initial size shouldn't stop our terminal from working.
|
||||
log.warn("unable to set initial window size: {s}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
if (config.title) |title| {
|
||||
try rt_surface.setTitle(title);
|
||||
try rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.set_title,
|
||||
.{ .title = title },
|
||||
);
|
||||
} else if ((comptime builtin.os.tag == .linux) and
|
||||
config.@"_xdg-terminal-exec")
|
||||
xdg: {
|
||||
@@ -599,7 +620,11 @@ pub fn init(
|
||||
break :xdg;
|
||||
};
|
||||
defer alloc.free(title);
|
||||
try rt_surface.setTitle(title);
|
||||
try rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.set_title,
|
||||
.{ .title = title },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -743,15 +768,15 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
// We know that our title should end in 0.
|
||||
const slice = std.mem.sliceTo(@as([*:0]const u8, @ptrCast(v)), 0);
|
||||
log.debug("changing title \"{s}\"", .{slice});
|
||||
try self.rt_surface.setTitle(slice);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.set_title,
|
||||
.{ .title = slice },
|
||||
);
|
||||
},
|
||||
|
||||
.report_title => |style| {
|
||||
const title: ?[:0]const u8 = title: {
|
||||
if (!@hasDecl(apprt.runtime.Surface, "getTitle")) break :title null;
|
||||
break :title self.rt_surface.getTitle();
|
||||
};
|
||||
|
||||
const title: ?[:0]const u8 = self.rt_surface.getTitle();
|
||||
const data = switch (style) {
|
||||
.csi_21_t => try std.fmt.allocPrint(
|
||||
self.alloc,
|
||||
@@ -773,7 +798,11 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
|
||||
.set_mouse_shape => |shape| {
|
||||
log.debug("changing mouse shape: {}", .{shape});
|
||||
try self.rt_surface.setMouseShape(shape);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_shape,
|
||||
shape,
|
||||
);
|
||||
},
|
||||
|
||||
.clipboard_read => |clipboard| {
|
||||
@@ -837,6 +866,18 @@ fn passwordInput(self: *Surface, v: bool) !void {
|
||||
self.io.terminal.flags.password_input = v;
|
||||
}
|
||||
|
||||
// Notify our apprt so it can do whatever it wants.
|
||||
self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.secure_input,
|
||||
if (v) .on else .off,
|
||||
) catch |err| {
|
||||
// We ignore this error because we don't want to fail this
|
||||
// entire operation just because the apprt failed to set
|
||||
// the secure input state.
|
||||
log.warn("apprt failed to set secure input state err={}", .{err});
|
||||
};
|
||||
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
@@ -889,8 +930,13 @@ fn modsChanged(self: *Surface, mods: input.Mods) void {
|
||||
/// Called when our renderer health state changes.
|
||||
fn updateRendererHealth(self: *Surface, health: renderer.Health) void {
|
||||
log.warn("renderer health status change status={}", .{health});
|
||||
if (!@hasDecl(apprt.runtime.Surface, "updateRendererHealth")) return;
|
||||
self.rt_surface.updateRendererHealth(health);
|
||||
self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.renderer_health,
|
||||
health,
|
||||
) catch |err| {
|
||||
log.warn("failed to notify app of renderer health change err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// Update our configuration at runtime.
|
||||
@@ -1146,10 +1192,8 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) !void {
|
||||
|
||||
// Check if our runtime supports the selection clipboard at all.
|
||||
// We can save a lot of work if it doesn't.
|
||||
if (@hasDecl(apprt.runtime.Surface, "supportsClipboard")) {
|
||||
if (!self.rt_surface.supportsClipboard(clipboard)) {
|
||||
return;
|
||||
}
|
||||
if (!self.rt_surface.supportsClipboard(clipboard)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buf = self.io.terminal.screen.selectionString(self.alloc, .{
|
||||
@@ -1189,7 +1233,11 @@ fn setCellSize(self: *Surface, size: renderer.CellSize) !void {
|
||||
}, .unlocked);
|
||||
|
||||
// Notify the window
|
||||
try self.rt_surface.setCellSize(size.width, size.height);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.cell_size,
|
||||
.{ .width = size.width, .height = size.height },
|
||||
);
|
||||
}
|
||||
|
||||
/// Change the font size.
|
||||
@@ -1454,7 +1502,7 @@ pub fn keyCallback(
|
||||
// mod changes can affect link highlighting.
|
||||
self.mouse.link_point = null;
|
||||
const pos = self.rt_surface.getCursorPos() catch break :mouse_mods;
|
||||
self.cursorPosCallback(pos) catch {};
|
||||
self.cursorPosCallback(pos, null) catch {};
|
||||
if (rehide) self.mouse.hidden = true;
|
||||
}
|
||||
|
||||
@@ -1467,8 +1515,11 @@ pub fn keyCallback(
|
||||
.mods = self.mouse.mods,
|
||||
.over_link = self.mouse.over_link,
|
||||
.hidden = self.mouse.hidden,
|
||||
}).keyToMouseShape()) |shape|
|
||||
try self.rt_surface.setMouseShape(shape);
|
||||
}).keyToMouseShape()) |shape| try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_shape,
|
||||
shape,
|
||||
);
|
||||
|
||||
// We've processed a key event that produced some data so we want to
|
||||
// track the last pressed key.
|
||||
@@ -1556,19 +1607,8 @@ fn maybeHandleBinding(
|
||||
const entry: input.Binding.Set.Entry = entry: {
|
||||
const set = self.keyboard.bindings orelse &self.config.keybind.set;
|
||||
|
||||
var trigger: input.Binding.Trigger = .{
|
||||
.mods = event.mods.binding(),
|
||||
.key = .{ .translated = event.key },
|
||||
};
|
||||
if (set.get(trigger)) |v| break :entry v;
|
||||
|
||||
trigger.key = .{ .physical = event.physical_key };
|
||||
if (set.get(trigger)) |v| break :entry v;
|
||||
|
||||
if (event.unshifted_codepoint > 0) {
|
||||
trigger.key = .{ .unicode = event.unshifted_codepoint };
|
||||
if (set.get(trigger)) |v| break :entry v;
|
||||
}
|
||||
// Get our entry from the set for the given event.
|
||||
if (set.getEvent(event)) |v| break :entry v;
|
||||
|
||||
// No entry found. If we're not looking at the root set of the
|
||||
// bindings we need to encode everything up to this point and
|
||||
@@ -1585,7 +1625,7 @@ fn maybeHandleBinding(
|
||||
};
|
||||
|
||||
// Determine if this entry has an action or if its a leader key.
|
||||
const action: input.Binding.Action, const consumed: bool = switch (entry) {
|
||||
const leaf: input.Binding.Set.Leaf = switch (entry) {
|
||||
.leader => |set| {
|
||||
// Setup the next set we'll look at.
|
||||
self.keyboard.bindings = set;
|
||||
@@ -1600,8 +1640,20 @@ fn maybeHandleBinding(
|
||||
return .consumed;
|
||||
},
|
||||
|
||||
.action => |v| .{ v, true },
|
||||
.action_unconsumed => |v| .{ v, false },
|
||||
.leaf => |leaf| leaf,
|
||||
};
|
||||
const action = leaf.action;
|
||||
|
||||
// consumed determines if the input is consumed or if we continue
|
||||
// encoding the key (if we have a key to encode).
|
||||
const consumed = consumed: {
|
||||
// If the consumed flag is explicitly set, then we are consumed.
|
||||
if (leaf.flags.consumed) break :consumed true;
|
||||
|
||||
// If the global or all flag is set, we always consume.
|
||||
if (leaf.flags.global or leaf.flags.all) break :consumed true;
|
||||
|
||||
break :consumed false;
|
||||
};
|
||||
|
||||
// We have an action, so at this point we're handling SOMETHING so
|
||||
@@ -1613,8 +1665,22 @@ fn maybeHandleBinding(
|
||||
self.keyboard.bindings = null;
|
||||
|
||||
// Attempt to perform the action
|
||||
log.debug("key event binding consumed={} action={}", .{ consumed, action });
|
||||
const performed = try self.performBindingAction(action);
|
||||
log.debug("key event binding flags={} action={}", .{
|
||||
leaf.flags,
|
||||
action,
|
||||
});
|
||||
const performed = performed: {
|
||||
// If this is a global or all action, then we perform it on
|
||||
// the app and it applies to every surface.
|
||||
if (leaf.flags.global or leaf.flags.all) {
|
||||
try self.app.performAllAction(self.rt_app, action);
|
||||
|
||||
// "All" actions are always performed since they are global.
|
||||
break :performed true;
|
||||
}
|
||||
|
||||
break :performed try self.performBindingAction(action);
|
||||
};
|
||||
|
||||
// If we performed an action and it was a closing action,
|
||||
// our "self" pointer is not safe to use anymore so we need to
|
||||
@@ -2410,15 +2476,33 @@ pub fn mouseButtonCallback(
|
||||
if (mods.shift and
|
||||
self.mouse.left_click_count > 0 and
|
||||
!shift_capture)
|
||||
{
|
||||
extend_selection: {
|
||||
// We split this conditional out on its own because this is the
|
||||
// only one that requires a renderer mutex grab which is VERY
|
||||
// expensive because it could block all our threads.
|
||||
if (self.hasSelection()) {
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
try self.cursorPosCallback(pos);
|
||||
return true;
|
||||
if (!self.hasSelection()) break :extend_selection;
|
||||
|
||||
// If we are within the interval that the click would register
|
||||
// an increment then we do not extend the selection.
|
||||
if (std.time.Instant.now()) |now| {
|
||||
const since = now.since(self.mouse.left_click_time);
|
||||
if (since <= self.config.mouse_interval) {
|
||||
// Click interval very short, we may be increasing
|
||||
// click counts so we don't extend the selection.
|
||||
break :extend_selection;
|
||||
}
|
||||
} else |err| {
|
||||
// This is a weird behavior, I think either behavior is actually
|
||||
// fine. This failure should be exceptionally rare anyways.
|
||||
// My thinking here is that we can't be sure if we should extend
|
||||
// the selection or not so we just don't.
|
||||
log.warn("failed to get time, not extending selection err={}", .{err});
|
||||
break :extend_selection;
|
||||
}
|
||||
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
try self.cursorPosCallback(pos, null);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2882,9 +2966,18 @@ pub fn mousePressureCallback(
|
||||
}
|
||||
}
|
||||
|
||||
/// Cursor position callback.
|
||||
///
|
||||
/// The mods parameter is optional because some apprts do not provide
|
||||
/// modifier information on cursor position events. If mods is null then
|
||||
/// we'll use the last known mods. This is usually accurate since mod events
|
||||
/// will trigger key press events but on some platforms we don't get them.
|
||||
/// For example, on macOS, unfocused surfaces don't receive key events but
|
||||
/// do receive mouse events so we have to rely on updated mods.
|
||||
pub fn cursorPosCallback(
|
||||
self: *Surface,
|
||||
pos: apprt.CursorPos,
|
||||
mods: ?input.Mods,
|
||||
) !void {
|
||||
// Crash metadata in case we crash in here
|
||||
crash.sentry.thread_state = self.crashThreadState();
|
||||
@@ -2893,6 +2986,9 @@ pub fn cursorPosCallback(
|
||||
// Always show the mouse again if it is hidden
|
||||
if (self.mouse.hidden) self.showMouse();
|
||||
|
||||
// Update our modifiers if they changed
|
||||
if (mods) |v| self.modsChanged(v);
|
||||
|
||||
// The mouse position in the viewport
|
||||
const pos_vp = self.posToViewport(pos.x, pos.y);
|
||||
|
||||
@@ -2943,7 +3039,11 @@ pub fn cursorPosCallback(
|
||||
// We also queue a render so the renderer can undo the rendered link
|
||||
// state.
|
||||
if (over_link) {
|
||||
self.rt_surface.mouseOverLink(null);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_over_link,
|
||||
.{ .url = "" },
|
||||
);
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
@@ -3029,7 +3129,11 @@ pub fn cursorPosCallback(
|
||||
self.renderer_state.mouse.point = pos_vp;
|
||||
self.mouse.over_link = true;
|
||||
self.renderer_state.terminal.screen.dirty.hyperlink_hover = true;
|
||||
try self.rt_surface.setMouseShape(.pointer);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_shape,
|
||||
.pointer,
|
||||
);
|
||||
|
||||
switch (link[0]) {
|
||||
.open => {
|
||||
@@ -3038,7 +3142,11 @@ pub fn cursorPosCallback(
|
||||
.trim = false,
|
||||
});
|
||||
defer self.alloc.free(str);
|
||||
self.rt_surface.mouseOverLink(str);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_over_link,
|
||||
.{ .url = str },
|
||||
);
|
||||
},
|
||||
|
||||
._open_osc8 => link: {
|
||||
@@ -3048,14 +3156,26 @@ pub fn cursorPosCallback(
|
||||
log.warn("failed to get URI for OSC8 hyperlink", .{});
|
||||
break :link;
|
||||
};
|
||||
self.rt_surface.mouseOverLink(uri);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_over_link,
|
||||
.{ .url = uri },
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
try self.queueRender();
|
||||
} else if (over_link) {
|
||||
try self.rt_surface.setMouseShape(self.io.terminal.mouse_shape);
|
||||
self.rt_surface.mouseOverLink(null);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_shape,
|
||||
self.io.terminal.mouse_shape,
|
||||
);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_over_link,
|
||||
.{ .url = "" },
|
||||
);
|
||||
try self.queueRender();
|
||||
}
|
||||
}
|
||||
@@ -3364,13 +3484,25 @@ fn scrollToBottom(self: *Surface) !void {
|
||||
fn hideMouse(self: *Surface) void {
|
||||
if (self.mouse.hidden) return;
|
||||
self.mouse.hidden = true;
|
||||
self.rt_surface.setMouseVisibility(false);
|
||||
self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_visibility,
|
||||
.hidden,
|
||||
) catch |err| {
|
||||
log.warn("apprt failed to set mouse visibility err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn showMouse(self: *Surface) void {
|
||||
if (!self.mouse.hidden) return;
|
||||
self.mouse.hidden = false;
|
||||
self.rt_surface.setMouseVisibility(true);
|
||||
self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.mouse_visibility,
|
||||
.visible,
|
||||
) catch |err| {
|
||||
log.warn("apprt failed to set mouse visibility err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// Perform a binding action. A binding is a keybinding. This function
|
||||
@@ -3384,14 +3516,25 @@ 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 => {},
|
||||
// Forward app-scoped actions to the app. Some app-scoped actions are
|
||||
// special-cased here because they do some special things when performed
|
||||
// from the surface.
|
||||
if (action.scoped(.app)) |app_action| {
|
||||
switch (app_action) {
|
||||
.new_window => try self.app.newWindow(
|
||||
self.rt_app,
|
||||
.{ .parent = self },
|
||||
),
|
||||
|
||||
.open_config => try self.app.openConfig(self.rt_app),
|
||||
|
||||
.reload_config => try self.app.reloadConfig(self.rt_app),
|
||||
else => try self.app.performAction(
|
||||
self.rt_app,
|
||||
action.scoped(.app).?,
|
||||
),
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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
|
||||
@@ -3613,109 +3756,105 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
v,
|
||||
),
|
||||
|
||||
.new_window => try self.app.newWindow(self.rt_app, .{ .parent = self }),
|
||||
.new_tab => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.new_tab,
|
||||
{},
|
||||
),
|
||||
|
||||
.new_tab => {
|
||||
if (@hasDecl(apprt.Surface, "newTab")) {
|
||||
try self.rt_surface.newTab();
|
||||
} else log.warn("runtime doesn't implement newTab", .{});
|
||||
},
|
||||
inline .previous_tab,
|
||||
.next_tab,
|
||||
.last_tab,
|
||||
.goto_tab,
|
||||
=> |v, tag| try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.goto_tab,
|
||||
switch (tag) {
|
||||
.previous_tab => .previous,
|
||||
.next_tab => .next,
|
||||
.last_tab => .last,
|
||||
.goto_tab => @enumFromInt(v),
|
||||
else => comptime unreachable,
|
||||
},
|
||||
),
|
||||
|
||||
.previous_tab => {
|
||||
if (@hasDecl(apprt.Surface, "hasTabs")) {
|
||||
if (!self.rt_surface.hasTabs()) {
|
||||
log.debug("surface has no tabs, ignoring previous_tab binding", .{});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
.new_split => |direction| try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.new_split,
|
||||
switch (direction) {
|
||||
.right => .right,
|
||||
.down => .down,
|
||||
.auto => if (self.screen_size.width > self.screen_size.height)
|
||||
.right
|
||||
else
|
||||
.down,
|
||||
},
|
||||
),
|
||||
|
||||
if (@hasDecl(apprt.Surface, "gotoTab")) {
|
||||
self.rt_surface.gotoTab(.previous);
|
||||
} else log.warn("runtime doesn't implement gotoTab", .{});
|
||||
},
|
||||
.goto_split => |direction| try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.goto_split,
|
||||
switch (direction) {
|
||||
inline else => |tag| @field(
|
||||
apprt.action.GotoSplit,
|
||||
@tagName(tag),
|
||||
),
|
||||
},
|
||||
),
|
||||
|
||||
.next_tab => {
|
||||
if (@hasDecl(apprt.Surface, "hasTabs")) {
|
||||
if (!self.rt_surface.hasTabs()) {
|
||||
log.debug("surface has no tabs, ignoring next_tab binding", .{});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
.resize_split => |value| try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.resize_split,
|
||||
.{
|
||||
.amount = value[1],
|
||||
.direction = switch (value[0]) {
|
||||
inline else => |tag| @field(
|
||||
apprt.action.ResizeSplit.Direction,
|
||||
@tagName(tag),
|
||||
),
|
||||
},
|
||||
},
|
||||
),
|
||||
|
||||
if (@hasDecl(apprt.Surface, "gotoTab")) {
|
||||
self.rt_surface.gotoTab(.next);
|
||||
} else log.warn("runtime doesn't implement gotoTab", .{});
|
||||
},
|
||||
.equalize_splits => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.equalize_splits,
|
||||
{},
|
||||
),
|
||||
|
||||
.last_tab => {
|
||||
if (@hasDecl(apprt.Surface, "hasTabs")) {
|
||||
if (!self.rt_surface.hasTabs()) {
|
||||
log.debug("surface has no tabs, ignoring last_tab binding", .{});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
.toggle_split_zoom => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.toggle_split_zoom,
|
||||
{},
|
||||
),
|
||||
|
||||
if (@hasDecl(apprt.Surface, "gotoTab")) {
|
||||
self.rt_surface.gotoTab(.last);
|
||||
} else log.warn("runtime doesn't implement gotoTab", .{});
|
||||
},
|
||||
.toggle_fullscreen => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.toggle_fullscreen,
|
||||
switch (self.config.macos_non_native_fullscreen) {
|
||||
.false => .native,
|
||||
.true => .macos_non_native,
|
||||
.@"visible-menu" => .macos_non_native_visible_menu,
|
||||
},
|
||||
),
|
||||
|
||||
.goto_tab => |n| {
|
||||
if (@hasDecl(apprt.Surface, "gotoTab")) {
|
||||
self.rt_surface.gotoTab(@enumFromInt(n));
|
||||
} else log.warn("runtime doesn't implement gotoTab", .{});
|
||||
},
|
||||
.toggle_window_decorations => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.toggle_window_decorations,
|
||||
{},
|
||||
),
|
||||
|
||||
.new_split => |direction| {
|
||||
if (@hasDecl(apprt.Surface, "newSplit")) {
|
||||
try self.rt_surface.newSplit(switch (direction) {
|
||||
.right => .right,
|
||||
.down => .down,
|
||||
.auto => if (self.screen_size.width > self.screen_size.height)
|
||||
.right
|
||||
else
|
||||
.down,
|
||||
});
|
||||
} else log.warn("runtime doesn't implement newSplit", .{});
|
||||
},
|
||||
.toggle_tab_overview => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.toggle_tab_overview,
|
||||
{},
|
||||
),
|
||||
|
||||
.goto_split => |direction| {
|
||||
if (@hasDecl(apprt.Surface, "gotoSplit")) {
|
||||
self.rt_surface.gotoSplit(direction);
|
||||
} else log.warn("runtime doesn't implement gotoSplit", .{});
|
||||
},
|
||||
|
||||
.resize_split => |param| {
|
||||
if (@hasDecl(apprt.Surface, "resizeSplit")) {
|
||||
const direction = param[0];
|
||||
const amount = param[1];
|
||||
self.rt_surface.resizeSplit(direction, amount);
|
||||
} else log.warn("runtime doesn't implement resizeSplit", .{});
|
||||
},
|
||||
|
||||
.equalize_splits => {
|
||||
if (@hasDecl(apprt.Surface, "equalizeSplits")) {
|
||||
self.rt_surface.equalizeSplits();
|
||||
} else log.warn("runtime doesn't implement equalizeSplits", .{});
|
||||
},
|
||||
|
||||
.toggle_split_zoom => {
|
||||
if (@hasDecl(apprt.Surface, "toggleSplitZoom")) {
|
||||
self.rt_surface.toggleSplitZoom();
|
||||
} else log.warn("runtime doesn't implement toggleSplitZoom", .{});
|
||||
},
|
||||
|
||||
.toggle_fullscreen => {
|
||||
if (@hasDecl(apprt.Surface, "toggleFullscreen")) {
|
||||
self.rt_surface.toggleFullscreen(self.config.macos_non_native_fullscreen);
|
||||
} else log.warn("runtime doesn't implement toggleFullscreen", .{});
|
||||
},
|
||||
|
||||
.toggle_window_decorations => {
|
||||
if (@hasDecl(apprt.Surface, "toggleWindowDecorations")) {
|
||||
self.rt_surface.toggleWindowDecorations();
|
||||
} else log.warn("runtime doesn't implement toggleWindowDecorations", .{});
|
||||
},
|
||||
.toggle_secure_input => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.secure_input,
|
||||
.toggle,
|
||||
),
|
||||
|
||||
.select_all => {
|
||||
const sel = self.io.terminal.screen.selectAll();
|
||||
@@ -3725,24 +3864,21 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
}
|
||||
},
|
||||
|
||||
.inspector => |mode| {
|
||||
if (@hasDecl(apprt.Surface, "controlInspector")) {
|
||||
self.rt_surface.controlInspector(mode);
|
||||
} else log.warn("runtime doesn't implement controlInspector", .{});
|
||||
},
|
||||
.inspector => |mode| try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.inspector,
|
||||
switch (mode) {
|
||||
inline else => |tag| @field(
|
||||
apprt.action.Inspector,
|
||||
@tagName(tag),
|
||||
),
|
||||
},
|
||||
),
|
||||
|
||||
.close_surface => self.close(),
|
||||
|
||||
.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"),
|
||||
|
||||
@@ -4124,11 +4260,6 @@ fn completeClipboardReadOSC52(
|
||||
}
|
||||
|
||||
fn showDesktopNotification(self: *Surface, title: [:0]const u8, body: [:0]const u8) !void {
|
||||
if (comptime !@hasDecl(apprt.Surface, "showDesktopNotification")) {
|
||||
log.warn("runtime doesn't support desktop notifications", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
// Wyhash is used to hash the contents of the desktop notification to limit
|
||||
// how fast identical notifications can be sent sequentially.
|
||||
const hash_algorithm = std.hash.Wyhash;
|
||||
@@ -4164,7 +4295,14 @@ fn showDesktopNotification(self: *Surface, title: [:0]const u8, body: [:0]const
|
||||
|
||||
self.app.last_notification_time = now;
|
||||
self.app.last_notification_digest = new_digest;
|
||||
try self.rt_surface.showDesktopNotification(title, body);
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.desktop_notification,
|
||||
.{
|
||||
.title = title,
|
||||
.body = body,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn crashThreadState(self: *Surface) crash.sentry.ThreadState {
|
||||
@@ -4177,9 +4315,11 @@ fn crashThreadState(self: *Surface) crash.sentry.ThreadState {
|
||||
/// Tell the surface to present itself to the user. This may involve raising the
|
||||
/// window and switching tabs.
|
||||
fn presentSurface(self: *Surface) !void {
|
||||
if (@hasDecl(apprt.Surface, "presentSurface")) {
|
||||
self.rt_surface.presentSurface();
|
||||
} else log.warn("runtime doesn't support presentSurface", .{});
|
||||
try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.present_terminal,
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
pub const face_ttf = @embedFile("font/res/JetBrainsMono-Regular.ttf");
|
||||
|
||||
@@ -14,6 +14,7 @@ const build_config = @import("build_config.zig");
|
||||
|
||||
const structs = @import("apprt/structs.zig");
|
||||
|
||||
pub const action = @import("apprt/action.zig");
|
||||
pub const glfw = @import("apprt/glfw.zig");
|
||||
pub const gtk = @import("apprt/gtk.zig");
|
||||
pub const none = @import("apprt/none.zig");
|
||||
@@ -21,17 +22,17 @@ pub const browser = @import("apprt/browser.zig");
|
||||
pub const embedded = @import("apprt/embedded.zig");
|
||||
pub const surface = @import("apprt/surface.zig");
|
||||
|
||||
pub const Action = action.Action;
|
||||
pub const Target = action.Target;
|
||||
|
||||
pub const ContentScale = structs.ContentScale;
|
||||
pub const Clipboard = structs.Clipboard;
|
||||
pub const ClipboardRequest = structs.ClipboardRequest;
|
||||
pub const ClipboardRequestType = structs.ClipboardRequestType;
|
||||
pub const ColorScheme = structs.ColorScheme;
|
||||
pub const CursorPos = structs.CursorPos;
|
||||
pub const DesktopNotification = structs.DesktopNotification;
|
||||
pub const GotoTab = structs.GotoTab;
|
||||
pub const IMEPos = structs.IMEPos;
|
||||
pub const Selection = structs.Selection;
|
||||
pub const SplitDirection = structs.SplitDirection;
|
||||
pub const SurfaceSize = structs.SurfaceSize;
|
||||
|
||||
/// The implementation to use for the app runtime. This is comptime chosen
|
||||
@@ -84,4 +85,6 @@ pub const Runtime = enum {
|
||||
test {
|
||||
_ = Runtime;
|
||||
_ = runtime;
|
||||
_ = action;
|
||||
_ = structs;
|
||||
}
|
||||
|
||||
407
src/apprt/action.zig
Normal file
407
src/apprt/action.zig
Normal file
@@ -0,0 +1,407 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const apprt = @import("../apprt.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,
|
||||
|
||||
/// 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,
|
||||
|
||||
/// 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,
|
||||
|
||||
/// 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,
|
||||
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,
|
||||
mouse_shape,
|
||||
mouse_visibility,
|
||||
mouse_over_link,
|
||||
renderer_health,
|
||||
open_config,
|
||||
quit_timer,
|
||||
secure_input,
|
||||
};
|
||||
|
||||
/// 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,
|
||||
};
|
||||
|
||||
// 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,
|
||||
};
|
||||
};
|
||||
|
||||
/// 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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// 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,
|
||||
};
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -127,9 +127,87 @@ pub const App = struct {
|
||||
glfw.postEmptyEvent();
|
||||
}
|
||||
|
||||
/// Open the configuration in the system editor.
|
||||
pub fn openConfig(self: *App) !void {
|
||||
try configpkg.edit.open(self.app.alloc);
|
||||
/// Perform a given action.
|
||||
pub fn performAction(
|
||||
self: *App,
|
||||
target: apprt.Target,
|
||||
comptime action: apprt.Action.Key,
|
||||
value: apprt.Action.Value(action),
|
||||
) !void {
|
||||
switch (action) {
|
||||
.new_window => _ = try self.newSurface(switch (target) {
|
||||
.app => null,
|
||||
.surface => |v| v,
|
||||
}),
|
||||
|
||||
.new_tab => try self.newTab(switch (target) {
|
||||
.app => null,
|
||||
.surface => |v| v,
|
||||
}),
|
||||
|
||||
.size_limit => switch (target) {
|
||||
.app => {},
|
||||
.surface => |surface| try surface.rt_surface.setSizeLimits(.{
|
||||
.width = value.min_width,
|
||||
.height = value.min_height,
|
||||
}, if (value.max_width > 0) .{
|
||||
.width = value.max_width,
|
||||
.height = value.max_height,
|
||||
} else null),
|
||||
},
|
||||
|
||||
.initial_size => switch (target) {
|
||||
.app => {},
|
||||
.surface => |surface| try surface.rt_surface.setInitialWindowSize(
|
||||
value.width,
|
||||
value.height,
|
||||
),
|
||||
},
|
||||
|
||||
.toggle_fullscreen => self.toggleFullscreen(target),
|
||||
|
||||
.open_config => try configpkg.edit.open(self.app.alloc),
|
||||
|
||||
.set_title => switch (target) {
|
||||
.app => {},
|
||||
.surface => |surface| try surface.rt_surface.setTitle(value.title),
|
||||
},
|
||||
|
||||
.mouse_shape => switch (target) {
|
||||
.app => {},
|
||||
.surface => |surface| try surface.rt_surface.setMouseShape(value),
|
||||
},
|
||||
|
||||
.mouse_visibility => switch (target) {
|
||||
.app => {},
|
||||
.surface => |surface| surface.rt_surface.setMouseVisibility(switch (value) {
|
||||
.visible => true,
|
||||
.hidden => false,
|
||||
}),
|
||||
},
|
||||
|
||||
// Unimplemented
|
||||
.new_split,
|
||||
.goto_split,
|
||||
.resize_split,
|
||||
.equalize_splits,
|
||||
.toggle_split_zoom,
|
||||
.present_terminal,
|
||||
.close_all_windows,
|
||||
.toggle_tab_overview,
|
||||
.toggle_window_decorations,
|
||||
.toggle_quick_terminal,
|
||||
.goto_tab,
|
||||
.inspector,
|
||||
.render_inspector,
|
||||
.quit_timer,
|
||||
.secure_input,
|
||||
.desktop_notification,
|
||||
.mouse_over_link,
|
||||
.cell_size,
|
||||
.renderer_health,
|
||||
=> log.info("unimplemented action={}", .{action}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reload the configuration. This should return the new configuration.
|
||||
@@ -150,8 +228,12 @@ pub const App = struct {
|
||||
}
|
||||
|
||||
/// Toggle the window to fullscreen mode.
|
||||
pub fn toggleFullscreen(self: *App, surface: *Surface) void {
|
||||
fn toggleFullscreen(self: *App, target: apprt.Target) void {
|
||||
_ = self;
|
||||
const surface: *Surface = switch (target) {
|
||||
.app => return,
|
||||
.surface => |v| v.rt_surface,
|
||||
};
|
||||
const win = surface.window;
|
||||
|
||||
if (surface.isFullscreen()) {
|
||||
@@ -195,18 +277,18 @@ pub const App = struct {
|
||||
win.setMonitor(monitor, 0, 0, video_mode.getWidth(), video_mode.getHeight(), 0);
|
||||
}
|
||||
|
||||
/// Create a new window for the app.
|
||||
pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
|
||||
_ = try self.newSurface(parent_);
|
||||
}
|
||||
|
||||
/// Create a new tab in the parent surface.
|
||||
fn newTab(self: *App, parent: *CoreSurface) !void {
|
||||
fn newTab(self: *App, parent_: ?*CoreSurface) !void {
|
||||
if (!Darwin.enabled) {
|
||||
log.warn("tabbing is not supported on this platform", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = parent_ orelse {
|
||||
_ = try self.newSurface(null);
|
||||
return;
|
||||
};
|
||||
|
||||
// Create the new window
|
||||
const window = try self.newSurface(parent);
|
||||
|
||||
@@ -370,7 +452,6 @@ pub const Surface = struct {
|
||||
/// Initialize the surface into the given self pointer. This gives a
|
||||
/// stable pointer to the destination that can be used for callbacks.
|
||||
pub fn init(self: *Surface, app: *App) !void {
|
||||
|
||||
// Create our window
|
||||
const win = glfw.Window.create(
|
||||
640,
|
||||
@@ -525,20 +606,11 @@ pub const Surface = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new tab in the window containing this surface.
|
||||
pub fn newTab(self: *Surface) !void {
|
||||
try self.app.newTab(&self.core_surface);
|
||||
}
|
||||
|
||||
/// Checks if the glfw window is in fullscreen.
|
||||
pub fn isFullscreen(self: *Surface) bool {
|
||||
return self.window.getMonitor() != null;
|
||||
}
|
||||
|
||||
pub fn toggleFullscreen(self: *Surface, _: Config.NonNativeFullscreen) void {
|
||||
self.app.toggleFullscreen(self);
|
||||
}
|
||||
|
||||
/// Close this surface.
|
||||
pub fn close(self: *Surface, processActive: bool) void {
|
||||
_ = processActive;
|
||||
@@ -550,7 +622,7 @@ pub const Surface = struct {
|
||||
/// Set the initial window size. This is called exactly once at
|
||||
/// surface initialization time. This may be called before "self"
|
||||
/// is fully initialized.
|
||||
pub fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void {
|
||||
fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void {
|
||||
const monitor = self.window.getMonitor() orelse glfw.Monitor.getPrimary() orelse {
|
||||
log.warn("window is not on a monitor, not setting initial size", .{});
|
||||
return;
|
||||
@@ -563,18 +635,11 @@ pub const Surface = struct {
|
||||
});
|
||||
}
|
||||
|
||||
/// Set the cell size. Unused by GLFW.
|
||||
pub fn setCellSize(self: *const Surface, width: u32, height: u32) !void {
|
||||
_ = self;
|
||||
_ = width;
|
||||
_ = height;
|
||||
}
|
||||
|
||||
/// Set the size limits of the window.
|
||||
/// Note: this interface is not good, we should redo it if we plan
|
||||
/// to use this more. i.e. you can't set max width but no max height,
|
||||
/// or no mins.
|
||||
pub fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void {
|
||||
fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void {
|
||||
self.window.setSizeLimits(.{
|
||||
.width = min.width,
|
||||
.height = min.height,
|
||||
@@ -624,7 +689,7 @@ pub const Surface = struct {
|
||||
}
|
||||
|
||||
/// Set the title of the window.
|
||||
pub fn setTitle(self: *Surface, slice: [:0]const u8) !void {
|
||||
fn setTitle(self: *Surface, slice: [:0]const u8) !void {
|
||||
if (self.title_text) |t| self.core_surface.alloc.free(t);
|
||||
self.title_text = try self.core_surface.alloc.dupeZ(u8, slice);
|
||||
self.window.setTitle(self.title_text.?.ptr);
|
||||
@@ -636,7 +701,7 @@ pub const Surface = struct {
|
||||
}
|
||||
|
||||
/// Set the shape of the cursor.
|
||||
pub fn setMouseShape(self: *Surface, shape: terminal.MouseShape) !void {
|
||||
fn setMouseShape(self: *Surface, shape: terminal.MouseShape) !void {
|
||||
if ((comptime builtin.target.isDarwin()) and
|
||||
!internal_os.macosVersionAtLeast(13, 0, 0))
|
||||
{
|
||||
@@ -672,15 +737,20 @@ pub const Surface = struct {
|
||||
self.cursor = new;
|
||||
}
|
||||
|
||||
pub fn mouseOverLink(self: *Surface, uri: ?[]const u8) void {
|
||||
// We don't do anything in GLFW.
|
||||
_ = self;
|
||||
_ = uri;
|
||||
/// Set the visibility of the mouse cursor.
|
||||
fn setMouseVisibility(self: *Surface, visible: bool) void {
|
||||
self.window.setInputModeCursor(if (visible) .normal else .hidden);
|
||||
}
|
||||
|
||||
/// Set the visibility of the mouse cursor.
|
||||
pub fn setMouseVisibility(self: *Surface, visible: bool) void {
|
||||
self.window.setInputModeCursor(if (visible) .normal else .hidden);
|
||||
pub fn supportsClipboard(
|
||||
self: *const Surface,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
) bool {
|
||||
_ = self;
|
||||
return switch (clipboard_type) {
|
||||
.standard => true,
|
||||
.selection, .primary => comptime builtin.os.tag == .linux,
|
||||
};
|
||||
}
|
||||
|
||||
/// Start an async clipboard request.
|
||||
@@ -1040,7 +1110,7 @@ pub const Surface = struct {
|
||||
core_win.cursorPosCallback(.{
|
||||
.x = @floatCast(pos.xpos),
|
||||
.y = @floatCast(pos.ypos),
|
||||
}) catch |err| {
|
||||
}, null) catch |err| {
|
||||
log.err("error in cursor pos callback err={}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ const apprt = @import("../../apprt.zig");
|
||||
const configpkg = @import("../../config.zig");
|
||||
const input = @import("../../input.zig");
|
||||
const internal_os = @import("../../os/main.zig");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
const Config = configpkg.Config;
|
||||
const CoreApp = @import("../../App.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
@@ -27,6 +28,7 @@ const Surface = @import("Surface.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig");
|
||||
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
|
||||
const Split = @import("Split.zig");
|
||||
const c = @import("c.zig").c;
|
||||
const version = @import("version.zig");
|
||||
const inspector = @import("inspector.zig");
|
||||
@@ -86,6 +88,16 @@ quit_timer: union(enum) {
|
||||
pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
_ = opts;
|
||||
|
||||
// Log our GTK version
|
||||
log.info("GTK version build={d}.{d}.{d} runtime={d}.{d}.{d}", .{
|
||||
c.GTK_MAJOR_VERSION,
|
||||
c.GTK_MINOR_VERSION,
|
||||
c.GTK_MICRO_VERSION,
|
||||
c.gtk_get_major_version(),
|
||||
c.gtk_get_minor_version(),
|
||||
c.gtk_get_micro_version(),
|
||||
});
|
||||
|
||||
if (version.atLeast(4, 16, 0)) {
|
||||
// From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE
|
||||
_ = internal_os.setenv("GDK_DISABLE", "gles-api");
|
||||
@@ -340,9 +352,340 @@ pub fn terminate(self: *App) void {
|
||||
self.config.deinit();
|
||||
}
|
||||
|
||||
/// Open the configuration in the system editor.
|
||||
pub fn openConfig(self: *App) !void {
|
||||
try configpkg.edit.open(self.core_app.alloc);
|
||||
/// Perform a given action.
|
||||
pub fn performAction(
|
||||
self: *App,
|
||||
target: apprt.Target,
|
||||
comptime action: apprt.Action.Key,
|
||||
value: apprt.Action.Value(action),
|
||||
) !void {
|
||||
switch (action) {
|
||||
.new_window => _ = try self.newWindow(switch (target) {
|
||||
.app => null,
|
||||
.surface => |v| v,
|
||||
}),
|
||||
.toggle_fullscreen => self.toggleFullscreen(target, value),
|
||||
|
||||
.new_tab => try self.newTab(target),
|
||||
.goto_tab => self.gotoTab(target, value),
|
||||
.new_split => try self.newSplit(target, value),
|
||||
.resize_split => self.resizeSplit(target, value),
|
||||
.equalize_splits => self.equalizeSplits(target),
|
||||
.goto_split => self.gotoSplit(target, value),
|
||||
.open_config => try configpkg.edit.open(self.core_app.alloc),
|
||||
.inspector => self.controlInspector(target, value),
|
||||
.desktop_notification => self.showDesktopNotification(target, value),
|
||||
.set_title => try self.setTitle(target, value),
|
||||
.present_terminal => self.presentTerminal(target),
|
||||
.initial_size => try self.setInitialSize(target, value),
|
||||
.mouse_visibility => self.setMouseVisibility(target, value),
|
||||
.mouse_shape => try self.setMouseShape(target, value),
|
||||
.mouse_over_link => self.setMouseOverLink(target, value),
|
||||
.toggle_tab_overview => self.toggleTabOverview(target),
|
||||
.toggle_window_decorations => self.toggleWindowDecorations(target),
|
||||
.quit_timer => self.quitTimer(value),
|
||||
|
||||
// Unimplemented
|
||||
.close_all_windows,
|
||||
.toggle_split_zoom,
|
||||
.toggle_quick_terminal,
|
||||
.size_limit,
|
||||
.cell_size,
|
||||
.secure_input,
|
||||
.render_inspector,
|
||||
.renderer_health,
|
||||
=> log.warn("unimplemented action={}", .{action}),
|
||||
}
|
||||
}
|
||||
|
||||
fn newTab(_: *App, target: apprt.Target) !void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const window = v.rt_surface.container.window() orelse {
|
||||
log.info(
|
||||
"new_tab invalid for container={s}",
|
||||
.{@tagName(v.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
try window.newTab(v);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn gotoTab(_: *App, target: apprt.Target, tab: apprt.action.GotoTab) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const window = v.rt_surface.container.window() orelse {
|
||||
log.info(
|
||||
"gotoTab invalid for container={s}",
|
||||
.{@tagName(v.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
switch (tab) {
|
||||
.previous => window.gotoPreviousTab(v.rt_surface),
|
||||
.next => window.gotoNextTab(v.rt_surface),
|
||||
.last => window.gotoLastTab(),
|
||||
else => window.gotoTab(@intCast(@intFromEnum(tab))),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn newSplit(
|
||||
self: *App,
|
||||
target: apprt.Target,
|
||||
direction: apprt.action.SplitDirection,
|
||||
) !void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const alloc = self.core_app.alloc;
|
||||
_ = try Split.create(alloc, v.rt_surface, direction);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn equalizeSplits(_: *App, target: apprt.Target) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const tab = v.rt_surface.container.tab() orelse return;
|
||||
const top_split = switch (tab.elem) {
|
||||
.split => |s| s,
|
||||
else => return,
|
||||
};
|
||||
_ = top_split.equalize();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn gotoSplit(
|
||||
_: *const App,
|
||||
target: apprt.Target,
|
||||
direction: apprt.action.GotoSplit,
|
||||
) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const s = v.rt_surface.container.split() orelse return;
|
||||
const map = s.directionMap(switch (v.rt_surface.container) {
|
||||
.split_tl => .top_left,
|
||||
.split_br => .bottom_right,
|
||||
.none, .tab_ => unreachable,
|
||||
});
|
||||
const surface_ = map.get(direction) orelse return;
|
||||
if (surface_) |surface| surface.grabFocus();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn resizeSplit(
|
||||
_: *const App,
|
||||
target: apprt.Target,
|
||||
resize: apprt.action.ResizeSplit,
|
||||
) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const s = v.rt_surface.container.firstSplitWithOrientation(
|
||||
Split.Orientation.fromResizeDirection(resize.direction),
|
||||
) orelse return;
|
||||
s.moveDivider(resize.direction, resize.amount);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn presentTerminal(
|
||||
_: *const App,
|
||||
target: apprt.Target,
|
||||
) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| v.rt_surface.present(),
|
||||
}
|
||||
}
|
||||
|
||||
fn controlInspector(
|
||||
_: *const App,
|
||||
target: apprt.Target,
|
||||
mode: apprt.action.Inspector,
|
||||
) void {
|
||||
const surface: *Surface = switch (target) {
|
||||
.app => return,
|
||||
.surface => |v| v.rt_surface,
|
||||
};
|
||||
|
||||
surface.controlInspector(mode);
|
||||
}
|
||||
|
||||
fn toggleFullscreen(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
_: apprt.action.Fullscreen,
|
||||
) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const window = v.rt_surface.container.window() orelse {
|
||||
log.info(
|
||||
"toggleFullscreen invalid for container={s}",
|
||||
.{@tagName(v.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
window.toggleFullscreen();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn toggleTabOverview(_: *App, target: apprt.Target) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const window = v.rt_surface.container.window() orelse {
|
||||
log.info(
|
||||
"toggleTabOverview invalid for container={s}",
|
||||
.{@tagName(v.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
window.toggleTabOverview();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn toggleWindowDecorations(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const window = v.rt_surface.container.window() orelse {
|
||||
log.info(
|
||||
"toggleFullscreen invalid for container={s}",
|
||||
.{@tagName(v.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
window.toggleWindowDecorations();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void {
|
||||
switch (mode) {
|
||||
.start => self.startQuitTimer(),
|
||||
.stop => self.stopQuitTimer(),
|
||||
}
|
||||
}
|
||||
|
||||
fn setTitle(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
title: apprt.action.SetTitle,
|
||||
) !void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| try v.rt_surface.setTitle(title.title),
|
||||
}
|
||||
}
|
||||
|
||||
fn setMouseVisibility(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
visibility: apprt.action.MouseVisibility,
|
||||
) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| v.rt_surface.setMouseVisibility(switch (visibility) {
|
||||
.visible => true,
|
||||
.hidden => false,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn setMouseShape(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
shape: terminal.MouseShape,
|
||||
) !void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| try v.rt_surface.setMouseShape(shape),
|
||||
}
|
||||
}
|
||||
|
||||
fn setMouseOverLink(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
value: apprt.action.MouseOverLink,
|
||||
) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| v.rt_surface.mouseOverLink(if (value.url.len > 0)
|
||||
value.url
|
||||
else
|
||||
null),
|
||||
}
|
||||
}
|
||||
|
||||
fn setInitialSize(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
value: apprt.action.InitialSize,
|
||||
) !void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| try v.rt_surface.setInitialWindowSize(
|
||||
value.width,
|
||||
value.height,
|
||||
),
|
||||
}
|
||||
}
|
||||
fn showDesktopNotification(
|
||||
self: *App,
|
||||
target: apprt.Target,
|
||||
n: apprt.action.DesktopNotification,
|
||||
) void {
|
||||
// Set a default title if we don't already have one
|
||||
const t = switch (n.title.len) {
|
||||
0 => "Ghostty",
|
||||
else => n.title,
|
||||
};
|
||||
|
||||
const notification = c.g_notification_new(t.ptr);
|
||||
defer c.g_object_unref(notification);
|
||||
c.g_notification_set_body(notification, n.body.ptr);
|
||||
|
||||
const icon = c.g_themed_icon_new("com.mitchellh.ghostty");
|
||||
defer c.g_object_unref(icon);
|
||||
c.g_notification_set_icon(notification, icon);
|
||||
|
||||
const pointer = c.g_variant_new_uint64(switch (target) {
|
||||
.app => 0,
|
||||
.surface => |v| @intFromPtr(v),
|
||||
});
|
||||
c.g_notification_set_default_action_and_target_value(
|
||||
notification,
|
||||
"app.present-surface",
|
||||
pointer,
|
||||
);
|
||||
|
||||
const g_app: *c.GApplication = @ptrCast(self.app);
|
||||
|
||||
// We set the notification ID to the body content. If the content is the
|
||||
// same, this notification may replace a previous notification
|
||||
c.g_application_send_notification(g_app, n.body.ptr, notification);
|
||||
}
|
||||
|
||||
/// Reload the configuration. This should return the new configuration.
|
||||
@@ -442,9 +785,9 @@ fn loadRuntimeCss(config: *const Config, provider: *c.GtkCssProvider) !void {
|
||||
\\ opacity: {d:.2};
|
||||
\\ background-color: rgb({d},{d},{d});
|
||||
\\}}
|
||||
\\window.ghostty-theme-inherit headerbar,
|
||||
\\window.ghostty-theme-inherit toolbarview > revealer > windowhandle,
|
||||
\\window.ghostty-theme-inherit box > tabbar {{
|
||||
\\window.window-theme-ghostty .top-bar,
|
||||
\\window.window-theme-ghostty .bottom-bar,
|
||||
\\window.window-theme-ghostty box > tabbar {{
|
||||
\\ background-color: rgb({d},{d},{d});
|
||||
\\ color: rgb({d},{d},{d});
|
||||
\\}}
|
||||
@@ -564,9 +907,9 @@ pub fn gtkQuitTimerExpired(ud: ?*anyopaque) callconv(.C) c.gboolean {
|
||||
}
|
||||
|
||||
/// This will get called when there are no more open surfaces.
|
||||
pub fn startQuitTimer(self: *App) void {
|
||||
fn startQuitTimer(self: *App) void {
|
||||
// Cancel any previous timer.
|
||||
self.cancelQuitTimer();
|
||||
self.stopQuitTimer();
|
||||
|
||||
// This is a no-op unless we are configured to quit after last window is closed.
|
||||
if (!self.config.@"quit-after-last-window-closed") return;
|
||||
@@ -581,7 +924,7 @@ pub fn startQuitTimer(self: *App) void {
|
||||
}
|
||||
|
||||
/// This will get called when a new surface gets opened.
|
||||
pub fn cancelQuitTimer(self: *App) void {
|
||||
fn stopQuitTimer(self: *App) void {
|
||||
switch (self.quit_timer) {
|
||||
.off => {},
|
||||
.expired => self.quit_timer = .{ .off = {} },
|
||||
@@ -607,7 +950,7 @@ pub fn redrawInspector(self: *App, surface: *Surface) void {
|
||||
}
|
||||
|
||||
/// Called by CoreApp to create a new window with a new surface.
|
||||
pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
|
||||
fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
|
||||
const alloc = self.core_app.alloc;
|
||||
|
||||
// Allocate a fixed pointer for our window. We try to minimize
|
||||
@@ -856,8 +1199,12 @@ fn gtkActionPresentSurface(
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert that u64 to pointer to a core surface.
|
||||
const surface: *CoreSurface = @ptrFromInt(c.g_variant_get_uint64(parameter));
|
||||
// Convert that u64 to pointer to a core surface. A value of zero
|
||||
// means that there was no target surface for the notification so
|
||||
// we dont' focus any surface.
|
||||
const ptr_int: u64 = c.g_variant_get_uint64(parameter);
|
||||
if (ptr_int == 0) return;
|
||||
const surface: *CoreSurface = @ptrFromInt(ptr_int);
|
||||
|
||||
// Send a message through the core app mailbox rather than presenting the
|
||||
// surface directly so that it can validate that the surface pointer is
|
||||
|
||||
@@ -7,7 +7,6 @@ const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const font = @import("../../font/main.zig");
|
||||
const input = @import("../../input.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
|
||||
const Surface = @import("Surface.zig");
|
||||
@@ -21,14 +20,14 @@ pub const Orientation = enum {
|
||||
horizontal,
|
||||
vertical,
|
||||
|
||||
pub fn fromDirection(direction: apprt.SplitDirection) Orientation {
|
||||
pub fn fromDirection(direction: apprt.action.SplitDirection) Orientation {
|
||||
return switch (direction) {
|
||||
.right => .horizontal,
|
||||
.down => .vertical,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn fromResizeDirection(direction: input.SplitResizeDirection) Orientation {
|
||||
pub fn fromResizeDirection(direction: apprt.action.ResizeSplit.Direction) Orientation {
|
||||
return switch (direction) {
|
||||
.up, .down => .vertical,
|
||||
.left, .right => .horizontal,
|
||||
@@ -58,7 +57,7 @@ bottom_right: Surface.Container.Elem,
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
sibling: *Surface,
|
||||
direction: apprt.SplitDirection,
|
||||
direction: apprt.action.SplitDirection,
|
||||
) !*Split {
|
||||
var split = try alloc.create(Split);
|
||||
errdefer alloc.destroy(split);
|
||||
@@ -69,7 +68,7 @@ pub fn create(
|
||||
pub fn init(
|
||||
self: *Split,
|
||||
sibling: *Surface,
|
||||
direction: apprt.SplitDirection,
|
||||
direction: apprt.action.SplitDirection,
|
||||
) !void {
|
||||
// Create the new child surface for the other direction.
|
||||
const alloc = sibling.app.core_app.alloc;
|
||||
@@ -164,7 +163,11 @@ fn removeChild(
|
||||
}
|
||||
|
||||
/// Move the divider in the given direction by the given amount.
|
||||
pub fn moveDivider(self: *Split, direction: input.SplitResizeDirection, amount: u16) void {
|
||||
pub fn moveDivider(
|
||||
self: *Split,
|
||||
direction: apprt.action.ResizeSplit.Direction,
|
||||
amount: u16,
|
||||
) void {
|
||||
const min_pos = 10;
|
||||
|
||||
const pos = c.gtk_paned_get_position(self.paned);
|
||||
@@ -263,7 +266,7 @@ fn updateChildren(self: *const Split) void {
|
||||
|
||||
/// A mapping of direction to the element (if any) in that direction.
|
||||
pub const DirectionMap = std.EnumMap(
|
||||
input.SplitFocusDirection,
|
||||
apprt.action.GotoSplit,
|
||||
?*Surface,
|
||||
);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ const configpkg = @import("../../config.zig");
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const font = @import("../../font/main.zig");
|
||||
const input = @import("../../input.zig");
|
||||
const renderer = @import("../../renderer.zig");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
const internal_os = @import("../../os/main.zig");
|
||||
@@ -688,7 +689,10 @@ pub fn close(self: *Surface, processActive: bool) void {
|
||||
c.gtk_widget_show(alert);
|
||||
}
|
||||
|
||||
pub fn controlInspector(self: *Surface, mode: input.InspectorMode) void {
|
||||
pub fn controlInspector(
|
||||
self: *Surface,
|
||||
mode: apprt.action.Inspector,
|
||||
) void {
|
||||
const show = switch (mode) {
|
||||
.toggle => self.inspector == null,
|
||||
.show => true,
|
||||
@@ -715,30 +719,6 @@ pub fn controlInspector(self: *Surface, mode: input.InspectorMode) void {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toggleFullscreen(self: *Surface, mac_non_native: configpkg.NonNativeFullscreen) void {
|
||||
const window = self.container.window() orelse {
|
||||
log.info(
|
||||
"toggleFullscreen invalid for container={s}",
|
||||
.{@tagName(self.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
window.toggleFullscreen(mac_non_native);
|
||||
}
|
||||
|
||||
pub fn toggleWindowDecorations(self: *Surface) void {
|
||||
const window = self.container.window() orelse {
|
||||
log.info(
|
||||
"toggleWindowDecorations invalid for container={s}",
|
||||
.{@tagName(self.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
window.toggleWindowDecorations();
|
||||
}
|
||||
|
||||
pub fn getTitleLabel(self: *Surface) ?*c.GtkWidget {
|
||||
switch (self.title) {
|
||||
.none => return null,
|
||||
@@ -749,69 +729,6 @@ pub fn getTitleLabel(self: *Surface) ?*c.GtkWidget {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn newSplit(self: *Surface, direction: apprt.SplitDirection) !void {
|
||||
const alloc = self.app.core_app.alloc;
|
||||
_ = try Split.create(alloc, self, direction);
|
||||
}
|
||||
|
||||
pub fn gotoSplit(self: *const Surface, direction: input.SplitFocusDirection) void {
|
||||
const s = self.container.split() orelse return;
|
||||
const map = s.directionMap(switch (self.container) {
|
||||
.split_tl => .top_left,
|
||||
.split_br => .bottom_right,
|
||||
.none, .tab_ => unreachable,
|
||||
});
|
||||
const surface_ = map.get(direction) orelse return;
|
||||
if (surface_) |surface| surface.grabFocus();
|
||||
}
|
||||
|
||||
pub fn resizeSplit(self: *const Surface, direction: input.SplitResizeDirection, amount: u16) void {
|
||||
const s = self.container.firstSplitWithOrientation(
|
||||
Split.Orientation.fromResizeDirection(direction),
|
||||
) orelse return;
|
||||
s.moveDivider(direction, amount);
|
||||
}
|
||||
|
||||
pub fn equalizeSplits(self: *const Surface) void {
|
||||
const tab = self.container.tab() orelse return;
|
||||
const top_split = switch (tab.elem) {
|
||||
.split => |s| s,
|
||||
else => return,
|
||||
};
|
||||
_ = top_split.equalize();
|
||||
}
|
||||
|
||||
pub fn newTab(self: *Surface) !void {
|
||||
const window = self.container.window() orelse {
|
||||
log.info("surface cannot create new tab when not attached to a window", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
try window.newTab(&self.core_surface);
|
||||
}
|
||||
|
||||
pub fn hasTabs(self: *const Surface) bool {
|
||||
const window = self.container.window() orelse return false;
|
||||
return window.hasTabs();
|
||||
}
|
||||
|
||||
pub fn gotoTab(self: *Surface, tab: apprt.GotoTab) void {
|
||||
const window = self.container.window() orelse {
|
||||
log.info(
|
||||
"gotoTab invalid for container={s}",
|
||||
.{@tagName(self.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
switch (tab) {
|
||||
.previous => window.gotoPreviousTab(self),
|
||||
.next => window.gotoNextTab(self),
|
||||
.last => window.gotoLastTab(),
|
||||
else => window.gotoTab(@intCast(@intFromEnum(tab))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setShouldClose(self: *Surface) void {
|
||||
_ = self;
|
||||
}
|
||||
@@ -867,18 +784,6 @@ pub fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setCellSize(self: *const Surface, width: u32, height: u32) !void {
|
||||
_ = self;
|
||||
_ = width;
|
||||
_ = height;
|
||||
}
|
||||
|
||||
pub fn setSizeLimits(self: *Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void {
|
||||
_ = self;
|
||||
_ = min;
|
||||
_ = max_;
|
||||
}
|
||||
|
||||
pub fn grabFocus(self: *Surface) void {
|
||||
if (self.container.tab()) |tab| tab.focus_child = self;
|
||||
|
||||
@@ -1026,6 +931,19 @@ pub fn mouseOverLink(self: *Surface, uri_: ?[]const u8) void {
|
||||
self.url_widget = URLWidget.init(self, uriZ);
|
||||
}
|
||||
|
||||
pub fn supportsClipboard(
|
||||
self: *const Surface,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
) bool {
|
||||
_ = self;
|
||||
return switch (clipboard_type) {
|
||||
.standard,
|
||||
.selection,
|
||||
.primary,
|
||||
=> true,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn clipboardRequest(
|
||||
self: *Surface,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
@@ -1386,7 +1304,7 @@ fn gtkMouseUp(
|
||||
}
|
||||
|
||||
fn gtkMouseMotion(
|
||||
_: *c.GtkEventControllerMotion,
|
||||
ec: *c.GtkEventControllerMotion,
|
||||
x: c.gdouble,
|
||||
y: c.gdouble,
|
||||
ud: ?*anyopaque,
|
||||
@@ -1415,7 +1333,12 @@ fn gtkMouseMotion(
|
||||
self.grabFocus();
|
||||
}
|
||||
|
||||
self.core_surface.cursorPosCallback(self.cursor_pos) catch |err| {
|
||||
// Get our modifiers
|
||||
const event = c.gtk_event_controller_get_current_event(@ptrCast(ec));
|
||||
const gtk_mods = c.gdk_event_get_modifier_state(event);
|
||||
const mods = translateMods(gtk_mods);
|
||||
|
||||
self.core_surface.cursorPosCallback(self.cursor_pos, mods) catch |err| {
|
||||
log.err("error in cursor pos callback err={}", .{err});
|
||||
return;
|
||||
};
|
||||
@@ -1975,7 +1898,7 @@ fn translateMods(state: c.GdkModifierType) input.Mods {
|
||||
return mods;
|
||||
}
|
||||
|
||||
pub fn presentSurface(self: *Surface) void {
|
||||
pub fn present(self: *Surface) void {
|
||||
if (self.container.window()) |window| {
|
||||
if (self.container.tab()) |tab| {
|
||||
if (window.notebook.getTabPosition(tab)) |position|
|
||||
@@ -1983,5 +1906,6 @@ pub fn presentSurface(self: *Surface) void {
|
||||
}
|
||||
c.gtk_window_present(window.window);
|
||||
}
|
||||
|
||||
self.grabFocus();
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ window: *c.GtkWindow,
|
||||
/// GtkHeaderBar depending on if adw is enabled and linked.
|
||||
header: ?*c.GtkWidget,
|
||||
|
||||
/// The tab overview for the window. This is possibly null since there is no
|
||||
/// taboverview without a AdwApplicationWindow (libadwaita >= 1.4.0).
|
||||
tab_overview: ?*c.GtkWidget,
|
||||
|
||||
/// The notebook (tab grouping) for this window.
|
||||
/// can be either c.GtkNotebook or c.AdwTabView.
|
||||
notebook: Notebook,
|
||||
@@ -68,6 +72,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
.app = app,
|
||||
.window = undefined,
|
||||
.header = null,
|
||||
.tab_overview = null,
|
||||
.notebook = undefined,
|
||||
.context_menu = undefined,
|
||||
.toast_overlay = undefined,
|
||||
@@ -97,7 +102,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
|
||||
// Apply class to color headerbar if window-theme is set to `ghostty`.
|
||||
if (app.config.@"window-theme" == .ghostty) {
|
||||
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "ghostty-theme-inherit");
|
||||
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "window-theme-ghostty");
|
||||
}
|
||||
|
||||
// Remove the window's background if any of the widgets need to be transparent
|
||||
@@ -114,7 +119,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
|
||||
|
||||
// If we are using an AdwWindow then we can support the tab overview.
|
||||
const tab_overview_: ?*c.GtkWidget = if (self.isAdwWindow()) overview: {
|
||||
self.tab_overview = if (self.isAdwWindow()) overview: {
|
||||
const tab_overview = c.adw_tab_overview_new();
|
||||
c.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1);
|
||||
_ = c.g_signal_connect_data(
|
||||
@@ -152,14 +157,15 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
c.gtk_widget_set_tooltip_text(btn, "Main Menu");
|
||||
c.gtk_menu_button_set_icon_name(@ptrCast(btn), "open-menu-symbolic");
|
||||
c.gtk_menu_button_set_menu_model(@ptrCast(btn), @ptrCast(@alignCast(app.menu)));
|
||||
if (self.isAdwWindow())
|
||||
c.adw_header_bar_pack_end(@ptrCast(header), btn)
|
||||
else
|
||||
c.gtk_header_bar_pack_end(@ptrCast(header), btn);
|
||||
if (self.isAdwWindow()) {
|
||||
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||
c.adw_header_bar_pack_end(@ptrCast(header), btn);
|
||||
} else c.gtk_header_bar_pack_end(@ptrCast(header), btn);
|
||||
}
|
||||
|
||||
// If we're using an AdwWindow then we can support the tab overview.
|
||||
if (tab_overview_) |tab_overview| {
|
||||
if (self.tab_overview) |tab_overview| {
|
||||
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||
assert(self.isAdwWindow());
|
||||
|
||||
const btn = c.gtk_toggle_button_new();
|
||||
@@ -235,7 +241,8 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
};
|
||||
|
||||
// If we have a tab overview then we can set it on our notebook.
|
||||
if (tab_overview_) |tab_overview| {
|
||||
if (self.tab_overview) |tab_overview| {
|
||||
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||
assert(self.notebook == .adw_tab_view);
|
||||
c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw_tab_view);
|
||||
}
|
||||
@@ -256,7 +263,8 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
// Our actions for the menu
|
||||
initActions(self);
|
||||
|
||||
if (self.hasAdwToolbar()) {
|
||||
if (self.isAdwWindow()) {
|
||||
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||
const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new());
|
||||
|
||||
const header_widget: *c.GtkWidget = @ptrCast(@alignCast(self.header.?));
|
||||
@@ -289,7 +297,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
|
||||
// Set our application window content. The content depends on if
|
||||
// we're using an AdwTabOverview or not.
|
||||
if (tab_overview_) |tab_overview| {
|
||||
if (self.tab_overview) |tab_overview| {
|
||||
c.adw_tab_overview_set_child(
|
||||
@ptrCast(tab_overview),
|
||||
@ptrCast(@alignCast(toolbar_view)),
|
||||
@@ -391,18 +399,9 @@ pub fn deinit(self: *Window) void {
|
||||
/// paths that are not enabled.
|
||||
inline fn isAdwWindow(self: *Window) bool {
|
||||
return (comptime adwaita.versionAtLeast(1, 4, 0)) and
|
||||
adwaita.enabled(&self.app.config) and
|
||||
self.app.config.@"gtk-titlebar" and
|
||||
adwaita.versionAtLeast(1, 4, 0);
|
||||
}
|
||||
|
||||
/// This must be `inline` so that the comptime check noops conditional
|
||||
/// paths that are not enabled.
|
||||
inline fn hasAdwToolbar(self: *Window) bool {
|
||||
return ((comptime adwaita.versionAtLeast(1, 4, 0)) and
|
||||
adwaita.enabled(&self.app.config) and
|
||||
adwaita.versionAtLeast(1, 4, 0) and
|
||||
self.app.config.@"gtk-titlebar");
|
||||
self.app.config.@"gtk-titlebar";
|
||||
}
|
||||
|
||||
/// Add a new tab to this window.
|
||||
@@ -421,11 +420,6 @@ pub fn closeTab(self: *Window, tab: *Tab) void {
|
||||
self.notebook.closeTab(tab);
|
||||
}
|
||||
|
||||
/// Returns true if this window has any tabs.
|
||||
pub fn hasTabs(self: *const Window) bool {
|
||||
return self.notebook.nPages() > 0;
|
||||
}
|
||||
|
||||
/// Go to the previous tab for a surface.
|
||||
pub fn gotoPreviousTab(self: *Window, surface: *Surface) void {
|
||||
const tab = surface.container.tab() orelse {
|
||||
@@ -463,8 +457,17 @@ pub fn gotoTab(self: *Window, n: usize) void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle tab overview (if present)
|
||||
pub fn toggleTabOverview(self: *Window) void {
|
||||
if (self.tab_overview) |tab_overview_widget| {
|
||||
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||
const tab_overview: *c.AdwTabOverview = @ptrCast(@alignCast(tab_overview_widget));
|
||||
c.adw_tab_overview_set_open(tab_overview, 1 - c.adw_tab_overview_get_open(tab_overview));
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle fullscreen for this window.
|
||||
pub fn toggleFullscreen(self: *Window, _: configpkg.NonNativeFullscreen) void {
|
||||
pub fn toggleFullscreen(self: *Window) void {
|
||||
const is_fullscreen = c.gtk_window_is_fullscreen(self.window);
|
||||
if (is_fullscreen == 0) {
|
||||
c.gtk_window_fullscreen(self.window);
|
||||
|
||||
@@ -66,9 +66,11 @@ pub const Notebook = union(enum) {
|
||||
|
||||
const tab_view: *c.AdwTabView = c.adw_tab_view_new().?;
|
||||
|
||||
// Adwaita enables all of the shortcuts by default.
|
||||
// We want to manage keybindings ourselves.
|
||||
c.adw_tab_view_remove_shortcuts(tab_view, c.ADW_TAB_VIEW_SHORTCUT_ALL_SHORTCUTS);
|
||||
if (comptime adwaita.versionAtLeast(1, 2, 0) and adwaita.versionAtLeast(1, 2, 0)) {
|
||||
// Adwaita enables all of the shortcuts by default.
|
||||
// We want to manage keybindings ourselves.
|
||||
c.adw_tab_view_remove_shortcuts(tab_view, c.ADW_TAB_VIEW_SHORTCUT_ALL_SHORTCUTS);
|
||||
}
|
||||
|
||||
_ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT);
|
||||
@@ -261,6 +263,10 @@ pub const Notebook = union(enum) {
|
||||
_ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(&Tab.gtkTabCloseClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(&Tab.gtkTabClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
// Tab settings
|
||||
c.gtk_notebook_set_tab_reorderable(notebook, box_widget, 1);
|
||||
c.gtk_notebook_set_tab_detachable(notebook, box_widget, 1);
|
||||
|
||||
if (self.nPages() > 1) {
|
||||
c.gtk_notebook_set_show_tabs(notebook, 1);
|
||||
}
|
||||
|
||||
40
src/apprt/gtk/version.zig
Normal file
40
src/apprt/gtk/version.zig
Normal file
@@ -0,0 +1,40 @@
|
||||
const c = @import("c.zig").c;
|
||||
|
||||
/// Verifies that the GTK version is at least the given version.
|
||||
///
|
||||
/// This can be run in both a comptime and runtime context. If it
|
||||
/// is run in a comptime context, it will only check the version
|
||||
/// in the headers. If it is run in a runtime context, it will
|
||||
/// check the actual version of the library we are linked against.
|
||||
///
|
||||
/// This is inlined so that the comptime checks will disable the
|
||||
/// runtime checks if the comptime checks fail.
|
||||
pub inline fn atLeast(
|
||||
comptime major: u16,
|
||||
comptime minor: u16,
|
||||
comptime micro: u16,
|
||||
) bool {
|
||||
// If our header has lower versions than the given version,
|
||||
// we can return false immediately. This prevents us from
|
||||
// compiling against unknown symbols and makes runtime checks
|
||||
// very slightly faster.
|
||||
if (comptime c.GTK_MAJOR_VERSION < major or
|
||||
c.GTK_MINOR_VERSION < minor or
|
||||
c.GTK_MICRO_VERSION < micro) return false;
|
||||
|
||||
// If we're in comptime then we can't check the runtime version.
|
||||
if (@inComptime()) return true;
|
||||
|
||||
// We use the functions instead of the constants such as
|
||||
// c.GTK_MINOR_VERSION because the function gets the actual
|
||||
// runtime version.
|
||||
if (c.gtk_get_major_version() >= major) {
|
||||
if (c.gtk_get_major_version() > major) return true;
|
||||
if (c.gtk_get_minor_version() >= minor) {
|
||||
if (c.gtk_get_minor_version() > minor) return true;
|
||||
return c.gtk_get_micro_version() >= micro;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -52,33 +52,6 @@ pub const ClipboardRequest = union(ClipboardRequestType) {
|
||||
osc_52_write: Clipboard,
|
||||
};
|
||||
|
||||
/// A desktop notification.
|
||||
pub const DesktopNotification = struct {
|
||||
/// The title of the notification. May be an empty string to not show a
|
||||
/// title.
|
||||
title: []const u8,
|
||||
|
||||
/// The body of a notification. This will always be shown.
|
||||
body: []const u8,
|
||||
};
|
||||
|
||||
/// 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,
|
||||
_,
|
||||
};
|
||||
|
||||
// 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,
|
||||
};
|
||||
|
||||
/// The color scheme in use (light vs dark).
|
||||
pub const ColorScheme = enum(u2) {
|
||||
light = 0,
|
||||
|
||||
@@ -12,7 +12,7 @@ pub const fish_completions = comptimeGenerateFishCompletions();
|
||||
|
||||
fn comptimeGenerateFishCompletions() []const u8 {
|
||||
comptime {
|
||||
@setEvalBranchQuota(16000);
|
||||
@setEvalBranchQuota(17000);
|
||||
var counter = std.io.countingWriter(std.io.null_writer);
|
||||
try writeFishCompletions(&counter.writer());
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
|
||||
while (iter.next()) |bind| {
|
||||
const action = switch (bind.value_ptr.*) {
|
||||
.leader => continue, // TODO: support this
|
||||
.action, .action_unconsumed => |action| action,
|
||||
.leaf => |leaf| leaf.action,
|
||||
};
|
||||
const key = switch (bind.key_ptr.key) {
|
||||
.translated => |k| try std.fmt.bufPrint(&buf, "{s}", .{@tagName(k)}),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
45
src/cli/lorem_ipsum.txt
Normal file
45
src/cli/lorem_ipsum.txt
Normal file
@@ -0,0 +1,45 @@
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras hendrerit aliquet
|
||||
turpis non dictum. Mauris pulvinar nisl sit amet dui cursus tempus. Pellentesque
|
||||
ut dui justo. Etiam quis magna sagittis nisi pretium consequat vitae ut nisl.
|
||||
Sed at metus id odio pulvinar sodales. Vestibulum sollicitudin, sem id tristique
|
||||
vestibulum, neque ante dictum tortor, in convallis mi enim ac lorem. Suspendisse
|
||||
orci ex, ullamcorper sed leo vitae, mattis egestas nisl. Morbi id est vel
|
||||
ipsum mollis convallis vel at mauris. Duis vehicula facilisis placerat. Aliquam
|
||||
venenatis auctor ipsum vel elementum. Proin ac tincidunt lacus. Sed facilisis
|
||||
tellus ullamcorper bibendum lobortis. Pellentesque porta, lacus quis efficitur
|
||||
pulvinar, sem mi varius ante, sed finibus diam ante et risus.
|
||||
|
||||
Morbi ut sollicitudin justo. Nulla mattis mi ac mauris tincidunt tempor. Morbi
|
||||
vel gravida erat. Ut eu risus quis nisi facilisis aliquet varius id orci.
|
||||
Pellentesque tortor diam, porttitor nec urna nec, convallis consectetur dui.
|
||||
Vestibulum et hendrerit ipsum. Morbi pharetra dictum turpis in elementum. Ut
|
||||
nec volutpat nunc, at venenatis leo. Morbi eget nulla luctus, tincidunt dui vel,
|
||||
cursus urna. Maecenas ac pellentesque nisi. Quisque ut lorem porta, eleifend
|
||||
metus id, pellentesque tellus.
|
||||
|
||||
Vivamus gravida convallis felis, at hendrerit dolor. Vestibulum tincidunt id
|
||||
augue quis hendrerit. Praesent venenatis elit quis posuere gravida. Praesent
|
||||
at massa a purus maximus tempus. Proin dui leo, feugiat et erat ac, tincidunt
|
||||
aliquam risus. Aenean rutrum hendrerit turpis, sit amet consectetur justo porta
|
||||
non. Sed auctor justo elit, sed mollis odio ullamcorper nec. Pellentesque ac
|
||||
hendrerit tortor. Praesent quis viverra dui, sit amet imperdiet magna.
|
||||
|
||||
Mauris iaculis maximus felis, aliquet vehicula neque sagittis nec. Duis
|
||||
convallis purus enim, vel scelerisque purus dignissim eu. Donec congue sapien
|
||||
a neque rhoncus, sit amet accumsan libero tincidunt. Proin vitae placerat urna.
|
||||
Donec dolor sapien, fringilla sed semper sit amet, sollicitudin sit amet orci.
|
||||
Mauris maximus convallis vehicula. Aliquam urna ipsum, fermentum ac iaculis vel,
|
||||
blandit eget lorem. Sed enim ante, sodales a diam in, convallis interdum quam.
|
||||
Duis non urna risus. Proin ac neque at risus ullamcorper mattis eu vel nunc.
|
||||
Proin et ipsum euismod, ullamcorper justo et, imperdiet est. Curabitur quis
|
||||
arcu faucibus, bibendum nisl nec, hendrerit sapien. Curabitur vitae ante risus.
|
||||
Praesent eget sagittis tortor.
|
||||
|
||||
Mauris aliquam nec nibh eu congue. Nullam congue auctor vestibulum. Donec
|
||||
posuere sapien nec massa efficitur tincidunt. Vestibulum ante ipsum primis in
|
||||
faucibus orci luctus et ultrices posuere cubilia curae; Proin molestie, nisl
|
||||
in tincidunt condimentum, ante metus fermentum felis, ac molestie lacus dui vel
|
||||
dolor. Donec ornare laoreet posuere. Etiam id tincidunt ante. Maecenas semper
|
||||
diam ac tortor facilisis egestas. Nam eu bibendum nisl. Integer tempor nisl nec
|
||||
ex consectetur, quis lobortis enim finibus. Sed ac erat posuere, fermentum metus
|
||||
sed, suscipit nisl.
|
||||
@@ -223,7 +223,7 @@ const c = @cImport({
|
||||
@"font-codepoint-map": RepeatableCodepointMap = .{},
|
||||
|
||||
/// Draw fonts with a thicker stroke, if supported. This is only supported
|
||||
/// currently on MacOS.
|
||||
/// currently on macOS.
|
||||
@"font-thicken": bool = false,
|
||||
|
||||
/// All of the configurations behavior adjust various metrics determined by the
|
||||
@@ -429,6 +429,8 @@ palette: Palette = .{},
|
||||
/// Hide the mouse immediately when typing. The mouse becomes visible again when
|
||||
/// the mouse is used. The mouse is only hidden if the mouse cursor is over the
|
||||
/// active terminal surface.
|
||||
///
|
||||
/// macOS: This feature requires macOS 15.0 (Sequoia) or later.
|
||||
@"mouse-hide-while-typing": bool = false,
|
||||
|
||||
/// Determines whether running programs can detect the shift key pressed with a
|
||||
@@ -649,7 +651,8 @@ class: ?[:0]const u8 = null,
|
||||
@"working-directory": ?[]const u8 = null,
|
||||
|
||||
/// Key bindings. The format is `trigger=action`. Duplicate triggers will
|
||||
/// overwrite previously set values.
|
||||
/// overwrite previously set values. The list of actions is available in
|
||||
/// the documentation or using the `ghostty +list-actions` command.
|
||||
///
|
||||
/// Trigger: `+`-separated list of keys and modifiers. Example: `ctrl+a`,
|
||||
/// `ctrl+shift+b`, `up`. Some notes:
|
||||
@@ -701,6 +704,9 @@ class: ?[:0]const u8 = null,
|
||||
/// `ctrl+a>t`, and then bind `ctrl+a` directly, both `ctrl+a>n` and
|
||||
/// `ctrl+a>t` will become unbound.
|
||||
///
|
||||
/// * Trigger sequences are not allowed for `global:` or `all:`-prefixed
|
||||
/// triggers. This is a limitation we could remove in the future.
|
||||
///
|
||||
/// Action is the action to take when the trigger is satisfied. It takes the
|
||||
/// format `action` or `action:param`. The latter form is only valid if the
|
||||
/// action requires a parameter.
|
||||
@@ -720,6 +726,9 @@ class: ?[:0]const u8 = null,
|
||||
/// * `text:text` - Send a string. Uses Zig string literal syntax.
|
||||
/// i.e. `text:\x15` sends Ctrl-U.
|
||||
///
|
||||
/// * All other actions can be found in the documentation or by using the
|
||||
/// `ghostty +list-actions` command.
|
||||
///
|
||||
/// Some notes for the action:
|
||||
///
|
||||
/// * The parameter is taken as-is after the `:`. Double quotes or
|
||||
@@ -734,11 +743,48 @@ class: ?[:0]const u8 = null,
|
||||
/// removes ALL keybindings up to this point, including the default
|
||||
/// keybindings.
|
||||
///
|
||||
/// A keybind by default causes the input to be consumed. This means that the
|
||||
/// associated encoding (if any) will not be sent to the running program
|
||||
/// in the terminal. If you wish to send the encoded value to the program,
|
||||
/// specify the "unconsumed:" prefix before the entire keybind. For example:
|
||||
/// "unconsumed:ctrl+a=reload_config"
|
||||
/// The keybind trigger can be prefixed with some special values to change
|
||||
/// the behavior of the keybind. These are:
|
||||
///
|
||||
/// * `all:` - Make the keybind apply to all terminal surfaces. By default,
|
||||
/// keybinds only apply to the focused terminal surface. If this is true,
|
||||
/// then the keybind will be sent to all terminal surfaces. This only
|
||||
/// applies to actions that are surface-specific. For actions that
|
||||
/// are already global (i.e. `quit`), this prefix has no effect.
|
||||
///
|
||||
/// * `global:` - Make the keybind global. By default, keybinds only work
|
||||
/// within Ghostty and under the right conditions (application focused,
|
||||
/// sometimes terminal focused, etc.). If you want a keybind to work
|
||||
/// globally across your system (i.e. even when Ghostty is not focused),
|
||||
/// specify this prefix. This prefix implies `all:`. Note: this does not
|
||||
/// work in all environments; see the additional notes below for more
|
||||
/// information.
|
||||
///
|
||||
/// * `unconsumed:` - Do not consume the input. By default, a keybind
|
||||
/// will consume the input, meaning that the associated encoding (if
|
||||
/// any) will not be sent to the running program in the terminal. If
|
||||
/// you wish to send the encoded value to the program, specify the
|
||||
/// `unconsumed:` prefix before the entire keybind. For example:
|
||||
/// `unconsumed:ctrl+a=reload_config`. `global:` and `all:`-prefixed
|
||||
/// keybinds will always consume the input regardless of this setting.
|
||||
/// Since they are not associated with a specific terminal surface,
|
||||
/// they're never encoded.
|
||||
///
|
||||
/// Keybind trigger are not unique per prefix combination. For example,
|
||||
/// `ctrl+a` and `global:ctrl+a` are not two separate keybinds. The keybind
|
||||
/// set later will overwrite the keybind set earlier. In this case, the
|
||||
/// `global:` keybind will be used.
|
||||
///
|
||||
/// Multiple prefixes can be specified. For example,
|
||||
/// `global:unconsumed:ctrl+a=reload_config` will make the keybind global
|
||||
/// and not consume the input to reload the config.
|
||||
///
|
||||
/// A note on `global:`: this feature is only supported on macOS. On macOS,
|
||||
/// this feature requires accessibility permissions to be granted to Ghostty.
|
||||
/// When a `global:` keybind is specified and Ghostty is launched or reloaded,
|
||||
/// Ghostty will attempt to request these permissions. If the permissions are
|
||||
/// not granted, the keybind will not work. On macOS, you can find these
|
||||
/// permissions in System Preferences -> Privacy & Security -> Accessibility.
|
||||
keybind: Keybinds = .{},
|
||||
|
||||
/// Horizontal window padding. This applies padding between the terminal cells
|
||||
@@ -845,13 +891,16 @@ keybind: Keybinds = .{},
|
||||
///
|
||||
/// * `true`
|
||||
/// * `false` - windows won't have native decorations, i.e. titlebar and
|
||||
/// borders. On MacOS this also disables tabs and tab overview.
|
||||
/// borders. On macOS this also disables tabs and tab overview.
|
||||
///
|
||||
/// The "toggle_window_decoration" keybind action can be used to create
|
||||
/// a keybinding to toggle this setting at runtime.
|
||||
///
|
||||
/// Changing this configuration in your configuration and reloading will
|
||||
/// only affect new windows. Existing windows will not be affected.
|
||||
///
|
||||
/// macOS: To hide the titlebar without removing the native window borders
|
||||
/// or rounded corners, use `macos-titlebar-style = hidden` instead.
|
||||
@"window-decoration": bool = true,
|
||||
|
||||
/// The font that will be used for the application's window and tab titles.
|
||||
@@ -1171,6 +1220,40 @@ keybind: Keybinds = .{},
|
||||
/// window is ever created. Only implemented on Linux.
|
||||
@"initial-window": bool = true,
|
||||
|
||||
/// The position of the "quick" terminal window. To learn more about the
|
||||
/// quick terminal, see the documentation for the `toggle_quick_terminal`
|
||||
/// binding action.
|
||||
///
|
||||
/// Valid values are:
|
||||
///
|
||||
/// * `top` - Terminal appears at the top of the screen.
|
||||
/// * `bottom` - Terminal appears at the bottom of the screen.
|
||||
/// * `left` - Terminal appears at the left of the screen.
|
||||
/// * `right` - Terminal appears at the right of the screen.
|
||||
///
|
||||
/// Changing this configuration requires restarting Ghostty completely.
|
||||
@"quick-terminal-position": QuickTerminalPosition = .top,
|
||||
|
||||
/// The screen where the quick terminal should show up.
|
||||
///
|
||||
/// Valid values are:
|
||||
///
|
||||
/// * `main` - The screen that the operating system recommends as the main
|
||||
/// screen. On macOS, this is the screen that is currently receiving
|
||||
/// keyboard input. This screen is defined by the operating system and
|
||||
/// not chosen by Ghostty.
|
||||
///
|
||||
/// * `mouse` - The screen that the mouse is currently hovered over.
|
||||
///
|
||||
/// * `macos-menu-bar` - The screen that contains the macOS menu bar as
|
||||
/// set in the display settings on macOS. This is a bit confusing because
|
||||
/// every screen on macOS has a menu bar, but this is the screen that
|
||||
/// contains the primary menu bar.
|
||||
///
|
||||
/// The default value is `main` because this is the recommended screen
|
||||
/// by the operating system.
|
||||
@"quick-terminal-screen": QuickTerminalScreen = .main,
|
||||
|
||||
/// Whether to enable shell integration auto-injection or not. Shell integration
|
||||
/// greatly enhances the terminal experience by enabling a number of features:
|
||||
///
|
||||
@@ -1304,7 +1387,7 @@ keybind: Keybinds = .{},
|
||||
@"macos-non-native-fullscreen": NonNativeFullscreen = .false,
|
||||
|
||||
/// The style of the macOS titlebar. Available values are: "native",
|
||||
/// "transparent", and "tabs".
|
||||
/// "transparent", "tabs", and "hidden".
|
||||
///
|
||||
/// The "native" style uses the native macOS titlebar with zero customization.
|
||||
/// The titlebar will match your window theme (see `window-theme`).
|
||||
@@ -1321,6 +1404,13 @@ keybind: Keybinds = .{},
|
||||
/// macOS 14 does not have this issue and any other macOS version has not
|
||||
/// been tested.
|
||||
///
|
||||
/// The "hidden" style hides the titlebar. Unlike `window-decoration = false`,
|
||||
/// however, it does not remove the frame from the window or cause it to have
|
||||
/// squared corners. Changing to or from this option at run-time may affect
|
||||
/// existing windows in buggy ways. The top titlebar area of the window will
|
||||
/// continue to drag the window around and you will not be able to use
|
||||
/// the mouse for terminal events in this space.
|
||||
///
|
||||
/// The default value is "transparent". This is an opinionated choice
|
||||
/// but its one I think is the most aesthetically pleasing and works in
|
||||
/// most cases.
|
||||
@@ -1348,6 +1438,34 @@ keybind: Keybinds = .{},
|
||||
/// find false more visually appealing.
|
||||
@"macos-window-shadow": bool = true,
|
||||
|
||||
/// If true, Ghostty on macOS will automatically enable the "Secure Input"
|
||||
/// feature when it detects that a password prompt is being displayed.
|
||||
///
|
||||
/// "Secure Input" is a macOS security feature that prevents applications from
|
||||
/// reading keyboard events. This can always be enabled manually using the
|
||||
/// `Ghostty > Secure Keyboard Entry` menu item.
|
||||
///
|
||||
/// Note that automatic password prompt detection is based on heuristics
|
||||
/// and may not always work as expected. Specifically, it does not work
|
||||
/// over SSH connections, but there may be other cases where it also
|
||||
/// doesn't work.
|
||||
///
|
||||
/// A reason to disable this feature is if you find that it is interfering
|
||||
/// with legitimate accessibility software (or software that uses the
|
||||
/// accessibility APIs), since secure input prevents any application from
|
||||
/// reading keyboard events.
|
||||
@"macos-auto-secure-input": bool = true,
|
||||
|
||||
/// If true, Ghostty will show a graphical indication when secure input is
|
||||
/// enabled. This indication is generally recommended to know when secure input
|
||||
/// is enabled.
|
||||
///
|
||||
/// Normally, secure input is only active when a password prompt is displayed
|
||||
/// or it is manually (and typically temporarily) enabled. However, if you
|
||||
/// always have secure input enabled, the indication can be distracting and
|
||||
/// you may want to disable it.
|
||||
@"macos-secure-input-indication": bool = true,
|
||||
|
||||
/// Put every surface (tab, split, window) into a dedicated Linux cgroup.
|
||||
///
|
||||
/// This makes it so that resource management can be done on a per-surface
|
||||
@@ -3664,11 +3782,16 @@ pub const Keybinds = struct {
|
||||
)) return false,
|
||||
|
||||
// Actions are compared by field directly
|
||||
inline .action, .action_unconsumed => |_, tag| if (!equalField(
|
||||
inputpkg.Binding.Action,
|
||||
@field(self_entry.value_ptr.*, @tagName(tag)),
|
||||
@field(other_entry.value_ptr.*, @tagName(tag)),
|
||||
)) return false,
|
||||
.leaf => {
|
||||
const self_leaf = self_entry.value_ptr.*.leaf;
|
||||
const other_leaf = other_entry.value_ptr.*.leaf;
|
||||
|
||||
if (!equalField(
|
||||
inputpkg.Binding.Set.Leaf,
|
||||
self_leaf,
|
||||
other_leaf,
|
||||
)) return false;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4241,6 +4364,7 @@ pub const MacTitlebarStyle = enum {
|
||||
native,
|
||||
transparent,
|
||||
tabs,
|
||||
hidden,
|
||||
};
|
||||
|
||||
/// See gtk-single-instance
|
||||
@@ -4311,6 +4435,21 @@ pub const ResizeOverlayPosition = enum {
|
||||
@"bottom-right",
|
||||
};
|
||||
|
||||
/// See quick-terminal-position
|
||||
pub const QuickTerminalPosition = enum {
|
||||
top,
|
||||
bottom,
|
||||
left,
|
||||
right,
|
||||
};
|
||||
|
||||
/// See quick-terminal-screen
|
||||
pub const QuickTerminalScreen = enum {
|
||||
main,
|
||||
mouse,
|
||||
@"macos-menu-bar",
|
||||
};
|
||||
|
||||
/// See grapheme-width-method
|
||||
pub const GraphemeWidthMethod = enum {
|
||||
legacy,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
const dir = @import("dir.zig");
|
||||
const sentry_envelope = @import("sentry_envelope.zig");
|
||||
|
||||
pub const minidump = @import("minidump.zig");
|
||||
pub const sentry = @import("sentry.zig");
|
||||
pub const Envelope = sentry_envelope.Envelope;
|
||||
pub const defaultDir = dir.defaultDir;
|
||||
|
||||
7
src/crash/minidump.zig
Normal file
7
src/crash/minidump.zig
Normal file
@@ -0,0 +1,7 @@
|
||||
pub const reader = @import("minidump/reader.zig");
|
||||
pub const stream = @import("minidump/stream.zig");
|
||||
pub const Reader = reader.Reader;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
||||
59
src/crash/minidump/external.zig
Normal file
59
src/crash/minidump/external.zig
Normal file
@@ -0,0 +1,59 @@
|
||||
//! This file contains the external structs and constants for the minidump
|
||||
//! format. Most are from the Microsoft documentation on the minidump format:
|
||||
//! https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/
|
||||
//!
|
||||
//! Wherever possible, we also compare our definitions to other projects
|
||||
//! such as rust-minidump, libmdmp, breakpad, etc. to ensure we're doing
|
||||
//! the right thing.
|
||||
|
||||
/// "MDMP" in little-endian.
|
||||
pub const signature = 0x504D444D;
|
||||
|
||||
/// The version of the minidump format.
|
||||
pub const version = 0xA793;
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_header
|
||||
pub const Header = extern struct {
|
||||
signature: u32,
|
||||
version: packed struct(u32) { low: u16, high: u16 },
|
||||
stream_count: u32,
|
||||
stream_directory_rva: u32,
|
||||
checksum: u32,
|
||||
time_date_stamp: u32,
|
||||
flags: u64,
|
||||
};
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_directory
|
||||
pub const Directory = extern struct {
|
||||
stream_type: u32,
|
||||
location: LocationDescriptor,
|
||||
};
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_location_descriptor
|
||||
pub const LocationDescriptor = extern struct {
|
||||
data_size: u32,
|
||||
rva: u32,
|
||||
};
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_memory_descriptor
|
||||
pub const MemoryDescriptor = extern struct {
|
||||
start_of_memory_range: u64,
|
||||
memory: LocationDescriptor,
|
||||
};
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_thread_list
|
||||
pub const ThreadList = extern struct {
|
||||
number_of_threads: u32,
|
||||
threads: [1]Thread,
|
||||
};
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_thread
|
||||
pub const Thread = extern struct {
|
||||
thread_id: u32,
|
||||
suspend_count: u32,
|
||||
priority_class: u32,
|
||||
priority: u32,
|
||||
teb: u64,
|
||||
stack: MemoryDescriptor,
|
||||
thread_context: LocationDescriptor,
|
||||
};
|
||||
242
src/crash/minidump/reader.zig
Normal file
242
src/crash/minidump/reader.zig
Normal file
@@ -0,0 +1,242 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const external = @import("external.zig");
|
||||
const stream = @import("stream.zig");
|
||||
const EncodedStream = stream.EncodedStream;
|
||||
|
||||
const log = std.log.scoped(.minidump_reader);
|
||||
|
||||
/// Possible minidump-specific errors that can occur when reading a minidump.
|
||||
/// This isn't the full error set since IO errors can also occur depending
|
||||
/// on the Source type.
|
||||
pub const ReadError = error{
|
||||
InvalidHeader,
|
||||
InvalidVersion,
|
||||
StreamSizeMismatch,
|
||||
};
|
||||
|
||||
/// Reader creates a new minidump reader for the given source type. The
|
||||
/// source must have both a "reader()" and "seekableStream()" function.
|
||||
///
|
||||
/// Given the format of a minidump file, we must keep the source open and
|
||||
/// continually access it because the format of the minidump is full of
|
||||
/// pointers and offsets that we must follow depending on the stream types.
|
||||
/// Also, since we're not aware of all stream types (in fact its impossible
|
||||
/// to be aware since custom stream types are allowed), its possible any stream
|
||||
/// type can define their own pointers and offsets. So, the source must always
|
||||
/// be available so callers can decode the streams as needed.
|
||||
pub fn Reader(comptime S: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
/// The source data.
|
||||
source: Source,
|
||||
|
||||
/// The endianness of the minidump file. This is detected by reading
|
||||
/// the byte order of the header.
|
||||
endian: std.builtin.Endian,
|
||||
|
||||
/// The number of streams within the minidump file. This is read from
|
||||
/// the header and stored here so we can quickly access them. Note
|
||||
/// the stream types require reading the source; this is an optimization
|
||||
/// to avoid any allocations on the reader and the caller can choose
|
||||
/// to store them if they want.
|
||||
stream_count: u32,
|
||||
stream_directory_rva: u32,
|
||||
|
||||
const SourceCallable = switch (@typeInfo(Source)) {
|
||||
.Pointer => |v| v.child,
|
||||
.Struct => Source,
|
||||
else => @compileError("Source type must be a pointer or struct"),
|
||||
};
|
||||
|
||||
const SourceReader = @typeInfo(@TypeOf(SourceCallable.reader)).Fn.return_type.?;
|
||||
const SourceSeeker = @typeInfo(@TypeOf(SourceCallable.seekableStream)).Fn.return_type.?;
|
||||
|
||||
/// A limited reader for reading data from the source.
|
||||
pub const LimitedReader = std.io.LimitedReader(SourceReader);
|
||||
|
||||
/// The source type for the reader.
|
||||
pub const Source = S;
|
||||
|
||||
/// The stream types for reading
|
||||
pub const ThreadList = stream.thread_list.ThreadListReader(Self);
|
||||
|
||||
/// The reader type for stream reading. This has some other methods so
|
||||
/// you must still call reader() on the result to get the actual
|
||||
/// reader to read the data.
|
||||
pub const StreamReader = struct {
|
||||
source: Source,
|
||||
endian: std.builtin.Endian,
|
||||
directory: external.Directory,
|
||||
|
||||
/// Should not be accessed directly. This is setup whenever
|
||||
/// reader() is called.
|
||||
limit_reader: LimitedReader = undefined,
|
||||
|
||||
pub const Reader = LimitedReader.Reader;
|
||||
|
||||
/// Returns a Reader implementation that reads the bytes of the
|
||||
/// stream.
|
||||
///
|
||||
/// The reader is dependent on the state of Source so any
|
||||
/// state-changing operations on Source will invalidate the
|
||||
/// reader. For example, making another reader, reading another
|
||||
/// stream directory, closing the source, etc.
|
||||
pub fn reader(self: *StreamReader) LimitedReader.Reader {
|
||||
try self.source.seekableStream().seekTo(self.directory.location.rva);
|
||||
self.limit_reader = .{
|
||||
.inner_reader = self.source.reader(),
|
||||
.bytes_left = self.directory.location.data_size,
|
||||
};
|
||||
return self.limit_reader.reader();
|
||||
}
|
||||
|
||||
/// Seeks the source to the location of the directory.
|
||||
pub fn seekToPayload(self: *StreamReader) !void {
|
||||
try self.source.seekableStream().seekTo(self.directory.location.rva);
|
||||
}
|
||||
};
|
||||
|
||||
/// Iterator type to read over the streams in the minidump file.
|
||||
pub const StreamIterator = struct {
|
||||
reader: *const Self,
|
||||
i: u32 = 0,
|
||||
|
||||
pub fn next(self: *StreamIterator) !?StreamReader {
|
||||
if (self.i >= self.reader.stream_count) return null;
|
||||
const dir = try self.reader.directory(self.i);
|
||||
self.i += 1;
|
||||
return try self.reader.streamReader(dir);
|
||||
}
|
||||
};
|
||||
|
||||
/// Initialize a reader. The source must remain available for the entire
|
||||
/// lifetime of the reader. The reader does not take ownership of the
|
||||
/// source so if it has resources that need to be cleaned up, the caller
|
||||
/// must do so once the reader is no longer needed.
|
||||
pub fn init(source: Source) !Self {
|
||||
const header, const endian = try readHeader(Source, source);
|
||||
return .{
|
||||
.source = source,
|
||||
.endian = endian,
|
||||
.stream_count = header.stream_count,
|
||||
.stream_directory_rva = header.stream_directory_rva,
|
||||
};
|
||||
}
|
||||
|
||||
/// Return an iterator to read over the streams in the minidump file.
|
||||
/// This is very similar to using a simple for loop to stream_count
|
||||
/// and calling directory() on each index, but is more idiomatic
|
||||
/// Zig.
|
||||
pub fn streamIterator(self: *const Self) StreamIterator {
|
||||
return .{ .reader = self };
|
||||
}
|
||||
|
||||
/// Return a StreamReader for the given directory type. This streams
|
||||
/// from the underlying source so the returned reader is only valid
|
||||
/// as long as the source is unmodified (i.e. the source is not
|
||||
/// closed, the source seek position is not moved, etc.).
|
||||
pub fn streamReader(
|
||||
self: *const Self,
|
||||
dir: external.Directory,
|
||||
) SourceSeeker.SeekError!StreamReader {
|
||||
return .{
|
||||
.source = self.source,
|
||||
.endian = self.endian,
|
||||
.directory = dir,
|
||||
};
|
||||
}
|
||||
|
||||
/// Get the directory entry with the given index.
|
||||
///
|
||||
/// Asserts the index is valid (idx < stream_count).
|
||||
pub fn directory(self: *const Self, idx: usize) !external.Directory {
|
||||
assert(idx < self.stream_count);
|
||||
|
||||
// Seek to the directory.
|
||||
const offset: u32 = @intCast(@sizeOf(external.Directory) * idx);
|
||||
const rva: u32 = self.stream_directory_rva + offset;
|
||||
try self.source.seekableStream().seekTo(rva);
|
||||
|
||||
// Read the directory.
|
||||
return try self.source.reader().readStructEndian(
|
||||
external.Directory,
|
||||
self.endian,
|
||||
);
|
||||
}
|
||||
|
||||
/// Return a reader for the given location descriptor. This is only
|
||||
/// valid until the reader source is modified in some way.
|
||||
pub fn locationReader(
|
||||
self: *const Self,
|
||||
loc: external.LocationDescriptor,
|
||||
) !LimitedReader {
|
||||
try self.source.seekableStream().seekTo(loc.rva);
|
||||
return .{
|
||||
.inner_reader = self.source.reader(),
|
||||
.bytes_left = loc.data_size,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Reads the header for the minidump file and returns endianness of
|
||||
/// the file.
|
||||
fn readHeader(comptime T: type, source: T) !struct {
|
||||
external.Header,
|
||||
std.builtin.Endian,
|
||||
} {
|
||||
// Start by trying LE.
|
||||
var endian: std.builtin.Endian = .little;
|
||||
var header = try source.reader().readStructEndian(external.Header, endian);
|
||||
|
||||
// If the signature doesn't match, we assume its BE.
|
||||
if (header.signature != external.signature) {
|
||||
// Seek back to the start of the file so we can reread.
|
||||
try source.seekableStream().seekTo(0);
|
||||
|
||||
// Try BE, if the signature doesn't match, return an error.
|
||||
endian = .big;
|
||||
header = try source.reader().readStructEndian(external.Header, endian);
|
||||
if (header.signature != external.signature) return ReadError.InvalidHeader;
|
||||
}
|
||||
|
||||
// "The low-order word is MINIDUMP_VERSION. The high-order word is an
|
||||
// internal value that is implementation specific."
|
||||
if (header.version.low != external.version) return ReadError.InvalidVersion;
|
||||
|
||||
return .{ header, endian };
|
||||
}
|
||||
|
||||
// Uncomment to dump some debug information for a minidump file.
|
||||
test "minidump debug" {
|
||||
var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp"));
|
||||
const r = try Reader(*@TypeOf(fbs)).init(&fbs);
|
||||
var it = r.streamIterator();
|
||||
while (try it.next()) |s| {
|
||||
log.warn("directory i={} dir={}", .{ it.i - 1, s.directory });
|
||||
}
|
||||
}
|
||||
|
||||
test "minidump read" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp"));
|
||||
const r = try Reader(*@TypeOf(fbs)).init(&fbs);
|
||||
try testing.expectEqual(std.builtin.Endian.little, r.endian);
|
||||
try testing.expectEqual(7, r.stream_count);
|
||||
{
|
||||
const dir = try r.directory(0);
|
||||
try testing.expectEqual(3, dir.stream_type);
|
||||
try testing.expectEqual(584, dir.location.data_size);
|
||||
|
||||
var bytes = std.ArrayList(u8).init(alloc);
|
||||
defer bytes.deinit();
|
||||
var sr = try r.streamReader(dir);
|
||||
try sr.reader().readAllArrayList(&bytes, std.math.maxInt(usize));
|
||||
try testing.expectEqual(584, bytes.items.len);
|
||||
}
|
||||
}
|
||||
30
src/crash/minidump/stream.zig
Normal file
30
src/crash/minidump/stream.zig
Normal file
@@ -0,0 +1,30 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = std.log.scoped(.minidump_stream);
|
||||
|
||||
/// The known stream types.
|
||||
pub const thread_list = @import("stream_threadlist.zig");
|
||||
|
||||
/// A stream within the minidump file. A stream can be either in an encoded
|
||||
/// form or decoded form. The encoded form are raw bytes and aren't validated
|
||||
/// until they're decoded. The decoded form is a structured form of the stream.
|
||||
///
|
||||
/// The decoded form is more ergonomic to work with but the encoded form is
|
||||
/// more efficient to read/write.
|
||||
pub const Stream = union(enum) {
|
||||
encoded: EncodedStream,
|
||||
};
|
||||
|
||||
/// An encoded stream value. It is "encoded" in the sense that it is raw bytes
|
||||
/// with a type associated. The raw bytes are not validated to be correct for
|
||||
/// the type.
|
||||
pub const EncodedStream = struct {
|
||||
type: u32,
|
||||
data: []const u8,
|
||||
};
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
||||
117
src/crash/minidump/stream_threadlist.zig
Normal file
117
src/crash/minidump/stream_threadlist.zig
Normal file
@@ -0,0 +1,117 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const external = @import("external.zig");
|
||||
const readerpkg = @import("reader.zig");
|
||||
const Reader = readerpkg.Reader;
|
||||
const ReadError = readerpkg.ReadError;
|
||||
|
||||
const log = std.log.scoped(.minidump_stream);
|
||||
|
||||
/// This is the list of threads from the process.
|
||||
///
|
||||
/// This is the Reader implementation. You usually do not use this directly.
|
||||
/// Instead, use Reader(T).ThreadList which will get you the same thing.
|
||||
///
|
||||
/// ThreadList is stream type 0x3.
|
||||
/// StreamReader is the Reader(T).StreamReader type.
|
||||
pub fn ThreadListReader(comptime R: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
/// The number of threads in the list.
|
||||
count: u32,
|
||||
|
||||
/// The rva to the first thread in the list.
|
||||
rva: u32,
|
||||
|
||||
/// Source data and endianness so we can read.
|
||||
source: R.Source,
|
||||
endian: std.builtin.Endian,
|
||||
|
||||
pub fn init(r: *R.StreamReader) !Self {
|
||||
assert(r.directory.stream_type == 0x3);
|
||||
try r.seekToPayload();
|
||||
const reader = r.source.reader();
|
||||
|
||||
// Our count is always a u32 in the header.
|
||||
const count = try reader.readInt(u32, r.endian);
|
||||
|
||||
// Determine if we have padding in our header. It is possible
|
||||
// for there to be padding if the list header was written by
|
||||
// a 32-bit process but is being read on a 64-bit process.
|
||||
const padding = padding: {
|
||||
const maybe_size = @sizeOf(u32) + (@sizeOf(external.Thread) * count);
|
||||
switch (std.math.order(maybe_size, r.directory.location.data_size)) {
|
||||
// It should never be larger than what the directory says.
|
||||
.gt => return ReadError.StreamSizeMismatch,
|
||||
|
||||
// If the sizes match exactly we're good.
|
||||
.eq => break :padding 0,
|
||||
|
||||
.lt => {
|
||||
const padding = r.directory.location.data_size - maybe_size;
|
||||
if (padding != 4) return ReadError.StreamSizeMismatch;
|
||||
break :padding padding;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// Rva is the location of the first thread in the list.
|
||||
const rva = r.directory.location.rva + @as(u32, @sizeOf(u32)) + padding;
|
||||
|
||||
return .{
|
||||
.count = count,
|
||||
.rva = rva,
|
||||
.source = r.source,
|
||||
.endian = r.endian,
|
||||
};
|
||||
}
|
||||
|
||||
/// Get the thread entry for the given index.
|
||||
///
|
||||
/// Index is asserted to be less than count.
|
||||
pub fn thread(self: *const Self, i: usize) !external.Thread {
|
||||
assert(i < self.count);
|
||||
|
||||
// Seek to the thread
|
||||
const offset: u32 = @intCast(@sizeOf(external.Thread) * i);
|
||||
const rva: u32 = self.rva + offset;
|
||||
try self.source.seekableStream().seekTo(rva);
|
||||
|
||||
// Read the thread
|
||||
return try self.source.reader().readStructEndian(
|
||||
external.Thread,
|
||||
self.endian,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test "minidump: threadlist" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var fbs = std.io.fixedBufferStream(@embedFile("../testdata/macos.dmp"));
|
||||
const R = Reader(*@TypeOf(fbs));
|
||||
const r = try R.init(&fbs);
|
||||
|
||||
// Get our thread list stream
|
||||
const dir = try r.directory(0);
|
||||
try testing.expectEqual(3, dir.stream_type);
|
||||
var sr = try r.streamReader(dir);
|
||||
|
||||
// Get our rich structure
|
||||
const v = try R.ThreadList.init(&sr);
|
||||
log.warn("threadlist count={} rva={}", .{ v.count, v.rva });
|
||||
|
||||
try testing.expectEqual(12, v.count);
|
||||
for (0..v.count) |i| {
|
||||
const t = try v.thread(i);
|
||||
log.warn("thread i={} thread={}", .{ i, t });
|
||||
|
||||
// Read our stack memory
|
||||
var stack_reader = try r.locationReader(t.stack.memory);
|
||||
const bytes = try stack_reader.reader().readAllAlloc(alloc, t.stack.memory.data_size);
|
||||
defer alloc.free(bytes);
|
||||
}
|
||||
}
|
||||
BIN
src/crash/testdata/macos.dmp
vendored
Normal file
BIN
src/crash/testdata/macos.dmp
vendored
Normal file
Binary file not shown.
@@ -594,30 +594,37 @@ pub const Face = struct {
|
||||
// All of these metrics are based on our layout above.
|
||||
const cell_height = @ceil(layout_metrics.height);
|
||||
const cell_baseline = @ceil(layout_metrics.height - layout_metrics.ascent);
|
||||
const underline_thickness = @ceil(@as(f32, @floatCast(ct_font.getUnderlineThickness())));
|
||||
const strikethrough_position = strikethrough_position: {
|
||||
// This is the height above baseline consumed by text. We must take
|
||||
// into account that our cell height splits the leading between two
|
||||
// rows so we subtract leading space (blank space).
|
||||
const above_baseline = layout_metrics.ascent - (layout_metrics.leading / 2);
|
||||
|
||||
// We want to position the strikethrough at 65% of the height.
|
||||
// This generally gives a nice visual appearance. The number 65%
|
||||
// is somewhat arbitrary but is a common value across terminals.
|
||||
const pos = above_baseline * 0.65;
|
||||
const underline_thickness = @ceil(@as(f32, @floatCast(ct_font.getUnderlineThickness())));
|
||||
const strikethrough_thickness = underline_thickness;
|
||||
|
||||
const strikethrough_position = strikethrough_position: {
|
||||
// This is the height of lower case letters in our font.
|
||||
const ex_height = ct_font.getXHeight();
|
||||
|
||||
// We want to position the strikethrough so that it's
|
||||
// vertically centered on any lower case text. This is
|
||||
// a fairly standard choice for strikethrough positioning.
|
||||
//
|
||||
// Because our `strikethrough_position` is relative to the
|
||||
// top of the cell we start with the ascent metric, which
|
||||
// is the distance from the top down to the baseline, then
|
||||
// we subtract half of the ex height to go back up to the
|
||||
// correct height that should evenly split lowercase text.
|
||||
const pos = layout_metrics.ascent -
|
||||
ex_height * 0.5 -
|
||||
strikethrough_thickness * 0.5;
|
||||
|
||||
break :strikethrough_position @ceil(pos);
|
||||
};
|
||||
const strikethrough_thickness = underline_thickness;
|
||||
|
||||
// Underline position reported is usually something like "-1" to
|
||||
// represent the amount under the baseline. We add this to our real
|
||||
// baseline to get the actual value from the bottom (+y is up).
|
||||
// The final underline position is +y from the TOP (confusing)
|
||||
// so we have to subtract from the cell height.
|
||||
const underline_position = cell_height -
|
||||
(cell_baseline + @ceil(@as(f32, @floatCast(ct_font.getUnderlinePosition())))) +
|
||||
1;
|
||||
const underline_position = @ceil(layout_metrics.ascent -
|
||||
@as(f32, @floatCast(ct_font.getUnderlinePosition())));
|
||||
|
||||
// Note: is this useful?
|
||||
// const units_per_em = ct_font.getUnitsPerEm();
|
||||
|
||||
@@ -607,6 +607,20 @@ pub const Face = struct {
|
||||
break :cell_width f26dot6ToFloat(size_metrics.max_advance);
|
||||
};
|
||||
|
||||
// Ex height is calculated by measuring the height of the `x` glyph.
|
||||
// If that fails then we just pretend it's 65% of the ascent height.
|
||||
const ex_height: f32 = ex_height: {
|
||||
if (face.getCharIndex('x')) |glyph_index| {
|
||||
if (face.loadGlyph(glyph_index, .{ .render = true })) {
|
||||
break :ex_height f26dot6ToFloat(face.handle.*.glyph.*.metrics.height);
|
||||
} else |_| {
|
||||
// Ignore the error since we just fall back to 65% of the ascent below
|
||||
}
|
||||
}
|
||||
|
||||
break :ex_height f26dot6ToFloat(size_metrics.ascender) * 0.65;
|
||||
};
|
||||
|
||||
// Cell height is calculated as the maximum of multiple things in order
|
||||
// to handle edge cases in fonts: (1) the height as reported in metadata
|
||||
// by the font designer (2) the maximum glyph height as measured in the
|
||||
@@ -646,50 +660,55 @@ pub const Face = struct {
|
||||
// is reversed.
|
||||
const cell_baseline = -1 * f26dot6ToFloat(size_metrics.descender);
|
||||
|
||||
const underline_thickness = @max(@as(f32, 1), fontUnitsToPxY(
|
||||
face,
|
||||
face.handle.*.underline_thickness,
|
||||
));
|
||||
|
||||
// The underline position. This is a value from the top where the
|
||||
// underline should go.
|
||||
const underline_position: f32 = underline_pos: {
|
||||
// The ascender is already scaled for scalable fonts, but the
|
||||
// underline position is not.
|
||||
const ascender_px = @as(i32, @intCast(size_metrics.ascender)) >> 6;
|
||||
const declared_px = freetype.mulFix(
|
||||
// From the FreeType docs:
|
||||
// > `underline_position`
|
||||
// > The position, in font units, of the underline line for
|
||||
// > this face. It is the center of the underlining stem.
|
||||
|
||||
const declared_px = @as(f32, @floatFromInt(freetype.mulFix(
|
||||
face.handle.*.underline_position,
|
||||
@intCast(face.handle.*.size.*.metrics.y_scale),
|
||||
) >> 6;
|
||||
))) / 64;
|
||||
|
||||
// We use the declared underline position if its available
|
||||
const declared = ascender_px - declared_px;
|
||||
// We use the declared underline position if its available.
|
||||
const declared = @ceil(cell_height - cell_baseline - declared_px - underline_thickness * 0.5);
|
||||
if (declared > 0)
|
||||
break :underline_pos @floatFromInt(declared);
|
||||
break :underline_pos declared;
|
||||
|
||||
// If we have no declared underline position, we go slightly under the
|
||||
// cell height (mainly: non-scalable fonts, i.e. emoji)
|
||||
break :underline_pos cell_height - 1;
|
||||
};
|
||||
const underline_thickness = @max(@as(f32, 1), fontUnitsToPxY(
|
||||
face,
|
||||
face.handle.*.underline_thickness,
|
||||
));
|
||||
|
||||
// The strikethrough position. We use the position provided by the
|
||||
// font if it exists otherwise we calculate a best guess.
|
||||
const strikethrough: struct {
|
||||
pos: f32,
|
||||
thickness: f32,
|
||||
} = if (face.getSfntTable(.os2)) |os2| .{
|
||||
.pos = pos: {
|
||||
// Ascender is scaled, strikeout pos is not
|
||||
const ascender_px = @as(i32, @intCast(size_metrics.ascender)) >> 6;
|
||||
const declared_px = freetype.mulFix(
|
||||
os2.yStrikeoutPosition,
|
||||
@as(i32, @intCast(face.handle.*.size.*.metrics.y_scale)),
|
||||
) >> 6;
|
||||
} = if (face.getSfntTable(.os2)) |os2| st: {
|
||||
const thickness = @max(@as(f32, 1), fontUnitsToPxY(face, os2.yStrikeoutSize));
|
||||
|
||||
break :pos @floatFromInt(ascender_px - declared_px);
|
||||
},
|
||||
.thickness = @max(@as(f32, 1), fontUnitsToPxY(face, os2.yStrikeoutSize)),
|
||||
const pos = @as(f32, @floatFromInt(freetype.mulFix(
|
||||
os2.yStrikeoutPosition,
|
||||
@as(i32, @intCast(face.handle.*.size.*.metrics.y_scale)),
|
||||
))) / 64;
|
||||
|
||||
break :st .{
|
||||
.pos = @ceil(cell_height - cell_baseline - pos),
|
||||
.thickness = thickness,
|
||||
};
|
||||
} else .{
|
||||
.pos = cell_baseline * 0.6,
|
||||
// Exactly 50% of the ex height so that our strikethrough is
|
||||
// centered through lowercase text. This is a common choice.
|
||||
.pos = @ceil(cell_height - cell_baseline - ex_height * 0.5 - underline_thickness * 0.5),
|
||||
.thickness = underline_thickness,
|
||||
};
|
||||
|
||||
@@ -832,7 +851,7 @@ test "metrics" {
|
||||
.cell_width = 16,
|
||||
.cell_height = 35,
|
||||
.cell_baseline = 7,
|
||||
.underline_position = 36,
|
||||
.underline_position = 35,
|
||||
.underline_thickness = 2,
|
||||
.strikethrough_position = 20,
|
||||
.strikethrough_thickness = 2,
|
||||
|
||||
@@ -27,189 +27,174 @@ pub fn renderGlyph(
|
||||
line_pos: u32,
|
||||
line_thickness: u32,
|
||||
) !font.Glyph {
|
||||
// Create the canvas we'll use to draw. We draw the underline in
|
||||
// a full cell size and position it according to "pos".
|
||||
var canvas = try font.sprite.Canvas.init(alloc, width, height);
|
||||
// Draw the appropriate sprite
|
||||
var canvas: font.sprite.Canvas, const offset_y: i32 = switch (sprite) {
|
||||
.underline => try drawSingle(alloc, width, line_thickness),
|
||||
.underline_double => try drawDouble(alloc, width, line_thickness),
|
||||
.underline_dotted => try drawDotted(alloc, width, line_thickness),
|
||||
.underline_dashed => try drawDashed(alloc, width, line_thickness),
|
||||
.underline_curly => try drawCurly(alloc, width, line_thickness),
|
||||
.strikethrough => try drawSingle(alloc, width, line_thickness),
|
||||
else => unreachable,
|
||||
};
|
||||
defer canvas.deinit(alloc);
|
||||
|
||||
// Perform the actual drawing
|
||||
(Draw{
|
||||
.width = width,
|
||||
.height = height,
|
||||
.pos = line_pos,
|
||||
.thickness = line_thickness,
|
||||
}).draw(&canvas, sprite);
|
||||
|
||||
// Write the drawing to the atlas
|
||||
const region = try canvas.writeAtlas(alloc, atlas);
|
||||
|
||||
// Our coordinates start at the BOTTOM for our renderers so we have to
|
||||
// specify an offset of the full height because we rendered a full size
|
||||
// cell.
|
||||
const offset_y = @as(i32, @intCast(height));
|
||||
|
||||
return font.Glyph{
|
||||
.width = width,
|
||||
.height = height,
|
||||
.height = @intCast(region.height),
|
||||
.offset_x = 0,
|
||||
.offset_y = offset_y,
|
||||
// Glyph.offset_y is the distance between the top of the glyph and the
|
||||
// bottom of the cell. We want the top of the glyph to be at line_pos
|
||||
// from the TOP of the cell, and then offset by the offset_y from the
|
||||
// draw function.
|
||||
.offset_y = @as(i32, @intCast(height - line_pos)) - offset_y,
|
||||
.atlas_x = region.x,
|
||||
.atlas_y = region.y,
|
||||
.advance_x = @floatFromInt(width),
|
||||
};
|
||||
}
|
||||
|
||||
/// Stores drawing state.
|
||||
const Draw = struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
pos: u32,
|
||||
thickness: u32,
|
||||
/// A tuple with the canvas that the desired sprite was drawn on and
|
||||
/// a recommended offset (+Y = down) to shift its Y position by, to
|
||||
/// correct for underline styles with additional thickness.
|
||||
const CanvasAndOffset = struct { font.sprite.Canvas, i32 };
|
||||
|
||||
/// Draw a specific underline sprite to the canvas.
|
||||
fn draw(self: Draw, canvas: *font.sprite.Canvas, sprite: Sprite) void {
|
||||
switch (sprite) {
|
||||
.underline => self.drawSingle(canvas),
|
||||
.underline_double => self.drawDouble(canvas),
|
||||
.underline_dotted => self.drawDotted(canvas),
|
||||
.underline_dashed => self.drawDashed(canvas),
|
||||
.underline_curly => self.drawCurly(canvas),
|
||||
.strikethrough => self.drawSingle(canvas),
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
/// Draw a single underline.
|
||||
fn drawSingle(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
|
||||
const height: u32 = thickness;
|
||||
var canvas = try font.sprite.Canvas.init(alloc, width, height);
|
||||
|
||||
/// Draw a single underline.
|
||||
fn drawSingle(self: Draw, canvas: *font.sprite.Canvas) void {
|
||||
// Ensure we never overflow out of bounds on the canvas
|
||||
const y_max = self.height -| 1;
|
||||
const bottom = @min(self.pos + self.thickness, y_max);
|
||||
const y = bottom -| self.thickness;
|
||||
const max_height = self.height - y;
|
||||
canvas.rect(.{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = width,
|
||||
.height = thickness,
|
||||
}, .on);
|
||||
|
||||
const offset_y: i32 = 0;
|
||||
|
||||
return .{ canvas, offset_y };
|
||||
}
|
||||
|
||||
/// Draw a double underline.
|
||||
fn drawDouble(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
|
||||
const height: u32 = thickness * 3;
|
||||
var canvas = try font.sprite.Canvas.init(alloc, width, height);
|
||||
|
||||
canvas.rect(.{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.width = width,
|
||||
.height = thickness,
|
||||
}, .on);
|
||||
|
||||
canvas.rect(.{
|
||||
.x = 0,
|
||||
.y = @intCast(thickness * 2),
|
||||
.width = width,
|
||||
.height = thickness,
|
||||
}, .on);
|
||||
|
||||
const offset_y: i32 = -@as(i32, @intCast(thickness));
|
||||
|
||||
return .{ canvas, offset_y };
|
||||
}
|
||||
|
||||
/// Draw a dotted underline.
|
||||
fn drawDotted(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
|
||||
const height: u32 = thickness;
|
||||
var canvas = try font.sprite.Canvas.init(alloc, width, height);
|
||||
|
||||
const dot_width = @max(thickness, 3);
|
||||
const dot_count = @max((width / dot_width) / 2, 1);
|
||||
const gap_width = try std.math.divCeil(u32, width -| (dot_count * dot_width), dot_count);
|
||||
var i: u32 = 0;
|
||||
while (i < dot_count) : (i += 1) {
|
||||
// Ensure we never go out of bounds for the rect
|
||||
const x = @min(i * (dot_width + gap_width), width - 1);
|
||||
const rect_width = @min(width - x, dot_width);
|
||||
canvas.rect(.{
|
||||
.x = 0,
|
||||
.y = @intCast(y),
|
||||
.width = self.width,
|
||||
.height = @min(self.thickness, max_height),
|
||||
.x = @intCast(x),
|
||||
.y = 0,
|
||||
.width = rect_width,
|
||||
.height = thickness,
|
||||
}, .on);
|
||||
}
|
||||
|
||||
/// Draw a double underline.
|
||||
fn drawDouble(self: Draw, canvas: *font.sprite.Canvas) void {
|
||||
// The maximum y value has to have space for the bottom underline.
|
||||
// If we underflow (saturated) to 0, then we don't draw. This should
|
||||
// never happen but we don't want to draw something undefined.
|
||||
const y_max = self.height -| 1 -| self.thickness;
|
||||
if (y_max == 0) return;
|
||||
const offset_y: i32 = 0;
|
||||
|
||||
const space = self.thickness * 2;
|
||||
const bottom = @min(self.pos + space, y_max);
|
||||
const top = bottom - space;
|
||||
return .{ canvas, offset_y };
|
||||
}
|
||||
|
||||
/// Draw a dashed underline.
|
||||
fn drawDashed(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
|
||||
const height: u32 = thickness;
|
||||
var canvas = try font.sprite.Canvas.init(alloc, width, height);
|
||||
|
||||
const dash_width = width / 3 + 1;
|
||||
const dash_count = (width / dash_width) + 1;
|
||||
var i: u32 = 0;
|
||||
while (i < dash_count) : (i += 2) {
|
||||
// Ensure we never go out of bounds for the rect
|
||||
const x = @min(i * dash_width, width - 1);
|
||||
const rect_width = @min(width - x, dash_width);
|
||||
canvas.rect(.{
|
||||
.x = 0,
|
||||
.y = @intCast(top),
|
||||
.width = self.width,
|
||||
.height = self.thickness,
|
||||
}, .on);
|
||||
|
||||
canvas.rect(.{
|
||||
.x = 0,
|
||||
.y = @intCast(bottom),
|
||||
.width = self.width,
|
||||
.height = self.thickness,
|
||||
.x = @intCast(x),
|
||||
.y = 0,
|
||||
.width = rect_width,
|
||||
.height = thickness,
|
||||
}, .on);
|
||||
}
|
||||
|
||||
/// Draw a dotted underline.
|
||||
fn drawDotted(self: Draw, canvas: *font.sprite.Canvas) void {
|
||||
const y_max = self.height -| 1 -| self.thickness;
|
||||
if (y_max == 0) return;
|
||||
const y = @min(self.pos, y_max);
|
||||
const dot_width = @max(self.thickness, 3);
|
||||
const dot_count = self.width / dot_width;
|
||||
var i: u32 = 0;
|
||||
while (i < dot_count) : (i += 2) {
|
||||
// Ensure we never go out of bounds for the rect
|
||||
const x = @min(i * dot_width, self.width - 1);
|
||||
const width = @min(self.width - 1 - x, dot_width);
|
||||
canvas.rect(.{
|
||||
.x = @intCast(i * dot_width),
|
||||
.y = @intCast(y),
|
||||
.width = width,
|
||||
.height = self.thickness,
|
||||
}, .on);
|
||||
const offset_y: i32 = 0;
|
||||
|
||||
return .{ canvas, offset_y };
|
||||
}
|
||||
|
||||
/// Draw a curly underline. Thanks to Wez Furlong for providing
|
||||
/// the basic math structure for this since I was lazy with the
|
||||
/// geometry.
|
||||
fn drawCurly(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset {
|
||||
const height: u32 = thickness * 4;
|
||||
var canvas = try font.sprite.Canvas.init(alloc, width, height);
|
||||
|
||||
// Calculate the wave period for a single character
|
||||
// `2 * pi...` = 1 peak per character
|
||||
// `4 * pi...` = 2 peaks per character
|
||||
const wave_period = 2 * std.math.pi / @as(f64, @floatFromInt(width - 1));
|
||||
|
||||
// The full amplitude of the wave can be from the bottom to the
|
||||
// underline position. We also calculate our mid y point of the wave
|
||||
const half_amplitude: f64 = @as(f64, @floatFromInt(thickness));
|
||||
const y_mid: f64 = half_amplitude + 1;
|
||||
|
||||
// follow Xiaolin Wu's antialias algorithm to draw the curve
|
||||
var x: u32 = 0;
|
||||
while (x < width) : (x += 1) {
|
||||
const cosx: f64 = @cos(@as(f64, @floatFromInt(x)) * wave_period);
|
||||
const y: f64 = y_mid + half_amplitude * cosx;
|
||||
const y_upper: u32 = @intFromFloat(@floor(y));
|
||||
const y_lower: u32 = y_upper + thickness + (thickness >> 1);
|
||||
const alpha: u8 = @intFromFloat(255 * @abs(y - @floor(y)));
|
||||
|
||||
// upper and lower bounds
|
||||
canvas.pixel(x, @min(y_upper, height), @enumFromInt(255 - alpha));
|
||||
canvas.pixel(x, @min(y_lower, height), @enumFromInt(alpha));
|
||||
|
||||
// fill between upper and lower bound
|
||||
var y_fill: u32 = y_upper + 1;
|
||||
while (y_fill < y_lower) : (y_fill += 1) {
|
||||
canvas.pixel(x, @min(y_fill, height), .on);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a dashed underline.
|
||||
fn drawDashed(self: Draw, canvas: *font.sprite.Canvas) void {
|
||||
const y_max = self.height -| 1 -| self.thickness;
|
||||
if (y_max == 0) return;
|
||||
const y = @min(self.pos, y_max);
|
||||
const dash_width = self.width / 3 + 1;
|
||||
const dash_count = (self.width / dash_width) + 1;
|
||||
var i: u32 = 0;
|
||||
while (i < dash_count) : (i += 2) {
|
||||
// Ensure we never go out of bounds for the rect
|
||||
const x = @min(i * dash_width, self.width - 1);
|
||||
const width = @min(self.width - 1 - x, dash_width);
|
||||
canvas.rect(.{
|
||||
.x = @intCast(x),
|
||||
.y = @intCast(y),
|
||||
.width = width,
|
||||
.height = self.thickness,
|
||||
}, .on);
|
||||
}
|
||||
}
|
||||
const offset_y: i32 = -@as(i32, @intCast(thickness * 2));
|
||||
|
||||
/// Draw a curly underline. Thanks to Wez Furlong for providing
|
||||
/// the basic math structure for this since I was lazy with the
|
||||
/// geometry.
|
||||
fn drawCurly(self: Draw, canvas: *font.sprite.Canvas) void {
|
||||
// This is the lowest that the curl can go.
|
||||
const y_max = self.height - 1;
|
||||
|
||||
// Calculate the wave period for a single character
|
||||
// `2 * pi...` = 1 peak per character
|
||||
// `4 * pi...` = 2 peaks per character
|
||||
const wave_period = 2 * std.math.pi / @as(f64, @floatFromInt(self.width - 1));
|
||||
|
||||
// Some fonts put the underline too close to the bottom of the
|
||||
// cell height and this doesn't allow us to make a high enough
|
||||
// wave. This constant is arbitrary, change it for aesthetics.
|
||||
const pos: u32 = pos: {
|
||||
const MIN_AMPLITUDE: u32 = @max(self.height / 9, 2);
|
||||
break :pos y_max - (MIN_AMPLITUDE * 2);
|
||||
};
|
||||
|
||||
// The full amplitude of the wave can be from the bottom to the
|
||||
// underline position. We also calculate our mid y point of the wave
|
||||
const double_amplitude: f64 = @floatFromInt(y_max - pos);
|
||||
const half_amplitude: f64 = @max(1, double_amplitude / 4);
|
||||
const y_mid: u32 = pos + @as(u32, @intFromFloat(2 * half_amplitude));
|
||||
|
||||
// follow Xiaolin Wu's antialias algorithm to draw the curve
|
||||
var x: u32 = 0;
|
||||
while (x < self.width) : (x += 1) {
|
||||
const y: f64 = @as(f64, @floatFromInt(y_mid)) + (half_amplitude * @cos(@as(f64, @floatFromInt(x)) * wave_period));
|
||||
const y_upper: u32 = @intFromFloat(@floor(y));
|
||||
const y_lower: u32 = y_upper + self.thickness;
|
||||
const alpha: u8 = @intFromFloat(255 * @abs(y - @floor(y)));
|
||||
|
||||
// upper and lower bounds
|
||||
canvas.pixel(x, @min(y_upper, y_max), @enumFromInt(255 - alpha));
|
||||
canvas.pixel(x, @min(y_lower, y_max), @enumFromInt(alpha));
|
||||
|
||||
// fill between upper and lower bound
|
||||
var y_fill: u32 = y_upper + 1;
|
||||
while (y_fill < y_lower) : (y_fill += 1) {
|
||||
canvas.pixel(x, @min(y_fill, y_max), .on);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return .{ canvas, offset_y };
|
||||
}
|
||||
|
||||
test "single" {
|
||||
const testing = std.testing;
|
||||
|
||||
@@ -6,6 +6,7 @@ const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const key = @import("key.zig");
|
||||
const KeyEvent = key.KeyEvent;
|
||||
|
||||
/// The trigger that needs to be performed to execute the action.
|
||||
trigger: Trigger,
|
||||
@@ -13,21 +14,36 @@ trigger: Trigger,
|
||||
/// The action to take if this binding matches
|
||||
action: Action,
|
||||
|
||||
/// True if this binding should consume the input when the
|
||||
/// action is triggered.
|
||||
consumed: bool = true,
|
||||
/// Boolean flags that can be set per binding.
|
||||
flags: Flags = .{},
|
||||
|
||||
pub const Error = error{
|
||||
InvalidFormat,
|
||||
InvalidAction,
|
||||
};
|
||||
|
||||
/// Flags the full binding-scoped flags that can be set per binding.
|
||||
pub const Flags = packed struct {
|
||||
/// True if this binding should consume the input when the
|
||||
/// action is triggered.
|
||||
consumed: bool = true,
|
||||
|
||||
/// True if this binding should be forwarded to all active surfaces
|
||||
/// in the application.
|
||||
all: bool = false,
|
||||
|
||||
/// True if this binding is global. Global bindings should work system-wide
|
||||
/// and not just while Ghostty is focused. This may not work on all platforms.
|
||||
/// See the keybind config documentation for more information.
|
||||
global: bool = false,
|
||||
};
|
||||
|
||||
/// Full binding parser. The binding parser is implemented as an iterator
|
||||
/// which yields elements to support multi-key sequences without allocation.
|
||||
pub const Parser = struct {
|
||||
unconsumed: bool = false,
|
||||
trigger_it: SequenceIterator,
|
||||
action: Action,
|
||||
flags: Flags = .{},
|
||||
|
||||
pub const Elem = union(enum) {
|
||||
/// A leader trigger in a sequence.
|
||||
@@ -38,11 +54,7 @@ pub const Parser = struct {
|
||||
};
|
||||
|
||||
pub fn init(raw_input: []const u8) Error!Parser {
|
||||
// If our entire input is prefixed with "unconsumed:" then we are
|
||||
// not consuming this keybind when the action is triggered.
|
||||
const unconsumed_prefix = "unconsumed:";
|
||||
const unconsumed = std.mem.startsWith(u8, raw_input, unconsumed_prefix);
|
||||
const start_idx = if (unconsumed) unconsumed_prefix.len else 0;
|
||||
const flags, const start_idx = try parseFlags(raw_input);
|
||||
const input = raw_input[start_idx..];
|
||||
|
||||
// Find the first = which splits are mapping into the trigger
|
||||
@@ -52,24 +64,63 @@ pub const Parser = struct {
|
||||
// Sequence iterator goes up to the equal, action is after. We can
|
||||
// parse the action now.
|
||||
return .{
|
||||
.unconsumed = unconsumed,
|
||||
.trigger_it = .{ .input = input[0..eql_idx] },
|
||||
.action = try Action.parse(input[eql_idx + 1 ..]),
|
||||
.flags = flags,
|
||||
};
|
||||
}
|
||||
|
||||
fn parseFlags(raw_input: []const u8) Error!struct { Flags, usize } {
|
||||
var flags: Flags = .{};
|
||||
|
||||
var start_idx: usize = 0;
|
||||
var input: []const u8 = raw_input;
|
||||
while (true) {
|
||||
// Find the next prefix
|
||||
const idx = std.mem.indexOf(u8, input, ":") orelse break;
|
||||
const prefix = input[0..idx];
|
||||
|
||||
// If the prefix is one of our flags then set it.
|
||||
if (std.mem.eql(u8, prefix, "all")) {
|
||||
if (flags.all) return Error.InvalidFormat;
|
||||
flags.all = true;
|
||||
} else if (std.mem.eql(u8, prefix, "global")) {
|
||||
if (flags.global) return Error.InvalidFormat;
|
||||
flags.global = true;
|
||||
} else if (std.mem.eql(u8, prefix, "unconsumed")) {
|
||||
if (!flags.consumed) return Error.InvalidFormat;
|
||||
flags.consumed = false;
|
||||
} else {
|
||||
// If we don't recognize the prefix then we're done.
|
||||
// There are trigger-specific prefixes like "physical:" so
|
||||
// this lets us fall into that.
|
||||
break;
|
||||
}
|
||||
|
||||
// Move past the prefix
|
||||
start_idx += idx + 1;
|
||||
input = input[idx + 1 ..];
|
||||
}
|
||||
|
||||
return .{ flags, start_idx };
|
||||
}
|
||||
|
||||
pub fn next(self: *Parser) Error!?Elem {
|
||||
// Get our trigger. If we're out of triggers then we're done.
|
||||
const trigger = (try self.trigger_it.next()) orelse return null;
|
||||
|
||||
// If this is our last trigger then it is our final binding.
|
||||
if (!self.trigger_it.done()) return .{ .leader = trigger };
|
||||
if (!self.trigger_it.done()) {
|
||||
// Global/all bindings can't be sequences
|
||||
if (self.flags.global or self.flags.all) return error.InvalidFormat;
|
||||
return .{ .leader = trigger };
|
||||
}
|
||||
|
||||
// Out of triggers, yield the final action.
|
||||
return .{ .binding = .{
|
||||
.trigger = trigger,
|
||||
.action = self.action,
|
||||
.consumed = !self.unconsumed,
|
||||
.flags = self.flags,
|
||||
} };
|
||||
}
|
||||
|
||||
@@ -228,7 +279,8 @@ pub const Action = union(enum) {
|
||||
/// available values.
|
||||
write_selection_file: WriteScreenAction,
|
||||
|
||||
/// Open a new window.
|
||||
/// Open a new window. If the application isn't currently focused,
|
||||
/// this will bring it to the front.
|
||||
new_window: void,
|
||||
|
||||
/// Open a new tab.
|
||||
@@ -246,6 +298,10 @@ pub const Action = union(enum) {
|
||||
/// Go to the tab with the specific number, 1-indexed.
|
||||
goto_tab: usize,
|
||||
|
||||
/// Toggle the tab overview.
|
||||
/// This only works with libadwaita enabled currently.
|
||||
toggle_tab_overview: void,
|
||||
|
||||
/// Create a new split in the given direction. The new split will appear in
|
||||
/// the direction given.
|
||||
new_split: SplitDirection,
|
||||
@@ -297,6 +353,37 @@ pub const Action = union(enum) {
|
||||
/// Toggle window decorations on and off. This only works on Linux.
|
||||
toggle_window_decorations: void,
|
||||
|
||||
/// Toggle secure input mode on or off. This is used to prevent apps
|
||||
/// that monitor input from seeing what you type. This is useful for
|
||||
/// entering passwords or other sensitive information.
|
||||
///
|
||||
/// This applies to the entire application, not just the focused
|
||||
/// terminal. You must toggle it off to disable it, or quit Ghostty.
|
||||
///
|
||||
/// This only works on macOS, since this is a system API on macOS.
|
||||
toggle_secure_input: void,
|
||||
|
||||
/// Toggle the "quick" terminal. The quick terminal is a terminal that
|
||||
/// appears on demand from a keybinding, often sliding in from a screen
|
||||
/// edge such as the top. This is useful for quick access to a terminal
|
||||
/// without having to open a new window or tab.
|
||||
///
|
||||
/// When the quick terminal loses focus, it disappears. The terminal state
|
||||
/// is preserved between appearances, so you can always press the keybinding
|
||||
/// to bring it back up.
|
||||
///
|
||||
/// The quick terminal has some limitations:
|
||||
///
|
||||
/// - It is a singleton; only one instance can exist at a time.
|
||||
/// - It does not support tabs.
|
||||
/// - It does not support fullscreen.
|
||||
/// - It will not be restored when the application is restarted
|
||||
/// (for systems that support window restoration).
|
||||
///
|
||||
/// See the various configurations for the quick terminal in the
|
||||
/// configuration file to customize its behavior.
|
||||
toggle_quick_terminal: void,
|
||||
|
||||
/// Quit ghostty.
|
||||
quit: void,
|
||||
|
||||
@@ -348,8 +435,7 @@ pub const Action = union(enum) {
|
||||
// Note: we don't support top or left yet
|
||||
};
|
||||
|
||||
// Extern because it is used in the embedded runtime ABI.
|
||||
pub const SplitFocusDirection = enum(c_int) {
|
||||
pub const SplitFocusDirection = enum {
|
||||
previous,
|
||||
next,
|
||||
|
||||
@@ -359,8 +445,7 @@ pub const Action = union(enum) {
|
||||
right,
|
||||
};
|
||||
|
||||
// Extern because it is used in the embedded runtime ABI.
|
||||
pub const SplitResizeDirection = enum(c_int) {
|
||||
pub const SplitResizeDirection = enum {
|
||||
up,
|
||||
down,
|
||||
left,
|
||||
@@ -378,7 +463,7 @@ pub const Action = union(enum) {
|
||||
};
|
||||
|
||||
// Extern because it is used in the embedded runtime ABI.
|
||||
pub const InspectorMode = enum(c_int) {
|
||||
pub const InspectorMode = enum {
|
||||
toggle,
|
||||
show,
|
||||
hide,
|
||||
@@ -479,6 +564,144 @@ 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,
|
||||
.toggle_quick_terminal,
|
||||
=> .app,
|
||||
|
||||
// These are app but can be special-cased in a surface context.
|
||||
.new_window,
|
||||
=> .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,
|
||||
=> .surface,
|
||||
|
||||
// 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_tab,
|
||||
.previous_tab,
|
||||
.next_tab,
|
||||
.last_tab,
|
||||
.goto_tab,
|
||||
.toggle_tab_overview,
|
||||
.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(
|
||||
@@ -534,10 +757,15 @@ pub const Action = union(enum) {
|
||||
/// action.
|
||||
pub fn hash(self: Action) u64 {
|
||||
var hasher = std.hash.Wyhash.init(0);
|
||||
self.hashIncremental(&hasher);
|
||||
return hasher.final();
|
||||
}
|
||||
|
||||
/// Hash the action into the given hasher.
|
||||
fn hashIncremental(self: Action, hasher: anytype) void {
|
||||
// Always has the active tag.
|
||||
const Tag = @typeInfo(Action).Union.tag_type.?;
|
||||
std.hash.autoHash(&hasher, @as(Tag, self));
|
||||
std.hash.autoHash(hasher, @as(Tag, self));
|
||||
|
||||
// Hash the value of the field.
|
||||
switch (self) {
|
||||
@@ -552,25 +780,23 @@ pub const Action = union(enum) {
|
||||
// signed zeros but these are not cases we expect for
|
||||
// our bindings.
|
||||
f32 => std.hash.autoHash(
|
||||
&hasher,
|
||||
hasher,
|
||||
@as(u32, @bitCast(field)),
|
||||
),
|
||||
f64 => std.hash.autoHash(
|
||||
&hasher,
|
||||
hasher,
|
||||
@as(u64, @bitCast(field)),
|
||||
),
|
||||
|
||||
// Everything else automatically handle.
|
||||
else => std.hash.autoHashStrat(
|
||||
&hasher,
|
||||
hasher,
|
||||
field,
|
||||
.DeepRecursive,
|
||||
),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return hasher.final();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -727,11 +953,16 @@ pub const Trigger = struct {
|
||||
/// Returns a hash code that can be used to uniquely identify this trigger.
|
||||
pub fn hash(self: Trigger) u64 {
|
||||
var hasher = std.hash.Wyhash.init(0);
|
||||
std.hash.autoHash(&hasher, self.key);
|
||||
std.hash.autoHash(&hasher, self.mods.binding());
|
||||
self.hashIncremental(&hasher);
|
||||
return hasher.final();
|
||||
}
|
||||
|
||||
/// Hash the trigger into the given hasher.
|
||||
fn hashIncremental(self: Trigger, hasher: anytype) void {
|
||||
std.hash.autoHash(hasher, self.key);
|
||||
std.hash.autoHash(hasher, self.mods.binding());
|
||||
}
|
||||
|
||||
/// Convert the trigger to a C API compatible trigger.
|
||||
pub fn cval(self: Trigger) C {
|
||||
return .{
|
||||
@@ -808,10 +1039,8 @@ pub const Set = struct {
|
||||
leader: *Set,
|
||||
|
||||
/// This trigger completes a sequence and the value is the action
|
||||
/// to take. The "_unconsumed" variant is used for triggers that
|
||||
/// should not consume the input.
|
||||
action: Action,
|
||||
action_unconsumed: Action,
|
||||
/// to take along with the flags that may define binding behavior.
|
||||
leaf: Leaf,
|
||||
|
||||
/// Implements the formatter for the fmt package. This encodes the
|
||||
/// action back into the format used by parse.
|
||||
@@ -836,14 +1065,28 @@ pub const Set = struct {
|
||||
}
|
||||
},
|
||||
|
||||
.action, .action_unconsumed => |action| {
|
||||
.leaf => |leaf| {
|
||||
// action implements the format
|
||||
try writer.print("={s}", .{action});
|
||||
try writer.print("={s}", .{leaf.action});
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Leaf node of a set is an action to trigger. This is a "leaf" compared
|
||||
/// to the inner nodes which are "leaders" for sequences.
|
||||
pub const Leaf = struct {
|
||||
action: Action,
|
||||
flags: Flags,
|
||||
|
||||
pub fn hash(self: Leaf) u64 {
|
||||
var hasher = std.hash.Wyhash.init(0);
|
||||
self.action.hash(&hasher);
|
||||
std.hash.autoHash(&hasher, self.flags);
|
||||
return hasher.final();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn deinit(self: *Set, alloc: Allocator) void {
|
||||
// Clear any leaders if we have them
|
||||
var it = self.bindings.iterator();
|
||||
@@ -852,7 +1095,7 @@ pub const Set = struct {
|
||||
s.deinit(alloc);
|
||||
alloc.destroy(s);
|
||||
},
|
||||
.action, .action_unconsumed => {},
|
||||
.leaf => {},
|
||||
};
|
||||
|
||||
self.bindings.deinit(alloc);
|
||||
@@ -924,7 +1167,7 @@ pub const Set = struct {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
},
|
||||
|
||||
.action, .action_unconsumed => {
|
||||
.leaf => {
|
||||
// Remove the existing action. Fallthrough as if
|
||||
// we don't have a leader.
|
||||
set.remove(alloc, t);
|
||||
@@ -948,11 +1191,11 @@ pub const Set = struct {
|
||||
set.remove(alloc, t);
|
||||
if (old) |entry| switch (entry) {
|
||||
.leader => unreachable, // Handled above
|
||||
inline .action, .action_unconsumed => |action, tag| set.put_(
|
||||
.leaf => |leaf| set.putFlags(
|
||||
alloc,
|
||||
t,
|
||||
action,
|
||||
tag == .action,
|
||||
leaf.action,
|
||||
leaf.flags,
|
||||
) catch {},
|
||||
};
|
||||
},
|
||||
@@ -967,11 +1210,12 @@ pub const Set = struct {
|
||||
return error.SequenceUnbind;
|
||||
},
|
||||
|
||||
else => if (b.consumed) {
|
||||
try set.put(alloc, b.trigger, b.action);
|
||||
} else {
|
||||
try set.putUnconsumed(alloc, b.trigger, b.action);
|
||||
},
|
||||
else => try set.putFlags(
|
||||
alloc,
|
||||
b.trigger,
|
||||
b.action,
|
||||
b.flags,
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -984,29 +1228,16 @@ pub const Set = struct {
|
||||
t: Trigger,
|
||||
action: Action,
|
||||
) Allocator.Error!void {
|
||||
try self.put_(alloc, t, action, true);
|
||||
try self.putFlags(alloc, t, action, .{});
|
||||
}
|
||||
|
||||
/// Same as put but marks the trigger as unconsumed. An unconsumed
|
||||
/// trigger will evaluate the action and continue to encode for the
|
||||
/// terminal.
|
||||
///
|
||||
/// This is a separate function because this case is rare.
|
||||
pub fn putUnconsumed(
|
||||
/// Add a binding to the set with explicit flags.
|
||||
pub fn putFlags(
|
||||
self: *Set,
|
||||
alloc: Allocator,
|
||||
t: Trigger,
|
||||
action: Action,
|
||||
) Allocator.Error!void {
|
||||
try self.put_(alloc, t, action, false);
|
||||
}
|
||||
|
||||
fn put_(
|
||||
self: *Set,
|
||||
alloc: Allocator,
|
||||
t: Trigger,
|
||||
action: Action,
|
||||
consumed: bool,
|
||||
flags: Flags,
|
||||
) Allocator.Error!void {
|
||||
// unbind should never go into the set, it should be handled prior
|
||||
assert(action != .unbind);
|
||||
@@ -1022,7 +1253,7 @@ pub const Set = struct {
|
||||
|
||||
// If we have an existing binding for this trigger, we have to
|
||||
// update the reverse mapping to remove the old action.
|
||||
.action, .action_unconsumed => {
|
||||
.leaf => {
|
||||
const t_hash = t.hash();
|
||||
var it = self.reverse.iterator();
|
||||
while (it.next()) |reverse_entry| it: {
|
||||
@@ -1034,11 +1265,10 @@ pub const Set = struct {
|
||||
},
|
||||
};
|
||||
|
||||
gop.value_ptr.* = if (consumed) .{
|
||||
gop.value_ptr.* = .{ .leaf = .{
|
||||
.action = action,
|
||||
} else .{
|
||||
.action_unconsumed = action,
|
||||
};
|
||||
.flags = flags,
|
||||
} };
|
||||
errdefer _ = self.bindings.remove(t);
|
||||
try self.reverse.put(alloc, action, t);
|
||||
errdefer _ = self.reverse.remove(action);
|
||||
@@ -1055,6 +1285,31 @@ pub const Set = struct {
|
||||
return self.reverse.get(a);
|
||||
}
|
||||
|
||||
/// Get an entry for the given key event. This will attempt to find
|
||||
/// a binding using multiple parts of the event in the following order:
|
||||
///
|
||||
/// 1. Translated key (event.key)
|
||||
/// 2. Physical key (event.physical_key)
|
||||
/// 3. Unshifted Unicode codepoint (event.unshifted_codepoint)
|
||||
///
|
||||
pub fn getEvent(self: *const Set, event: KeyEvent) ?Entry {
|
||||
var trigger: Trigger = .{
|
||||
.mods = event.mods.binding(),
|
||||
.key = .{ .translated = event.key },
|
||||
};
|
||||
if (self.get(trigger)) |v| return v;
|
||||
|
||||
trigger.key = .{ .physical = event.physical_key };
|
||||
if (self.get(trigger)) |v| return v;
|
||||
|
||||
if (event.unshifted_codepoint > 0) {
|
||||
trigger.key = .{ .unicode = event.unshifted_codepoint };
|
||||
if (self.get(trigger)) |v| return v;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Remove a binding for a given trigger.
|
||||
pub fn remove(self: *Set, alloc: Allocator, t: Trigger) void {
|
||||
const entry = self.bindings.get(t) orelse return;
|
||||
@@ -1073,15 +1328,16 @@ pub const Set = struct {
|
||||
// Note: we'd LIKE to replace this with the most recent binding but
|
||||
// our hash map obviously has no concept of ordering so we have to
|
||||
// choose whatever. Maybe a switch to an array hash map here.
|
||||
.action, .action_unconsumed => |action| {
|
||||
const action_hash = action.hash();
|
||||
.leaf => |leaf| {
|
||||
const action_hash = leaf.action.hash();
|
||||
|
||||
var it = self.bindings.iterator();
|
||||
while (it.next()) |it_entry| {
|
||||
switch (it_entry.value_ptr.*) {
|
||||
.leader => {},
|
||||
.action, .action_unconsumed => |action_search| {
|
||||
if (action_search.hash() == action_hash) {
|
||||
self.reverse.putAssumeCapacity(action, it_entry.key_ptr.*);
|
||||
.leaf => |leaf_search| {
|
||||
if (leaf_search.action.hash() == action_hash) {
|
||||
self.reverse.putAssumeCapacity(leaf.action, it_entry.key_ptr.*);
|
||||
break;
|
||||
}
|
||||
},
|
||||
@@ -1089,7 +1345,7 @@ pub const Set = struct {
|
||||
} else {
|
||||
// No over trigger points to this action so we remove
|
||||
// the reverse mapping completely.
|
||||
_ = self.reverse.remove(action);
|
||||
_ = self.reverse.remove(leaf.action);
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1106,7 +1362,7 @@ pub const Set = struct {
|
||||
var it = result.bindings.iterator();
|
||||
while (it.next()) |entry| switch (entry.value_ptr.*) {
|
||||
// No data to clone
|
||||
.action, .action_unconsumed => {},
|
||||
.leaf => {},
|
||||
|
||||
// Must be deep cloned.
|
||||
.leader => |*s| {
|
||||
@@ -1208,7 +1464,7 @@ test "parse: triggers" {
|
||||
.key = .{ .translated = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.consumed = false,
|
||||
.flags = .{ .consumed = false },
|
||||
}, try parseSingle("unconsumed:shift+a=ignore"));
|
||||
|
||||
// unconsumed physical keys
|
||||
@@ -1218,7 +1474,7 @@ test "parse: triggers" {
|
||||
.key = .{ .physical = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.consumed = false,
|
||||
.flags = .{ .consumed = false },
|
||||
}, try parseSingle("unconsumed:physical:a+shift=ignore"));
|
||||
|
||||
// invalid key
|
||||
@@ -1231,6 +1487,92 @@ test "parse: triggers" {
|
||||
try testing.expectError(Error.InvalidFormat, parseSingle("a+b=ignore"));
|
||||
}
|
||||
|
||||
test "parse: global triggers" {
|
||||
const testing = std.testing;
|
||||
|
||||
// global keys
|
||||
try testing.expectEqual(Binding{
|
||||
.trigger = .{
|
||||
.mods = .{ .shift = true },
|
||||
.key = .{ .translated = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.flags = .{ .global = true },
|
||||
}, try parseSingle("global:shift+a=ignore"));
|
||||
|
||||
// global physical keys
|
||||
try testing.expectEqual(Binding{
|
||||
.trigger = .{
|
||||
.mods = .{ .shift = true },
|
||||
.key = .{ .physical = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.flags = .{ .global = true },
|
||||
}, try parseSingle("global:physical:a+shift=ignore"));
|
||||
|
||||
// global unconsumed keys
|
||||
try testing.expectEqual(Binding{
|
||||
.trigger = .{
|
||||
.mods = .{ .shift = true },
|
||||
.key = .{ .translated = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.flags = .{
|
||||
.global = true,
|
||||
.consumed = false,
|
||||
},
|
||||
}, try parseSingle("unconsumed:global:a+shift=ignore"));
|
||||
|
||||
// global sequences not allowed
|
||||
{
|
||||
var p = try Parser.init("global:a>b=ignore");
|
||||
try testing.expectError(Error.InvalidFormat, p.next());
|
||||
}
|
||||
}
|
||||
|
||||
test "parse: all triggers" {
|
||||
const testing = std.testing;
|
||||
|
||||
// all keys
|
||||
try testing.expectEqual(Binding{
|
||||
.trigger = .{
|
||||
.mods = .{ .shift = true },
|
||||
.key = .{ .translated = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.flags = .{ .all = true },
|
||||
}, try parseSingle("all:shift+a=ignore"));
|
||||
|
||||
// all physical keys
|
||||
try testing.expectEqual(Binding{
|
||||
.trigger = .{
|
||||
.mods = .{ .shift = true },
|
||||
.key = .{ .physical = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.flags = .{ .all = true },
|
||||
}, try parseSingle("all:physical:a+shift=ignore"));
|
||||
|
||||
// all unconsumed keys
|
||||
try testing.expectEqual(Binding{
|
||||
.trigger = .{
|
||||
.mods = .{ .shift = true },
|
||||
.key = .{ .translated = .a },
|
||||
},
|
||||
.action = .{ .ignore = {} },
|
||||
.flags = .{
|
||||
.all = true,
|
||||
.consumed = false,
|
||||
},
|
||||
}, try parseSingle("unconsumed:all:a+shift=ignore"));
|
||||
|
||||
// all sequences not allowed
|
||||
{
|
||||
var p = try Parser.init("all:a>b=ignore");
|
||||
try testing.expectError(Error.InvalidFormat, p.next());
|
||||
}
|
||||
}
|
||||
|
||||
test "parse: modifier aliases" {
|
||||
const testing = std.testing;
|
||||
|
||||
@@ -1456,8 +1798,9 @@ test "set: parseAndPut typical binding" {
|
||||
|
||||
// Creates forward mapping
|
||||
{
|
||||
const action = s.get(.{ .key = .{ .translated = .a } }).?.action;
|
||||
try testing.expect(action == .new_window);
|
||||
const action = s.get(.{ .key = .{ .translated = .a } }).?.leaf;
|
||||
try testing.expect(action.action == .new_window);
|
||||
try testing.expectEqual(Flags{}, action.flags);
|
||||
}
|
||||
|
||||
// Creates reverse mapping
|
||||
@@ -1479,8 +1822,9 @@ test "set: parseAndPut unconsumed binding" {
|
||||
// Creates forward mapping
|
||||
{
|
||||
const trigger: Trigger = .{ .key = .{ .translated = .a } };
|
||||
const action = s.get(trigger).?.action_unconsumed;
|
||||
try testing.expect(action == .new_window);
|
||||
const action = s.get(trigger).?.leaf;
|
||||
try testing.expect(action.action == .new_window);
|
||||
try testing.expectEqual(Flags{ .consumed = false }, action.flags);
|
||||
}
|
||||
|
||||
// Creates reverse mapping
|
||||
@@ -1526,8 +1870,9 @@ test "set: parseAndPut sequence" {
|
||||
{
|
||||
const t: Trigger = .{ .key = .{ .translated = .b } };
|
||||
const e = current.get(t).?;
|
||||
try testing.expect(e == .action);
|
||||
try testing.expect(e.action == .new_window);
|
||||
try testing.expect(e == .leaf);
|
||||
try testing.expect(e.leaf.action == .new_window);
|
||||
try testing.expectEqual(Flags{}, e.leaf.flags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1550,14 +1895,16 @@ test "set: parseAndPut sequence with two actions" {
|
||||
{
|
||||
const t: Trigger = .{ .key = .{ .translated = .b } };
|
||||
const e = current.get(t).?;
|
||||
try testing.expect(e == .action);
|
||||
try testing.expect(e.action == .new_window);
|
||||
try testing.expect(e == .leaf);
|
||||
try testing.expect(e.leaf.action == .new_window);
|
||||
try testing.expectEqual(Flags{}, e.leaf.flags);
|
||||
}
|
||||
{
|
||||
const t: Trigger = .{ .key = .{ .translated = .c } };
|
||||
const e = current.get(t).?;
|
||||
try testing.expect(e == .action);
|
||||
try testing.expect(e.action == .new_tab);
|
||||
try testing.expect(e == .leaf);
|
||||
try testing.expect(e.leaf.action == .new_tab);
|
||||
try testing.expectEqual(Flags{}, e.leaf.flags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1580,8 +1927,9 @@ test "set: parseAndPut overwrite sequence" {
|
||||
{
|
||||
const t: Trigger = .{ .key = .{ .translated = .b } };
|
||||
const e = current.get(t).?;
|
||||
try testing.expect(e == .action);
|
||||
try testing.expect(e.action == .new_window);
|
||||
try testing.expect(e == .leaf);
|
||||
try testing.expect(e.leaf.action == .new_window);
|
||||
try testing.expectEqual(Flags{}, e.leaf.flags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1604,8 +1952,9 @@ test "set: parseAndPut overwrite leader" {
|
||||
{
|
||||
const t: Trigger = .{ .key = .{ .translated = .b } };
|
||||
const e = current.get(t).?;
|
||||
try testing.expect(e == .action);
|
||||
try testing.expect(e.action == .new_window);
|
||||
try testing.expect(e == .leaf);
|
||||
try testing.expect(e.leaf.action == .new_window);
|
||||
try testing.expectEqual(Flags{}, e.leaf.flags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1734,11 +2083,19 @@ test "set: consumed state" {
|
||||
defer s.deinit(alloc);
|
||||
|
||||
try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} });
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .action);
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .leaf);
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.leaf.flags.consumed);
|
||||
|
||||
try s.putUnconsumed(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} });
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .action_unconsumed);
|
||||
try s.putFlags(
|
||||
alloc,
|
||||
.{ .key = .{ .translated = .a } },
|
||||
.{ .new_window = {} },
|
||||
.{ .consumed = false },
|
||||
);
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .leaf);
|
||||
try testing.expect(!s.get(.{ .key = .{ .translated = .a } }).?.leaf.flags.consumed);
|
||||
|
||||
try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} });
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .action);
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).? == .leaf);
|
||||
try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.leaf.flags.consumed);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const std = @import("std");
|
||||
const build_config = @import("build_config.zig");
|
||||
|
||||
/// See build_config.ExeEntrypoint for why we do this.
|
||||
@@ -16,6 +17,12 @@ const entrypoint = switch (build_config.exe_entrypoint) {
|
||||
/// The main entrypoint for the program.
|
||||
pub const main = entrypoint.main;
|
||||
|
||||
/// Standard options such as logger overrides.
|
||||
pub const std_options: std.Options = if (@hasDecl(entrypoint, "std_options"))
|
||||
entrypoint.std_options
|
||||
else
|
||||
.{};
|
||||
|
||||
test {
|
||||
_ = entrypoint;
|
||||
}
|
||||
|
||||
@@ -2523,6 +2523,45 @@ fn updateCell(
|
||||
}
|
||||
}
|
||||
|
||||
// If the cell has an underline, draw it before the character glyph,
|
||||
// so that it layers underneath instead of overtop, since that can
|
||||
// make text difficult to read.
|
||||
if (underline != .none) {
|
||||
const sprite: font.Sprite = switch (underline) {
|
||||
.none => unreachable,
|
||||
.single => .underline,
|
||||
.double => .underline_double,
|
||||
.dotted => .underline_dotted,
|
||||
.dashed => .underline_dashed,
|
||||
.curly => .underline_curly,
|
||||
};
|
||||
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
font.sprite_index,
|
||||
@intFromEnum(sprite),
|
||||
.{
|
||||
.cell_width = if (cell.wide == .wide) 2 else 1,
|
||||
.grid_metrics = self.grid_metrics,
|
||||
},
|
||||
);
|
||||
|
||||
const color = style.underlineColor(palette) orelse colors.fg;
|
||||
|
||||
try self.cells.add(self.alloc, .underline, .{
|
||||
.mode = .fg,
|
||||
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
|
||||
.constraint_width = cell.gridWidth(),
|
||||
.color = .{ color.r, color.g, color.b, alpha },
|
||||
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
|
||||
.glyph_size = .{ render.glyph.width, render.glyph.height },
|
||||
.bearings = .{
|
||||
@intCast(render.glyph.offset_x),
|
||||
@intCast(render.glyph.offset_y),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// If the shaper cell has a glyph, draw it.
|
||||
if (shaper_cell.glyph_index) |glyph_index| glyph: {
|
||||
// Render
|
||||
@@ -2566,42 +2605,6 @@ fn updateCell(
|
||||
});
|
||||
}
|
||||
|
||||
if (underline != .none) {
|
||||
const sprite: font.Sprite = switch (underline) {
|
||||
.none => unreachable,
|
||||
.single => .underline,
|
||||
.double => .underline_double,
|
||||
.dotted => .underline_dotted,
|
||||
.dashed => .underline_dashed,
|
||||
.curly => .underline_curly,
|
||||
};
|
||||
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
font.sprite_index,
|
||||
@intFromEnum(sprite),
|
||||
.{
|
||||
.cell_width = if (cell.wide == .wide) 2 else 1,
|
||||
.grid_metrics = self.grid_metrics,
|
||||
},
|
||||
);
|
||||
|
||||
const color = style.underlineColor(palette) orelse colors.fg;
|
||||
|
||||
try self.cells.add(self.alloc, .underline, .{
|
||||
.mode = .fg,
|
||||
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
|
||||
.constraint_width = cell.gridWidth(),
|
||||
.color = .{ color.r, color.g, color.b, alpha },
|
||||
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
|
||||
.glyph_size = .{ render.glyph.width, render.glyph.height },
|
||||
.bearings = .{
|
||||
@intCast(render.glyph.offset_x),
|
||||
@intCast(render.glyph.offset_y),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (style.flags.strikethrough) {
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
|
||||
@@ -1761,52 +1761,9 @@ fn updateCell(
|
||||
@intFromFloat(@max(0, @min(255, @round(self.config.background_opacity * 255)))),
|
||||
};
|
||||
|
||||
// If the cell has a character, draw it
|
||||
if (cell.hasText()) fg: {
|
||||
// Render
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
shaper_run.font_index,
|
||||
shaper_cell.glyph_index orelse break :fg,
|
||||
.{
|
||||
.grid_metrics = self.grid_metrics,
|
||||
.thicken = self.config.font_thicken,
|
||||
},
|
||||
);
|
||||
|
||||
// If we're rendering a color font, we use the color atlas
|
||||
const mode: CellProgram.CellMode = switch (try fgMode(
|
||||
render.presentation,
|
||||
cell_pin,
|
||||
)) {
|
||||
.normal => .fg,
|
||||
.color => .fg_color,
|
||||
.constrained => .fg_constrained,
|
||||
.powerline => .fg_powerline,
|
||||
};
|
||||
|
||||
try self.cells.append(self.alloc, .{
|
||||
.mode = mode,
|
||||
.grid_col = @intCast(x),
|
||||
.grid_row = @intCast(y),
|
||||
.grid_width = cell.gridWidth(),
|
||||
.glyph_x = render.glyph.atlas_x,
|
||||
.glyph_y = render.glyph.atlas_y,
|
||||
.glyph_width = render.glyph.width,
|
||||
.glyph_height = render.glyph.height,
|
||||
.glyph_offset_x = render.glyph.offset_x + shaper_cell.x_offset,
|
||||
.glyph_offset_y = render.glyph.offset_y + shaper_cell.y_offset,
|
||||
.r = colors.fg.r,
|
||||
.g = colors.fg.g,
|
||||
.b = colors.fg.b,
|
||||
.a = alpha,
|
||||
.bg_r = bg[0],
|
||||
.bg_g = bg[1],
|
||||
.bg_b = bg[2],
|
||||
.bg_a = bg[3],
|
||||
});
|
||||
}
|
||||
|
||||
// If the cell has an underline, draw it before the character glyph,
|
||||
// so that it layers underneath instead of overtop, since that can
|
||||
// make text difficult to read.
|
||||
if (underline != .none) {
|
||||
const sprite: font.Sprite = switch (underline) {
|
||||
.none => unreachable,
|
||||
@@ -1851,6 +1808,58 @@ fn updateCell(
|
||||
});
|
||||
}
|
||||
|
||||
// If the shaper cell has a glyph, draw it.
|
||||
if (shaper_cell.glyph_index) |glyph_index| glyph: {
|
||||
// Render
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
shaper_run.font_index,
|
||||
glyph_index,
|
||||
.{
|
||||
.grid_metrics = self.grid_metrics,
|
||||
.thicken = self.config.font_thicken,
|
||||
},
|
||||
);
|
||||
|
||||
// If the glyph is 0 width or height, it will be invisible
|
||||
// when drawn, so don't bother adding it to the buffer.
|
||||
if (render.glyph.width == 0 or render.glyph.height == 0) {
|
||||
break :glyph;
|
||||
}
|
||||
|
||||
// If we're rendering a color font, we use the color atlas
|
||||
const mode: CellProgram.CellMode = switch (try fgMode(
|
||||
render.presentation,
|
||||
cell_pin,
|
||||
)) {
|
||||
.normal => .fg,
|
||||
.color => .fg_color,
|
||||
.constrained => .fg_constrained,
|
||||
.powerline => .fg_powerline,
|
||||
};
|
||||
|
||||
try self.cells.append(self.alloc, .{
|
||||
.mode = mode,
|
||||
.grid_col = @intCast(x),
|
||||
.grid_row = @intCast(y),
|
||||
.grid_width = cell.gridWidth(),
|
||||
.glyph_x = render.glyph.atlas_x,
|
||||
.glyph_y = render.glyph.atlas_y,
|
||||
.glyph_width = render.glyph.width,
|
||||
.glyph_height = render.glyph.height,
|
||||
.glyph_offset_x = render.glyph.offset_x + shaper_cell.x_offset,
|
||||
.glyph_offset_y = render.glyph.offset_y + shaper_cell.y_offset,
|
||||
.r = colors.fg.r,
|
||||
.g = colors.fg.g,
|
||||
.b = colors.fg.b,
|
||||
.a = alpha,
|
||||
.bg_r = bg[0],
|
||||
.bg_g = bg[1],
|
||||
.bg_b = bg[2],
|
||||
.bg_a = bg[3],
|
||||
});
|
||||
}
|
||||
|
||||
if (style.flags.strikethrough) {
|
||||
const render = try self.font_grid.renderGlyph(
|
||||
self.alloc,
|
||||
|
||||
@@ -224,30 +224,30 @@ vertex CellTextVertexOut cell_text_vertex(
|
||||
out.color = float4(in.color) / 255.0f;
|
||||
|
||||
// === Grid Cell ===
|
||||
//
|
||||
// offset.x = bearings.x
|
||||
// .|.
|
||||
// | |
|
||||
// +-------+_.
|
||||
// ._| | |
|
||||
// | | .###. | |
|
||||
// | | #...# | +- bearings.y
|
||||
// glyph_size.y -+ | ##### | |
|
||||
// | | #.... | |
|
||||
// ^ |_| .#### |_| _.
|
||||
// | | | +- offset.y = cell_size.y - bearings.y
|
||||
// . cell_pos -> +-------+ -'
|
||||
// +Y. |_._|
|
||||
// . |
|
||||
// | glyph_size.x
|
||||
// 0,0--...->
|
||||
// +X
|
||||
// 0,0--...->
|
||||
// |
|
||||
// . offset.x = bearings.x
|
||||
// +Y. .|.
|
||||
// . | |
|
||||
// | cell_pos -> +-------+ _.
|
||||
// v ._| |_. _|- offset.y = cell_size.y - bearings.y
|
||||
// | | .###. | |
|
||||
// | | #...# | |
|
||||
// glyph_size.y -+ | ##### | |
|
||||
// | | #.... | +- bearings.y
|
||||
// |_| .#### | |
|
||||
// | |_|
|
||||
// +-------+
|
||||
// |_._|
|
||||
// |
|
||||
// glyph_size.x
|
||||
//
|
||||
// In order to get the bottom left of the glyph, we compute an offset based
|
||||
// on the bearings. The Y bearing is the distance from the top of the cell
|
||||
// to the bottom of the glyph, so we subtract it from the cell height to get
|
||||
// the y offset. The X bearing is the distance from the left of the cell to
|
||||
// the left of the glyph, so it works as the x offset directly.
|
||||
// In order to get the top left of the glyph, we compute an offset based on
|
||||
// the bearings. The Y bearing is the distance from the bottom of the cell
|
||||
// to the top of the glyph, so we subtract it from the cell height to get
|
||||
// the y offset. The X bearing is the distance from the left of the cell
|
||||
// to the left of the glyph, so it works as the x offset directly.
|
||||
|
||||
float2 size = float2(in.glyph_size);
|
||||
float2 offset = float2(in.bearings);
|
||||
|
||||
@@ -1694,7 +1694,14 @@ pub fn grow(self: *PageList) !?*List.Node {
|
||||
// If allocation would exceed our max size, we prune the first page.
|
||||
// We don't need to reallocate because we can simply reuse that first
|
||||
// page.
|
||||
if (self.page_size + PagePool.item_size > self.maxSize()) prune: {
|
||||
//
|
||||
// We only take this path if we have more than one page since pruning
|
||||
// reuses the popped page. It is possible to have a single page and
|
||||
// exceed the max size if that page was adjusted to be larger after
|
||||
// initial allocation.
|
||||
if (self.pages.len > 1 and
|
||||
self.page_size + PagePool.item_size > self.maxSize())
|
||||
prune: {
|
||||
// If we need to add more memory to ensure our active area is
|
||||
// satisfied then we do not prune.
|
||||
if (self.growRequiredForActive()) break :prune;
|
||||
@@ -3772,6 +3779,51 @@ test "PageList grow allows exceeding max size for active area" {
|
||||
try testing.expectEqual(start_pages + 1, s.totalPages());
|
||||
}
|
||||
|
||||
test "PageList grow prune required with a single page" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 80, 24, 0);
|
||||
defer s.deinit();
|
||||
|
||||
// This block is all test setup. There is nothing required about this
|
||||
// behavior during a refactor. This is setting up a scenario that is
|
||||
// possible to trigger a bug (#2280).
|
||||
{
|
||||
// Adjust our capacity until our page is larger than the standard size.
|
||||
// This is important because it triggers a scenario where our calculated
|
||||
// minSize() which is supposed to accommodate 2 pages is no longer true.
|
||||
var cap = std_capacity;
|
||||
while (true) {
|
||||
cap.grapheme_bytes *= 2;
|
||||
const layout = Page.layout(cap);
|
||||
if (layout.total_size > std_size) break;
|
||||
}
|
||||
|
||||
// Adjust to that capacity. After we should still have one page.
|
||||
_ = try s.adjustCapacity(
|
||||
s.pages.first.?,
|
||||
.{ .grapheme_bytes = cap.grapheme_bytes },
|
||||
);
|
||||
try testing.expect(s.pages.first != null);
|
||||
try testing.expect(s.pages.first == s.pages.last);
|
||||
}
|
||||
|
||||
// Figure out the remaining number of rows. This is the amount that
|
||||
// can be added to the current page before we need to allocate a new
|
||||
// page.
|
||||
const rem = rem: {
|
||||
const page = s.pages.first.?;
|
||||
break :rem page.data.capacity.rows - page.data.size.rows;
|
||||
};
|
||||
for (0..rem) |_| try testing.expect(try s.grow() == null);
|
||||
|
||||
// The next one we add will trigger a new page.
|
||||
const new = try s.grow();
|
||||
try testing.expect(new != null);
|
||||
try testing.expect(new != s.pages.first);
|
||||
}
|
||||
|
||||
test "PageList scroll top" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@@ -122,6 +122,26 @@ test "garbage Kitty command" {
|
||||
try testing.expect(h.end() == null);
|
||||
}
|
||||
|
||||
test "Kitty command with overflow u32" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var h: Handler = .{};
|
||||
h.start();
|
||||
for ("Ga=p,i=10000000000") |c| h.feed(alloc, c);
|
||||
try testing.expect(h.end() == null);
|
||||
}
|
||||
|
||||
test "Kitty command with overflow i32" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var h: Handler = .{};
|
||||
h.start();
|
||||
for ("Ga=p,i=1,z=-9999999999") |c| h.feed(alloc, c);
|
||||
try testing.expect(h.end() == null);
|
||||
}
|
||||
|
||||
test "valid Kitty command" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@@ -23,10 +23,10 @@ pub const Parser = struct {
|
||||
/// This is the list of KV pairs that we're building up.
|
||||
kv: KV = .{},
|
||||
|
||||
/// This is used as a buffer to store the key/value of a KV pair.
|
||||
/// The value of a KV pair is at most a 32-bit integer which at most
|
||||
/// is 10 characters (4294967295).
|
||||
kv_temp: [10]u8 = undefined,
|
||||
/// This is used as a buffer to store the key/value of a KV pair. The value
|
||||
/// of a KV pair is at most a 32-bit integer which at most is 10 characters
|
||||
/// (4294967295), plus one character for the sign bit on signed ints.
|
||||
kv_temp: [11]u8 = undefined,
|
||||
kv_temp_len: u4 = 0,
|
||||
kv_current: u8 = 0, // Current kv key
|
||||
|
||||
@@ -237,16 +237,14 @@ pub const Parser = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Only "z" is currently signed. This is a bit of a kloodge; if more
|
||||
// fields become signed we can rethink this but for now we parse
|
||||
// "z" as i32 then bitcast it to u32 then bitcast it back later.
|
||||
if (self.kv_current == 'z') {
|
||||
const v = try std.fmt.parseInt(i32, self.kv_temp[0..self.kv_temp_len], 10);
|
||||
try self.kv.put(alloc, self.kv_current, @bitCast(v));
|
||||
} else {
|
||||
const v = try std.fmt.parseInt(u32, self.kv_temp[0..self.kv_temp_len], 10);
|
||||
try self.kv.put(alloc, self.kv_current, v);
|
||||
}
|
||||
// Handle integer fields, parsing signed fields accordingly. We still
|
||||
// store the fields as u32 as they can be bitcast back later during
|
||||
// building of the higher-level command tree.
|
||||
const v: u32 = switch (self.kv_current) {
|
||||
'z', 'H', 'V' => @bitCast(try std.fmt.parseInt(i32, self.kv_temp[0..self.kv_temp_len], 10)),
|
||||
else => try std.fmt.parseInt(u32, self.kv_temp[0..self.kv_temp_len], 10),
|
||||
};
|
||||
try self.kv.put(alloc, self.kv_current, v);
|
||||
|
||||
// Clear our temp buffer
|
||||
self.kv_temp_len = 0;
|
||||
@@ -505,8 +503,8 @@ pub const Display = struct {
|
||||
virtual_placement: bool = false, // U
|
||||
parent_id: u32 = 0, // P
|
||||
parent_placement_id: u32 = 0, // Q
|
||||
horizontal_offset: u32 = 0, // H
|
||||
vertical_offset: u32 = 0, // V
|
||||
horizontal_offset: i32 = 0, // H
|
||||
vertical_offset: i32 = 0, // V
|
||||
z: i32 = 0, // z
|
||||
|
||||
pub const CursorMovement = enum {
|
||||
@@ -591,11 +589,13 @@ pub const Display = struct {
|
||||
}
|
||||
|
||||
if (kv.get('H')) |v| {
|
||||
result.horizontal_offset = v;
|
||||
// We can bitcast here because of how we parse it earlier.
|
||||
result.horizontal_offset = @bitCast(v);
|
||||
}
|
||||
|
||||
if (kv.get('V')) |v| {
|
||||
result.vertical_offset = v;
|
||||
// We can bitcast here because of how we parse it earlier.
|
||||
result.vertical_offset = @bitCast(v);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -1069,6 +1069,95 @@ test "ignore very long values" {
|
||||
try testing.expectEqual(@as(u32, 0), v.height);
|
||||
}
|
||||
|
||||
test "ensure very large negative values don't get skipped" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
var p = Parser.init(alloc);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "a=p,i=1,z=-2000000000";
|
||||
for (input) |c| try p.feed(c);
|
||||
const command = try p.complete();
|
||||
defer command.deinit(alloc);
|
||||
|
||||
try testing.expect(command.control == .display);
|
||||
const v = command.control.display;
|
||||
try testing.expectEqual(1, v.image_id);
|
||||
try testing.expectEqual(-2000000000, v.z);
|
||||
}
|
||||
|
||||
test "ensure proper overflow error for u32" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
var p = Parser.init(alloc);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "a=p,i=10000000000";
|
||||
for (input) |c| try p.feed(c);
|
||||
try testing.expectError(error.Overflow, p.complete());
|
||||
}
|
||||
|
||||
test "ensure proper overflow error for i32" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
var p = Parser.init(alloc);
|
||||
defer p.deinit();
|
||||
|
||||
const input = "a=p,i=1,z=-9999999999";
|
||||
for (input) |c| try p.feed(c);
|
||||
try testing.expectError(error.Overflow, p.complete());
|
||||
}
|
||||
|
||||
test "all i32 values" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
{
|
||||
// 'z' (usually z-axis values)
|
||||
var p = Parser.init(alloc);
|
||||
defer p.deinit();
|
||||
const input = "a=p,i=1,z=-1";
|
||||
for (input) |c| try p.feed(c);
|
||||
const command = try p.complete();
|
||||
defer command.deinit(alloc);
|
||||
|
||||
try testing.expect(command.control == .display);
|
||||
const v = command.control.display;
|
||||
try testing.expectEqual(1, v.image_id);
|
||||
try testing.expectEqual(-1, v.z);
|
||||
}
|
||||
|
||||
{
|
||||
// 'H' (relative placement, horizontal offset)
|
||||
var p = Parser.init(alloc);
|
||||
defer p.deinit();
|
||||
const input = "a=p,i=1,H=-1";
|
||||
for (input) |c| try p.feed(c);
|
||||
const command = try p.complete();
|
||||
defer command.deinit(alloc);
|
||||
|
||||
try testing.expect(command.control == .display);
|
||||
const v = command.control.display;
|
||||
try testing.expectEqual(1, v.image_id);
|
||||
try testing.expectEqual(-1, v.horizontal_offset);
|
||||
}
|
||||
|
||||
{
|
||||
// 'V' (relative placement, vertical offset)
|
||||
var p = Parser.init(alloc);
|
||||
defer p.deinit();
|
||||
const input = "a=p,i=1,V=-1";
|
||||
for (input) |c| try p.feed(c);
|
||||
const command = try p.complete();
|
||||
defer command.deinit(alloc);
|
||||
|
||||
try testing.expect(command.control == .display);
|
||||
const v = command.control.display;
|
||||
try testing.expectEqual(1, v.image_id);
|
||||
try testing.expectEqual(-1, v.vertical_offset);
|
||||
}
|
||||
}
|
||||
|
||||
test "response: encode nothing without ID or image number" {
|
||||
const testing = std.testing;
|
||||
var buf: [1024]u8 = undefined;
|
||||
|
||||
@@ -495,3 +495,37 @@ test "kittygfx default format is rgba" {
|
||||
const img = storage.imageById(1).?;
|
||||
try testing.expectEqual(command.Transmission.Format.rgba, img.format);
|
||||
}
|
||||
|
||||
test "kittygfx test valid u32 (expect invalid image ID)" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var t = try Terminal.init(alloc, .{ .rows = 5, .cols = 5 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
const cmd = try command.Parser.parseString(
|
||||
alloc,
|
||||
"a=p,i=4294967295",
|
||||
);
|
||||
defer cmd.deinit(alloc);
|
||||
const resp = execute(alloc, &t, &cmd).?;
|
||||
try testing.expect(!resp.ok());
|
||||
try testing.expectEqual(resp.message, "ENOENT: image not found");
|
||||
}
|
||||
|
||||
test "kittygfx test valid i32 (expect invalid image ID)" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var t = try Terminal.init(alloc, .{ .rows = 5, .cols = 5 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
const cmd = try command.Parser.parseString(
|
||||
alloc,
|
||||
"a=p,i=1,z=-2147483648",
|
||||
);
|
||||
defer cmd.deinit(alloc);
|
||||
const resp = execute(alloc, &t, &cmd).?;
|
||||
try testing.expect(!resp.ok());
|
||||
try testing.expectEqual(resp.message, "ENOENT: image not found");
|
||||
}
|
||||
|
||||
@@ -165,6 +165,8 @@ pub const Parser = struct {
|
||||
|
||||
9 => return Attribute{ .strikethrough = {} },
|
||||
|
||||
21 => return Attribute{ .underline = .double },
|
||||
|
||||
22 => return Attribute{ .reset_bold = {} },
|
||||
|
||||
23 => return Attribute{ .reset_italic = {} },
|
||||
|
||||
@@ -154,7 +154,7 @@ pub fn threadEnter(
|
||||
processExit,
|
||||
);
|
||||
|
||||
// Start our termios timer. We only support this on Windows.
|
||||
// Start our termios timer. We don't support this on Windows.
|
||||
// Fundamentally, we could support this on Windows so we're just
|
||||
// waiting for someone to implement it.
|
||||
if (comptime builtin.os.tag != .windows) {
|
||||
@@ -1257,9 +1257,19 @@ const Subprocess = struct {
|
||||
// descendents are well and truly dead. We will not rest
|
||||
// until the entire family tree is obliterated.
|
||||
while (true) {
|
||||
if (c.killpg(pgid, c.SIGHUP) < 0) {
|
||||
log.warn("error killing process group pgid={}", .{pgid});
|
||||
return error.KillFailed;
|
||||
switch (posix.errno(c.killpg(pgid, c.SIGHUP))) {
|
||||
.SUCCESS => log.debug("process group killed pgid={}", .{pgid}),
|
||||
else => |err| killpg: {
|
||||
if ((comptime builtin.target.isDarwin()) and
|
||||
err == .PERM)
|
||||
{
|
||||
log.debug("killpg failed with EPERM, expected on Darwin and ignoring", .{});
|
||||
break :killpg;
|
||||
}
|
||||
|
||||
log.warn("error killing process group pgid={} err={}", .{ pgid, err });
|
||||
return error.KillFailed;
|
||||
},
|
||||
}
|
||||
|
||||
// See Command.zig wait for why we specify WNOHANG.
|
||||
@@ -1267,6 +1277,7 @@ const Subprocess = struct {
|
||||
// are still alive without blocking so that we can
|
||||
// kill them again.
|
||||
const res = posix.waitpid(pid, std.c.W.NOHANG);
|
||||
log.debug("waitpid result={}", .{res.pid});
|
||||
if (res.pid != 0) break;
|
||||
std.time.sleep(10 * std.time.ns_per_ms);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user