aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/Surface.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/Surface.zig
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/Surface.zig')
-rw-r--r--vendor/libghostty-vt/src/Surface.zig1073
1 files changed, 791 insertions, 282 deletions
diff --git a/vendor/libghostty-vt/src/Surface.zig b/vendor/libghostty-vt/src/Surface.zig
index 2c4edc49..77529212 100644
--- a/vendor/libghostty-vt/src/Surface.zig
+++ b/vendor/libghostty-vt/src/Surface.zig
@@ -20,8 +20,9 @@ const builtin = @import("builtin");
const assert = @import("quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
-const global_state = &@import("global.zig").state;
+const global = @import("global.zig");
const oni = @import("oniguruma");
+const simd = @import("simd/main.zig");
const crash = @import("crash/main.zig");
const unicode = @import("unicode/main.zig");
const rendererpkg = @import("renderer.zig");
@@ -154,6 +155,10 @@ child_exited: bool = false,
/// to let us know.
focused: bool = true,
+/// Whether this surface may be visible. Unknown visibility is considered
+/// visible so reporting remains conservative.
+visible: bool = true,
+
/// Used to determine whether to continuously scroll.
selection_scroll_active: bool = false,
@@ -168,13 +173,13 @@ readonly: bool = false,
/// precision timestamp. It does not necessarily need to correspond to the
/// actual time, but we must be able to compare two subsequent timestamps to get
/// the wall clock time that has elapsed between timestamps.
-command_timer: ?std.time.Instant = null,
+command_timer: ?std.Io.Timestamp = null,
/// Search state
search: ?Search = null,
/// Used to rate limit BEL handling.
-last_bell_time: ?std.time.Instant = null,
+last_bell_time: ?std.Io.Timestamp = null,
/// The effect of an input event. This can be used by callers to take
/// the appropriate action after an input event. For example, key
@@ -330,6 +335,7 @@ const DerivedConfig = struct {
title: ?[:0]const u8,
title_report: bool,
links: []DerivedConfig.Link,
+ link_osc8: bool,
link_previews: configpkg.LinkPreviews,
scroll_to_bottom: configpkg.Config.ScrollToBottom,
notify_on_command_finish: configpkg.Config.NotifyOnCommandFinish,
@@ -409,6 +415,7 @@ const DerivedConfig = struct {
.title = config.title,
.title_report = config.@"title-report",
.links = links,
+ .link_osc8 = config.@"link-osc8",
.link_previews = config.@"link-previews",
.scroll_to_bottom = config.@"scroll-to-bottom",
.notify_on_command_finish = config.@"notify-on-command-finish",
@@ -559,8 +566,8 @@ pub fn init(
errdefer renderer_impl.deinit();
// The mutex used to protect our renderer state.
- const mutex = try alloc.create(std.Thread.Mutex);
- mutex.* = .{};
+ const mutex = try alloc.create(std.Io.Mutex);
+ mutex.* = .init;
errdefer alloc.destroy(mutex);
// Create the renderer thread
@@ -581,7 +588,11 @@ pub fn init(
self.* = .{
.id = id: {
while (true) {
- const candidate = std.crypto.random.int(u64);
+ const candidate = candidate: {
+ const rng_impl: std.Random.IoSource = .{ .io = global.io() };
+ const rng = rng_impl.interface();
+ break :candidate rng.int(u64);
+ };
if (candidate == 0) continue;
break :id candidate;
}
@@ -631,13 +642,12 @@ pub fn init(
var env = rt_surface.defaultTermioEnv() catch |err| env: {
// If an error occurs, we don't want to block surface startup.
log.warn("error getting env map for surface err={}", .{err});
- break :env internal_os.getEnvMap(alloc) catch
- std.process.EnvMap.init(alloc);
+ break :env global.environMap() catch std.process.Environ.Map.init(alloc);
};
errdefer env.deinit();
// don't leak GHOSTTY_LOG to any subprocesses
- env.remove("GHOSTTY_LOG");
+ _ = env.orderedRemove("GHOSTTY_LOG");
var buf: [18]u8 = undefined;
try env.put(
@@ -654,7 +664,7 @@ pub fn init(
.shell_integration_features = config.@"shell-integration-features",
.cursor_blink = config.@"cursor-style-blink",
.working_directory = if (config.@"working-directory") |wd| wd.value() else null,
- .resources_dir = global_state.resources_dir.host(),
+ .resources_dir = global.resourcesDir().host(),
.term = config.term,
.rt_pre_exec_info = .init(config),
.rt_post_fork_info = .init(config),
@@ -717,7 +727,7 @@ pub fn init(
rendererpkg.Thread.threadMain,
.{&self.renderer_thread},
);
- self.renderer_thr.setName("renderer") catch {};
+ self.renderer_thr.setName(global.io(), "renderer") catch {};
// Start our IO thread
self.io_thr = try std.Thread.spawn(
@@ -725,7 +735,7 @@ pub fn init(
termio.Thread.threadMain,
.{ &self.io_thread, &self.io },
);
- self.io_thr.setName("io") catch {};
+ self.io_thr.setName(global.io(), "io") catch {};
// Determine our initial window size if configured. We need to do this
// quite late in the process because our height/width are in grid dimensions,
@@ -834,7 +844,7 @@ pub fn deinit(self: *Surface) void {
self.alloc.destroy(self.renderer_state.mutex);
self.config.deinit();
- log.info("surface closed addr={x}", .{@intFromPtr(self)});
+ log.info("surface closed id={x}", .{self.id});
}
/// Close this surface. This will trigger the runtime to start the
@@ -851,7 +861,7 @@ inline fn surfaceMailbox(self: *Surface) Mailbox {
};
}
-/// Queue a message for the IO thread.
+/// Queue a message for the IO thread, taking ownership of `msg`.
///
/// We centralize all our logic into this spot so we can intercept
/// messages for example in readonly mode.
@@ -866,7 +876,10 @@ fn queueIo(
.write_small,
.write_stable,
.write_alloc,
- => return,
+ => {
+ msg.deinit();
+ return;
+ },
else => {},
}
@@ -900,14 +913,14 @@ pub fn activateInspector(self: *Surface) !void {
// Put the inspector onto the render state
{
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
assert(self.renderer_state.inspector == null);
self.renderer_state.inspector = self.inspector;
}
// Notify our components we have an inspector active
- _ = self.renderer_thread.mailbox.push(.{ .inspector = true }, .{ .forever = {} });
+ _ = self.renderer_thread.mailbox.push(global.io(), .{ .inspector = true }, .{ .forever = {} });
self.queueIo(.{ .inspector = true }, .unlocked);
}
@@ -917,14 +930,14 @@ pub fn deactivateInspector(self: *Surface) void {
// Remove the inspector from the render state
{
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
assert(self.renderer_state.inspector != null);
self.renderer_state.inspector = null;
}
// Notify our components we have deactivated inspector
- _ = self.renderer_thread.mailbox.push(.{ .inspector = false }, .{ .forever = {} });
+ _ = self.renderer_thread.mailbox.push(global.io(), .{ .inspector = false }, .{ .forever = {} });
self.queueIo(.{ .inspector = false }, .unlocked);
// Deinit the inspector
@@ -948,8 +961,8 @@ pub fn needsConfirmQuit(self: *Surface) bool {
.always => true,
.false => false,
.true => true: {
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
break :true !self.io.terminal.cursorIsAtPrompt();
},
};
@@ -1049,6 +1062,10 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
_ = try self.startClipboardRequest(.standard, .{ .osc_52_read = clipboard });
},
+ .kitty_clipboard_read => |req| try self.kittyClipboardRead(req),
+
+ .kitty_clipboard_write => |req| try self.kittyClipboardWrite(req),
+
.clipboard_write => |w| switch (w.req) {
.small => |v| try self.clipboardWrite(v.data[0..v.len], w.clipboard_type),
.stable => |v| try self.clipboardWrite(v, w.clipboard_type),
@@ -1061,9 +1078,10 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.pwd_change => |w| {
defer w.deinit();
- // We always allocate for this because we need to null-terminate.
- const str = try self.alloc.dupeZ(u8, w.slice());
- defer self.alloc.free(str);
+ var stack = std.heap.stackFallback(256, self.alloc);
+ const alloc = stack.get();
+ const str = try alloc.dupeZ(u8, w.slice());
+ defer alloc.free(str);
_ = try self.rt_app.performAction(
.{ .surface = self },
@@ -1096,9 +1114,9 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.password_input => |v| try self.passwordInput(v),
.ring_bell => bell: {
- const now = std.time.Instant.now() catch unreachable;
+ const now: std.Io.Timestamp = .now(global.io(), .awake);
if (self.last_bell_time) |last| {
- if (now.since(last) < 100 * std.time.ns_per_ms) break :bell;
+ if (last.durationTo(now).toMilliseconds() < 100) break :bell;
}
self.last_bell_time = now;
_ = self.rt_app.performAction(
@@ -1126,15 +1144,22 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
},
.start_command => {
- self.command_timer = try .now();
+ self.command_timer = .now(global.io(), .awake);
},
.stop_command => |v| timer: {
- const end: std.time.Instant = try .now();
+ const end: std.Io.Timestamp = .now(global.io(), .awake);
const start = self.command_timer orelse break :timer;
self.command_timer = null;
+ const duration_raw = start.durationTo(end).nanoseconds;
+ assert(duration_raw >= 0 and duration_raw <= std.math.maxInt(u64));
- const duration: Duration = .{ .duration = end.since(start) };
+ const duration: Duration = .{
+ .duration = @as(
+ u64,
+ @intCast(std.math.clamp(start.durationTo(end).nanoseconds, 0, std.math.maxInt(u64))),
+ ),
+ };
log.debug("command took {f}", .{duration});
_ = self.rt_app.performAction(
@@ -1185,8 +1210,8 @@ fn selectionScrollTick(self: *Surface) !void {
const pos_vp = self.posToViewport(pos.x, pos.y);
// We need our locked state for the remainder
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
const t: *terminal.Terminal = self.renderer_state.terminal;
const selection = self.mouse.selection_gesture.autoscrollTick(t, .{
@@ -1276,8 +1301,8 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void {
// If the native GUI can't be shown, display a text message in the
// terminal.
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
const t: *terminal.Terminal = self.renderer_state.terminal;
t.carriageReturn();
t.linefeed() catch break :terminal;
@@ -1316,8 +1341,8 @@ fn childExitedAbnormally(
});
const runtime_str = try std.fmt.allocPrint(alloc, "{d} ms", .{info.runtime_ms});
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
const t: *terminal.Terminal = self.renderer_state.terminal;
// No matter what move the cursor back to the column 0.
@@ -1383,8 +1408,8 @@ fn childExitedAbnormally(
/// Called when the terminal detects there is a password input prompt.
fn passwordInput(self: *Surface, v: bool) !void {
{
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
// If our password input state is unchanged then we don't
// waste time doing anything more.
@@ -1436,6 +1461,7 @@ fn searchCallback_(
for (matches) |*m| m.* = try m.clone(alloc);
_ = self.renderer_thread.mailbox.push(
+ global.io(),
.{ .search_viewport_matches = .{
.arena = arena,
.matches = matches,
@@ -1454,6 +1480,7 @@ fn searchCallback_(
const match = try sel.highlight.clone(alloc);
_ = self.renderer_thread.mailbox.push(
+ global.io(),
.{ .search_selected_match = .{
.arena = arena,
.match = match,
@@ -1469,6 +1496,7 @@ fn searchCallback_(
} else {
// Reset our selected match
_ = self.renderer_thread.mailbox.push(
+ global.io(),
.{ .search_selected_match = null },
.forever,
);
@@ -1493,10 +1521,12 @@ fn searchCallback_(
// When we quit, tell our renderer to reset any search state.
.quit => {
_ = self.renderer_thread.mailbox.push(
+ global.io(),
.{ .search_selected_match = null },
.forever,
);
_ = self.renderer_thread.mailbox.push(
+ global.io(),
.{ .search_viewport_matches = .{
.arena = .init(self.alloc),
.matches = &.{},
@@ -1539,8 +1569,8 @@ fn modsChanged(self: *Surface, mods: input.Mods) void {
// highlight links. Additionally, mark the screen as dirty so
// that the highlight state of all links is properly updated.
{
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
self.renderer_state.mouse.mods = self.mouseModsWithCapture(self.mouse.mods);
// We use the clear screen dirty flag to force a rebuild of all
@@ -1786,7 +1816,7 @@ pub fn updateConfig(
termio_config_ptr.* = try termio.Termio.DerivedConfig.init(self.alloc, config);
errdefer termio_config_ptr.deinit();
- _ = self.renderer_thread.mailbox.push(renderer_message, .{ .forever = {} });
+ _ = self.renderer_thread.mailbox.push(global.io(), renderer_message, .{ .forever = {} });
self.queueIo(.{
.change_config = .{
.alloc = self.alloc,
@@ -1907,8 +1937,8 @@ pub fn dumpText(
alloc: Allocator,
sel: terminal.Selection,
) !Text {
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
return try self.dumpTextLocked(alloc, sel);
}
@@ -2033,15 +2063,15 @@ pub fn dumpTextLocked(
/// Returns true if the terminal has a selection.
pub fn hasSelection(self: *const Surface) bool {
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
return self.io.terminal.screens.active.selection != null;
}
/// Returns the selected text. This is allocated.
pub fn selectionString(self: *Surface, alloc: Allocator) !?[:0]const u8 {
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
const sel = self.io.terminal.screens.active.selection orelse return null;
return try self.io.terminal.screens.active.selectionString(alloc, .{
.sel = sel,
@@ -2056,8 +2086,8 @@ pub fn pwd(
self: *const Surface,
alloc: Allocator,
) Allocator.Error!?[]const u8 {
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
const terminal_pwd = self.io.terminal.getPwd() orelse return null;
return try alloc.dupe(u8, terminal_pwd);
}
@@ -2074,7 +2104,7 @@ fn resolvePathForOpening(
const resolved = try std.fs.path.resolve(self.alloc, &.{ terminal_pwd, path });
- std.fs.accessAbsolute(resolved, .{}) catch {
+ std.Io.Dir.accessAbsolute(global.io(), resolved, .{}) catch {
self.alloc.free(resolved);
return null;
};
@@ -2088,10 +2118,10 @@ fn resolvePathForOpening(
/// Returns the x/y coordinate of where the IME (Input Method Editor)
/// keyboard should be rendered.
pub fn imePoint(self: *const Surface) apprt.IMEPos {
- self.renderer_state.mutex.lock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
const cursor = self.renderer_state.terminal.screens.active.cursor;
const preedit_width: usize = if (self.renderer_state.preedit) |preedit| preedit.width() else 0;
- self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.unlock(global.io());
// TODO: need to handle when scrolling and the cursor is not
// in the visible portion of the screen.
@@ -2165,34 +2195,18 @@ fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard)
return;
}
- const dec = std.base64.standard.Decoder;
-
- // Build buffer
- const size = dec.calcSizeForSlice(data) catch |err| switch (err) {
- error.InvalidPadding => {
- log.info("application sent invalid base64 data for OSC 52", .{});
- return;
- },
-
- // Should not be reachable but don't want to risk it.
- else => return,
- };
- var buf = try self.alloc.allocSentinel(u8, size, 0);
+ // Decode with the SIMD decoder, strict per the Kitty clipboard
+ // spec that also governs OSC 52 base64 handling: a request with
+ // characters outside the base64 alphabet is discarded entirely
+ // (never partially decoded), while a missing-padding tail is
+ // tolerated since OSC 52 has no way to report errors.
+ var buf = try self.alloc.allocSentinel(u8, simd.base64.maxLen(data), 0);
defer self.alloc.free(buf);
- buf[buf.len] = 0;
-
- // Decode
- dec.decode(buf, data) catch |err| switch (err) {
- // Ignore this. It is possible to actually have valid data and
- // get this error, so we allow it.
- error.InvalidPadding => {},
-
- else => {
- log.info("application sent invalid base64 data for OSC 52", .{});
- return;
- },
+ const decoded = simd.base64.decodeStrict(data, buf, .optional) catch {
+ log.info("application sent invalid base64 data for OSC 52", .{});
+ return;
};
- assert(buf[buf.len] == 0);
+ buf[decoded.len] = 0;
// When clipboard-write is "ask" a prompt is displayed to the user asking
// them to confirm the clipboard access. Each app runtime handles this
@@ -2200,7 +2214,7 @@ fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard)
const confirm = self.config.clipboard_write == .ask;
self.rt_surface.setClipboard(loc, &.{.{
.mime = "text/plain",
- .data = buf,
+ .data = buf[0..decoded.len :0],
}}, confirm) catch |err| {
log.err("error setting clipboard string err={}", .{err});
return;
@@ -2232,13 +2246,14 @@ fn copySelectionToClipboards(
const ScreenFormatter = terminal.formatter.ScreenFormatter;
var aw: std.Io.Writer.Allocating = .init(alloc);
- var contents: std.ArrayList(apprt.ClipboardContent) = .empty;
+ var contents_buf: [2]apprt.ClipboardContent = undefined;
+ var contents: std.ArrayList(apprt.ClipboardContent) = .initBuffer(&contents_buf);
switch (format) {
.plain => {
var formatter: ScreenFormatter = .init(self.io.terminal.screens.active, opts);
formatter.content = .{ .selection = sel };
try formatter.format(&aw.writer);
- try contents.append(alloc, .{
+ contents.appendAssumeCapacity(.{
.mime = "text/plain",
.data = try aw.toOwnedSliceSentinel(0),
});
@@ -2255,7 +2270,7 @@ fn copySelectionToClipboards(
// Note: We don't apply codepoint mappings to VT format since it contains
// escape sequences that should be preserved as-is
- try contents.append(alloc, .{
+ contents.appendAssumeCapacity(.{
.mime = "text/plain",
.data = try aw.toOwnedSliceSentinel(0),
});
@@ -2272,7 +2287,7 @@ fn copySelectionToClipboards(
// Note: We don't apply codepoint mappings to HTML format since HTML
// has its own character encoding and entity system
- try contents.append(alloc, .{
+ contents.appendAssumeCapacity(.{
.mime = "text/html",
.data = try aw.toOwnedSliceSentinel(0),
});
@@ -2283,7 +2298,7 @@ fn copySelectionToClipboards(
var formatter: ScreenFormatter = .init(self.io.terminal.screens.active, opts);
formatter.content = .{ .selection = sel };
try formatter.format(&aw.writer);
- try contents.append(alloc, .{
+ contents.appendAssumeCapacity(.{
.mime = "text/plain",
.data = try aw.toOwnedSliceSentinel(0),
});
@@ -2306,7 +2321,7 @@ fn copySelectionToClipboards(
try formatter.format(&aw.writer);
// Note: We don't apply codepoint mappings to HTML format
- try contents.append(alloc, .{
+ contents.appendAssumeCapacity(.{
.mime = "text/html",
.data = try aw.toOwnedSliceSentinel(0),
});
@@ -2363,31 +2378,29 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) !void {
fn setSelectionAndCopy(self: *Surface, sel: terminal.Selection) !void {
try self.setSelection(sel);
- // If copy on select is false then exit early.
- if (self.config.copy_on_select == .false) return;
-
switch (self.config.copy_on_select) {
- .false => unreachable, // handled above with an early exit
+ .none => {},
- // Both standard and selection clipboards are set.
+ // The selection clipboard is set if supported, otherwise nothing is copied.
+ .primary => try self.copySelectionToClipboards(
+ sel,
+ &.{.selection},
+ .mixed,
+ ),
+
+ // Only the standard clipboard is set.
.clipboard => try self.copySelectionToClipboards(
sel,
- &.{ .standard, .selection },
+ &.{.standard},
.mixed,
),
- // The selection clipboard is set if supported, otherwise the standard.
- .true => {
- const clipboard: apprt.Clipboard = if (self.rt_surface.supportsClipboard(.selection))
- .selection
- else
- .standard;
- try self.copySelectionToClipboards(
- sel,
- &.{clipboard},
- .mixed,
- );
- },
+ // Both standard and selection clipboards are set.
+ .both => try self.copySelectionToClipboards(
+ sel,
+ &.{ .standard, .selection },
+ .mixed,
+ ),
}
}
@@ -2440,7 +2453,7 @@ pub fn setFontSize(self: *Surface, size: font.face.DesiredSize) !void {
// Notify our render thread of the new font stack. The renderer
// MUST accept the new font grid and deref the old.
- _ = self.renderer_thread.mailbox.push(.{
+ _ = self.renderer_thread.mailbox.push(global.io(), .{
.font_grid = .{
.grid = font_grid,
.set = &self.app.font_grid_set,
@@ -2530,8 +2543,8 @@ pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void {
crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null;
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
// We clear our selection when ANY OF:
// 1. We have an existing preedit
@@ -2566,7 +2579,7 @@ pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void {
// Allocate the codepoints slice
const Codepoint = rendererpkg.State.Preedit.Codepoint;
- var codepoints: std.ArrayListUnmanaged(Codepoint) = .{};
+ var codepoints: std.ArrayList(Codepoint) = .empty;
defer codepoints.deinit(self.alloc);
while (it.nextCodepoint()) |cp| {
const width: usize = @intCast(unicode.table.get(cp).width);
@@ -2697,8 +2710,8 @@ pub fn keyCallback(
)) |v| return v;
// If we allow KAM and KAM is enabled then we do nothing.
if (self.config.vt_kam_allowed) {
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
if (self.io.terminal.modes.get(.disable_keyboard)) return .consumed;
}
@@ -2728,8 +2741,8 @@ pub fn keyCallback(
{
// Refresh our link state
const pos = self.rt_surface.getCursorPos() catch break :mouse_mods;
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
self.mouseRefreshLinks(
pos,
self.posToViewport(pos.x, pos.y),
@@ -2802,11 +2815,11 @@ pub fn keyCallback(
// an encoded value, we close the surface. We want to eventually
// move this behavior to the apprt probably.
if (self.child_exited) {
+ write_req.deinit();
self.close();
return .closed;
}
- errdefer write_req.deinit();
self.queueIo(switch (write_req) {
.small => |v| .{ .write_small = v },
.stable => |v| .{ .write_stable = v },
@@ -2821,8 +2834,8 @@ pub fn keyCallback(
// some data to send to the pty, then we move the viewport down to the
// bottom. We also clear the selection for any key other then modifiers.
if (!event.key.modifier()) {
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
if (self.config.selection_clear_on_typing or
event.key == .escape)
@@ -2924,16 +2937,19 @@ fn maybeHandleBinding(
// Determine if this entry has an action or if its a leader key.
const leaf: input.Binding.Set.GenericLeaf = switch (entry.value_ptr.*) {
.leader => |set| {
- // Setup the next set we'll look at.
- self.keyboard.sequence_set = set;
-
// Store this event so that we can drain and encode on invalid.
// We don't need to cap this because it is naturally capped by
// the config validation.
if (try self.encodeKey(event, insp_ev)) |req| {
- try self.keyboard.sequence_queued.append(self.alloc, req);
+ self.keyboard.sequence_queued.append(self.alloc, req) catch |err| {
+ req.deinit();
+ return err;
+ };
}
+ // Setup the next set we'll look at only after all fallible work.
+ self.keyboard.sequence_set = set;
+
// Start or continue our key sequence
_ = self.rt_app.performAction(
.{ .surface = self },
@@ -3222,17 +3238,15 @@ fn encodeKey(
);
defer alloc_writer.deinit();
- // This results in a double allocation but this is such an unlikely
- // path the performance impact is unimportant.
try input.key_encode.encode(
&alloc_writer.writer,
event,
encoding_opts,
);
- break :req try termio.Message.WriteReq.init(
- self.alloc,
- alloc_writer.writer.buffered(),
- );
+ break :req .{ .alloc = .{
+ .alloc = self.alloc,
+ .data = try alloc_writer.toOwnedSlice(),
+ } };
};
// Copy the encoded data into the inspector event if we have one.
@@ -3253,8 +3267,8 @@ fn encodeKey(
}
fn encodeKeyOpts(self: *const Surface) input.key_encode.Options {
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
const t = &self.io.terminal;
var opts: input.key_encode.Options = .fromTerminal(t);
@@ -3295,9 +3309,27 @@ pub fn occlusionCallback(self: *Surface, visible: bool) !void {
crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null;
- _ = self.renderer_thread.mailbox.push(.{
+ // Avoid duplicate renderer and visibility reports.
+ if (self.visible == visible) return;
+ self.visible = visible;
+
+ // Update the terminal state for synchronous queries, then notify the IO
+ // thread so it can emit a mode 2033 report when enabled.
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ self.io.terminal.flags.visible = visible;
+ const report_visibility = self.io.terminal.modes.get(.report_visibility);
+ self.renderer_state.mutex.unlock(global.io());
+ if (report_visibility) {
+ self.queueIo(.{ .visibility_report = .{
+ .visible = visible,
+ .force = false,
+ } }, .unlocked);
+ }
+
+ _ = self.renderer_thread.mailbox.push(global.io(), .{
.visible = visible,
}, .{ .forever = {} });
+
try self.queueRender();
}
@@ -3315,7 +3347,7 @@ pub fn focusCallback(self: *Surface, focused: bool) !void {
self.focused = focused;
// Notify our render thread of the new state
- _ = self.renderer_thread.mailbox.push(.{
+ _ = self.renderer_thread.mailbox.push(global.io(), .{
.focus = focused,
}, .{ .forever = {} });
@@ -3382,9 +3414,9 @@ pub fn focusCallback(self: *Surface, focused: bool) !void {
// Update the focus state and notify the terminal
{
- self.renderer_state.mutex.lock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
self.io.terminal.flags.focused = focused;
- self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.unlock(global.io());
self.queueIo(.{ .focused = focused }, .unlocked);
}
}
@@ -3516,8 +3548,8 @@ pub fn scrollCallback(
// log.info("SCROLL: delta_y={} delta_x={}", .{ y.delta, x.delta });
{
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
// If we have an active mouse reporting mode, clear the selection.
// The selection can occur if the user uses the shift mod key to
@@ -3642,6 +3674,12 @@ fn isMouseReporting(self: *const Surface) bool {
self.io.terminal.flags.mouse_event != .none;
}
+pub fn mouseReportingActive(self: *Surface) bool {
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
+ return self.isMouseReporting();
+}
+
fn mouseReport(
self: *Surface,
button: ?input.MouseButton,
@@ -3717,8 +3755,8 @@ fn mouseShiftCapture(self: *const Surface, lock: bool) bool {
.false, .true => {},
}
- if (lock) self.renderer_state.mutex.lock();
- defer if (lock) self.renderer_state.mutex.unlock();
+ if (lock) self.renderer_state.mutex.lockUncancelable(global.io());
+ defer if (lock) self.renderer_state.mutex.unlock(global.io());
// If the terminal explicitly requests it then we always allow it
// since we processed never/always at this point.
@@ -3739,8 +3777,8 @@ fn mouseShiftCapture(self: *const Surface, lock: bool) bool {
/// Returns true if the mouse is currently captured by the terminal
/// (i.e. reporting events).
pub fn mouseCaptured(self: *Surface) bool {
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
return self.io.terminal.flags.mouse_event != .none;
}
@@ -3796,21 +3834,12 @@ pub fn mouseButtonCallback(
// 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 click_time = self.mouse.selection_gesture.left_click_time orelse
- break :extend_selection;
- const since = now.since(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});
+ const click_time = self.mouse.selection_gesture.left_click_time orelse
+ break :extend_selection;
+ const since = click_time.untilNow(global.io(), .awake);
+ if (since.toNanoseconds() <= self.config.mouse_interval) {
+ // Click interval very short, we may be increasing
+ // click counts so we don't extend the selection.
break :extend_selection;
}
@@ -3821,8 +3850,8 @@ pub fn mouseButtonCallback(
}
if (button == .left and action == .release) {
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
// The selection gesture tracks whether a press became a drag by
// comparing the release cell to the original press cell. Resolve the
@@ -3860,7 +3889,7 @@ pub fn mouseButtonCallback(
// The selection clipboard is only updated for left-click drag when
// the left button is released. This is to avoid the clipboard
// being updated on every mouse move which would be noisy.
- if (self.config.copy_on_select != .false) {
+ if (self.config.copy_on_select != .none) {
const prev_ = self.io.terminal.screens.active.selection;
if (prev_) |prev| {
try self.setSelectionAndCopy(terminal.Selection.init(
@@ -3897,8 +3926,8 @@ pub fn mouseButtonCallback(
// Report mouse events if enabled
{
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
if (self.isMouseReporting()) report: {
// If we have shift-pressed and we aren't allowed to capture it,
// then we do not do a mouse report.
@@ -3937,8 +3966,8 @@ pub fn mouseButtonCallback(
// For left button clicks we always record some information for
// selection/highlighting purposes.
if (button == .left and action == .press) click: {
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
const t: *terminal.Terminal = self.renderer_state.terminal;
const screen: *terminal.Screen = self.renderer_state.terminal.screens.active;
@@ -3962,12 +3991,8 @@ pub fn mouseButtonCallback(
break :pin pin;
};
- const time = std.time.Instant.now() catch |err| time: {
- log.err("error reading time, mouse multi-click won't work err={}", .{err});
- break :time null;
- };
var press_selection = try self.mouse.selection_gesture.press(t, .{
- .time = time,
+ .time = std.Io.Timestamp.now(global.io(), .awake),
.pin = pin,
.xpos = pos.x,
.ypos = pos.y,
@@ -4022,22 +4047,16 @@ pub fn mouseButtonCallback(
}
}
- // Middle-click paste source follows copy-on-select: when copy-on-select
- // targets the selection clipboard, middle-click reads from it; when
- // copy-on-select targets the system clipboard, middle-click reads from
- // that instead. Falls back to the standard clipboard on platforms that
- // do not support the selection clipboard.
+ // Middle-click action, either ignore, or paste from clipboard or paste from the selection clipboard if supported.
if (button == .middle and action == .press) switch (self.config.middle_click_action) {
.ignore => {},
+ .@"clipboard-paste" => {
+ _ = try self.startClipboardRequest(.standard, .{ .paste = .standard });
+ },
.@"primary-paste" => {
- const clipboard: apprt.Clipboard = switch (self.config.copy_on_select) {
- .clipboard => .standard,
- .true, .false => if (self.rt_surface.supportsClipboard(.selection))
- .selection
- else
- .standard,
- };
- _ = try self.startClipboardRequest(clipboard, .{ .paste = {} });
+ if (self.rt_surface.supportsClipboard(.selection)) {
+ _ = try self.startClipboardRequest(.selection, .{ .paste = .selection });
+ }
},
};
@@ -4047,8 +4066,8 @@ pub fn mouseButtonCallback(
// want to be careful in the future we can add a function to apprts
// that let's us know.
if (button == .right and action == .press) sel: {
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
// Get our viewport pin
const screen: *terminal.Screen = self.renderer_state.terminal.screens.active;
@@ -4119,9 +4138,9 @@ pub fn mouseButtonCallback(
} else {
// Pasting can trigger a lock grab in complete clipboard
// request so we need to unlock.
- self.renderer_state.mutex.unlock();
- defer self.renderer_state.mutex.lock();
- _ = try self.startClipboardRequest(.standard, .paste);
+ self.renderer_state.mutex.unlock(global.io());
+ defer self.renderer_state.mutex.lockUncancelable(global.io());
+ _ = try self.startClipboardRequest(.standard, .{ .paste = .standard });
// We don't need to clear selection because we didn't have
// one to begin with.
@@ -4134,9 +4153,9 @@ pub fn mouseButtonCallback(
// Pasting can trigger a lock grab in complete clipboard
// request so we need to unlock.
- self.renderer_state.mutex.unlock();
- defer self.renderer_state.mutex.lock();
- _ = try self.startClipboardRequest(.standard, .paste);
+ self.renderer_state.mutex.unlock(global.io());
+ defer self.renderer_state.mutex.lockUncancelable(global.io());
+ _ = try self.startClipboardRequest(.standard, .{ .paste = .standard });
},
}
@@ -4295,7 +4314,9 @@ fn linkAtPos(
const mouse_mods = self.mouseModsWithCapture(self.mouse.mods);
// If we have the proper modifiers set then we can check for OSC8 links.
- if (mouse_mods.equal(input.ctrlOrSuper(.{}))) hyperlink: {
+ if (self.config.link_osc8 and
+ mouse_mods.equal(input.ctrlOrSuper(.{})))
+ hyperlink: {
const rac = mouse_pin.rowAndCell();
const cell = rac.cell;
if (!cell.hyperlink) break :hyperlink;
@@ -4329,12 +4350,10 @@ fn linkAtPin(
.semantic_prompt_boundary = true,
}) orelse return null;
- var strmap: terminal.StringMap = undefined;
- self.alloc.free(try screen.selectionString(self.alloc, .{
+ const strmap = try screen.selectionStringMap(self.alloc, .{
.sel = line,
.trim = false,
- .map = &strmap,
- }));
+ });
defer strmap.deinit(self.alloc);
for (self.config.links) |link| {
@@ -4405,7 +4424,7 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
log.warn("failed to get URI for OSC8 hyperlink", .{});
return false;
};
- try self.openUrl(.{ .kind = .unknown, .url = uri });
+ try self.openUrl(.{ .kind = .osc8, .url = uri });
},
}
@@ -4428,7 +4447,6 @@ fn openUrl(
// apprts to handle this themselves.
log.warn("apprt did not handle open URL action, falling back to default opener", .{});
try internal_os.open(
- self.alloc,
action.kind,
action.url,
);
@@ -4474,8 +4492,8 @@ pub fn mousePressureCallback(
if (self.mouse.click_state[left_idx] == .press and
stage == .deep)
select: {
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
const sel = self.mouse.selection_gesture.deepPress(
self.renderer_state.terminal,
@@ -4540,8 +4558,8 @@ pub fn cursorPosCallback(
try self.queueRender();
}
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
// No mouse point so we don't highlight links
self.renderer_state.mouse.point = null;
@@ -4567,8 +4585,8 @@ pub fn cursorPosCallback(
self.mouse.over_link = false;
// We are reading/writing state for the remainder
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
// Update our mouse state. We set this to null initially because we only
// want to set it when we're not selecting or doing any other mouse
@@ -4824,8 +4842,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
// CSI/ESC triggers a scroll.
{
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
self.scrollToBottom() catch |err| {
log.warn("error scrolling to bottom err={}", .{err});
};
@@ -4833,10 +4851,10 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
},
.text => |data| {
- // For text we always allocate just because its easier to
- // handle all cases that way.
- const buf = try self.alloc.alloc(u8, data.len);
- defer self.alloc.free(buf);
+ var stack = std.heap.stackFallback(256, self.alloc);
+ const alloc = stack.get();
+ const buf = try alloc.alloc(u8, data.len);
+ defer alloc.free(buf);
const text = configpkg.string.parse(buf, data) catch |err| {
log.warn(
"error parsing text binding text={s} err={}",
@@ -4851,8 +4869,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
// Text triggers a scroll.
{
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
self.scrollToBottom() catch |err| {
log.warn("error scrolling to bottom err={}", .{err});
};
@@ -4864,8 +4882,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
// in cursor keys mode. We're in "normal" mode if cursor
// keys mode is NOT set.
const normal = normal: {
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
// With the lock held, we must scroll to the bottom.
// We always scroll to the bottom for these inputs.
@@ -4884,8 +4902,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
},
.reset => {
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
self.renderer_state.terminal.fullReset();
},
@@ -4955,7 +4973,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
terminal.search.Thread.threadMain,
.{&s.state},
);
- s.thread.setName("search") catch {};
+ s.thread.setName(global.io(), "search") catch {};
break :init s;
};
@@ -4968,6 +4986,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
}
_ = s.state.mailbox.push(
+ global.io(),
.{ .change_needle = try .init(
self.alloc,
text,
@@ -4980,6 +4999,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.navigate_search => |nav| {
const s: *Search = if (self.search) |*s| s else return false;
_ = s.state.mailbox.push(
+ global.io(),
.{ .select = switch (nav) {
.next => .next,
.previous => .prev,
@@ -4990,8 +5010,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
},
.copy_to_clipboard => |format| {
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
if (self.io.terminal.screens.active.selection) |sel| {
try self.copySelectionToClipboards(
@@ -5022,8 +5042,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
if (!self.mouse.over_link) return false;
const pos = try self.rt_surface.getCursorPos();
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
if (try self.linkAtPos(pos)) |link_info| {
const url_text = switch (link_info.action) {
.open => url_text: {
@@ -5068,15 +5088,15 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
{},
),
- .paste_from_clipboard => return try self.startClipboardRequest(
+ .paste_from_clipboard => return (try self.startClipboardRequest(
.standard,
- .{ .paste = {} },
- ),
+ .{ .paste = .standard },
+ )) == .started,
- .paste_from_selection => return try self.startClipboardRequest(
+ .paste_from_selection => return (try self.startClipboardRequest(
.selection,
- .{ .paste = {} },
- ),
+ .{ .paste = .selection },
+ )) == .started,
.increase_font_size => |delta| {
// Max delta is somewhat arbitrary.
@@ -5141,6 +5161,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.tab,
),
+ .prompt_window_title => return try self.rt_app.performAction(
+ .{ .surface = self },
+ .prompt_title,
+ .window,
+ ),
+
.set_surface_title => |v| {
const title = try self.alloc.dupeZ(u8, v);
defer self.alloc.free(title);
@@ -5161,6 +5187,16 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
);
},
+ .set_window_title => |v| {
+ const title = try self.alloc.dupeZ(u8, v);
+ defer self.alloc.free(title);
+ return try self.rt_app.performAction(
+ .{ .surface = self },
+ .set_window_title,
+ .{ .title = title },
+ );
+ },
+
.clear_screen => {
// This is a duplicate of some of the logic in termio.clearScreen
// but we need to do this here so we can know the answer before
@@ -5168,8 +5204,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
// alternate screen then clear screen does nothing so we want to
// return false so the keybind can be unconsumed.
{
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
if (self.io.terminal.screens.active_key == .alternate) return false;
}
@@ -5192,8 +5228,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.scroll_to_row => |n| {
{
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
const t: *terminal.Terminal = self.renderer_state.terminal;
t.screens.active.scroll(.{ .row = n });
}
@@ -5203,8 +5239,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.scroll_to_selection => {
{
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
const sel = self.io.terminal.screens.active.selection orelse return false;
const tl = sel.topLeft(self.io.terminal.screens.active);
self.io.terminal.screens.active.scroll(.{ .pin = tl });
@@ -5300,6 +5336,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.{ .amount = position },
),
+ .move_tab_to_new_window => return try self.rt_app.performAction(
+ .{ .surface = self },
+ .move_tab_to_new_window,
+ {},
+ ),
+
.new_split => |direction| return try self.rt_app.performAction(
.{ .surface = self },
.new_split,
@@ -5442,8 +5484,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
),
.select_all => {
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
const sel = self.io.terminal.screens.active.selectAll();
if (sel) |s| {
@@ -5564,7 +5606,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.main => @panic("crash binding action, crashing intentionally"),
.render => {
- _ = self.renderer_thread.mailbox.push(.{ .crash = {} }, .{ .forever = {} });
+ _ = self.renderer_thread.mailbox.push(global.io(), .{ .crash = {} }, .{ .forever = {} });
self.queueRender() catch |err| {
// Not a big deal if this fails.
log.warn("failed to notify renderer of crash message err={}", .{err});
@@ -5575,8 +5617,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
},
.adjust_selection => |direction| {
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
const screen: *terminal.Screen = self.io.terminal.screens.active;
const sel = if (screen.selection) |*sel| sel else {
@@ -5655,7 +5697,8 @@ fn writeScreenFile(
) !void {
// Create a temporary directory to store our scrollback.
var tmp_dir = try internal_os.TempDir.init();
- errdefer tmp_dir.deinit();
+ var retain_tmp_dir = false;
+ defer if (retain_tmp_dir) tmp_dir.close(.retain) else tmp_dir.deinit();
var filename_buf: [std.fs.max_path_bytes]u8 = undefined;
const filename = try std.fmt.bufPrint(
@@ -5672,23 +5715,24 @@ fn writeScreenFile(
// Open our scrollback file
var file = try tmp_dir.dir.createFile(
+ global.io(),
filename,
switch (builtin.os.tag) {
.windows => .{},
- else => .{ .mode = 0o600 },
+ else => .{ .permissions = .fromMode(0o600) },
},
);
- defer file.close();
+ defer file.close(global.io());
// Screen.dumpString writes byte-by-byte, so buffer it
var buf: [4096]u8 = undefined;
- var file_writer = file.writer(&buf);
+ var file_writer = file.writer(global.io(), &buf);
var buf_writer = &file_writer.interface;
// Write the scrollback contents. This requires a lock.
{
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
// We only dump history if we have history. We still keep
// the file and write the empty file to the pty so that this
@@ -5724,7 +5768,6 @@ fn writeScreenFile(
const sel = sel_ orelse {
// If we have no selection we have no data so we do nothing.
- tmp_dir.deinit();
return;
};
@@ -5751,7 +5794,11 @@ fn writeScreenFile(
// Get the final path
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
- const path = try tmp_dir.dir.realpath(filename, &path_buf);
+ const path = path_buf[0..try tmp_dir.dir.realPathFile(
+ global.io(),
+ filename,
+ &path_buf,
+ )];
switch (write_screen.action) {
.copy => {
@@ -5774,71 +5821,294 @@ fn writeScreenFile(
path,
), .unlocked),
}
+
+ // The action accepted the path, so retain the file for its consumer.
+ retain_tmp_dir = true;
}
+/// The payload for completing a clipboard request with
+/// completeClipboardRequest.
+pub const CompleteClipboard = struct {
+ /// The representations the apprt could serve for the request's MIME
+ /// types. These are immediately copied as needed so they only need
+ /// to live for the duration of the completion call. Requesters that
+ /// only carry text (paste, OSC 52) use the first text-like
+ /// representation.
+ contents: []const terminal.clipboard.Content = &.{},
+
+ /// The listing of MIME types available on the clipboard, only
+ /// gathered when the request asked for it.
+ available: []const []const u8 = &.{},
+
+ /// True if any clipboard confirmation prompt was already answered
+ /// by the user, skipping further prompts:
+ ///
+ /// - For "regular" pasting this means that unsafe pastes are
+ /// allowed. Unsafe data is defined as data that contains
+ /// newlines, though this definition may change later to detect
+ /// other scenarios.
+ ///
+ /// - For OSC 52 and Kitty clipboard protocol reads and writes no
+ /// prompt is shown to the user when this is true.
+ confirmed: bool = false,
+
+ /// True if the user asked to remember their decision. This is only
+ /// honored by request types that support session grants (Kitty
+ /// clipboard protocol requests carrying a password).
+ remember: bool = false,
+};
+
/// Call this to complete a clipboard request sent to apprt. This should
-/// only be called once for each request. The data is immediately copied so
-/// it is safe to free the data after this call.
-///
-/// If `confirmed` is true then any clipboard confirmation prompts are skipped:
-///
-/// - For "regular" pasting this means that unsafe pastes are allowed. Unsafe
-/// data is defined as data that contains newlines, though this definition
-/// may change later to detect other scenarios.
-///
-/// - For OSC 52 reads and writes no prompt is shown to the user if
-/// `confirmed` is true.
+/// only be called once for each request.
///
-/// If `confirmed` is false then this may return either an UnsafePaste or
-/// UnauthorizedPaste error, depending on the type of clipboard request.
+/// If `complete.confirmed` is false then this may return either an
+/// UnsafePaste or UnauthorizedPaste error, depending on the type of
+/// clipboard request. The request state remains alive in that case so
+/// the apprt can run its confirmation flow.
pub fn completeClipboardRequest(
self: *Surface,
req: apprt.ClipboardRequest,
- data: [:0]const u8,
- confirmed: bool,
+ complete: CompleteClipboard,
) !void {
switch (req) {
- .paste => try self.completeClipboardPaste(data, confirmed),
+ .paste => try self.completeClipboardPaste(
+ clipboardTextContent(complete.contents) orelse "",
+ complete.confirmed,
+ ),
+
+ .list => |clipboard| if (!try self.completeClipboardPasteEvent(
+ clipboard,
+ complete.available,
+ )) {
+ log.debug("mode 5522 paste event was not written", .{});
+ },
.osc_52_read => |clipboard| try self.completeClipboardReadOSC52(
- data,
+ clipboardTextContent(complete.contents) orelse "",
clipboard,
- confirmed,
+ complete.confirmed,
),
- .osc_52_write => |clipboard| try self.rt_surface.setClipboard(clipboard, &.{.{
- .mime = "text/plain",
- .data = data,
- }}, !confirmed),
+ .osc_52_write => |clipboard| {
+ // The write API wants sentinel-terminated data; the write
+ // text round-tripped through the apprt confirmation flow as
+ // a plain representation.
+ const data = try self.alloc.dupeZ(
+ u8,
+ clipboardTextContent(complete.contents) orelse "",
+ );
+ defer self.alloc.free(data);
+ try self.rt_surface.setClipboard(clipboard, &.{.{
+ .mime = "text/plain",
+ .data = data,
+ }}, !complete.confirmed);
+ },
+
+ .kitty_read => |kitty| {
+ // If we need confirmation we return an error without
+ // consuming the request state; the apprt keeps it alive
+ // for the confirmation flow. A session grant carried by
+ // the request skips the prompt, and a request with no
+ // data types is exempt from prompting entirely; see
+ // readPromptExempt.
+ if (self.config.clipboard_read == .ask and
+ !complete.confirmed and
+ !kitty.granted and
+ !terminal.kitty.clipboard.readPromptExempt(kitty.mimes.len))
+ {
+ return error.UnauthorizedPaste;
+ }
+
+ // Past the confirmation check the request is consumed:
+ // every path from here, including errors, must destroy it.
+ defer kitty.destroy();
+
+ // Record a session grant when the user asked to remember
+ // their decision and the request carried a usable
+ // password. The grants live with the terminal state on
+ // the IO thread.
+ if (complete.remember and kitty.pw.len > 0) {
+ const pw = try self.alloc.dupe(u8, kitty.pw);
+ self.queueIo(.{ .kitty_clipboard_grant_read = .{
+ .alloc = self.alloc,
+ .pw = pw,
+ } }, .unlocked);
+ }
+
+ try self.completeKittyClipboardRead(
+ kitty,
+ complete.contents,
+ complete.available,
+ );
+ },
+
+ .kitty_write => |kitty| {
+ // If we need confirmation we return an error without
+ // consuming the request state; the apprt keeps it alive
+ // for the confirmation flow. A session grant carried by
+ // the request skips the prompt.
+ if (self.config.clipboard_write == .ask and
+ !complete.confirmed and
+ !kitty.granted)
+ {
+ return error.UnauthorizedPaste;
+ }
+
+ // Past the confirmation check the request is consumed:
+ // every path from here, including errors, must destroy it.
+ defer kitty.destroy();
+
+ // Record a session grant when the user asked to remember
+ // their decision and the request carried a usable
+ // password. The grants live with the terminal state on
+ // the IO thread.
+ if (complete.remember and kitty.pw.len > 0) {
+ const pw = try self.alloc.dupe(u8, kitty.pw);
+ self.queueIo(.{ .kitty_clipboard_grant_write = .{
+ .alloc = self.alloc,
+ .pw = pw,
+ } }, .unlocked);
+ }
+
+ // Apply the committed representations carried by the
+ // request itself; any contents echoed back by the apprt
+ // are only what its confirmation prompt displayed. An
+ // empty commit clears the clipboard, which the apprt
+ // write API expresses as a single empty text entry.
+ self.rt_surface.setClipboard(
+ kitty.location,
+ if (kitty.contents.len > 0) kitty.contents else &.{.{
+ .mime = "text/plain",
+ .data = "",
+ }},
+ false,
+ ) catch |err| {
+ log.err("error setting clipboard err={}", .{err});
+ try self.kittyClipboardStatus(
+ .write,
+ kitty.id,
+ kitty.terminator,
+ .EIO,
+ );
+ return;
+ };
+
+ try self.kittyClipboardStatus(
+ .write,
+ kitty.id,
+ kitty.terminator,
+ .DONE,
+ );
+ },
+ }
+}
+
+/// The first text-like representation of the contents, if any.
+fn clipboardTextContent(contents: []const terminal.clipboard.Content) ?[]const u8 {
+ for (contents) |content| {
+ if (terminal.clipboard.isTextMime(content.mime)) return content.data;
+ }
+ return null;
+}
+
+/// Deny an in-flight clipboard request. This consumes the request: for
+/// request types whose protocol expects an answer, the denial reply is
+/// written to the pty.
+pub fn denyClipboardRequest(self: *Surface, req: apprt.ClipboardRequest) void {
+ switch (req) {
+ // A denied paste simply doesn't happen.
+ .paste, .list => {},
+
+ // OSC 52 has no error responses, but the client is waiting on
+ // a reply, so a denied read is answered with empty contents.
+ .osc_52_read => |clipboard| self.completeClipboardReadOSC52(
+ "",
+ clipboard,
+ true,
+ ) catch |err| {
+ log.warn("error replying to OSC 52 clipboard read err={}", .{err});
+ },
+
+ // A denied write simply doesn't happen.
+ .osc_52_write => {},
+
+ // The Kitty clipboard protocol reports denial explicitly.
+ .kitty_read => |kitty| {
+ defer kitty.destroy();
+ self.kittyClipboardStatus(
+ .read,
+ kitty.id,
+ kitty.terminator,
+ .EPERM,
+ ) catch |err| {
+ log.warn("error replying to kitty clipboard read err={}", .{err});
+ };
+ },
+
+ .kitty_write => |kitty| {
+ defer kitty.destroy();
+ self.kittyClipboardStatus(
+ .write,
+ kitty.id,
+ kitty.terminator,
+ .EPERM,
+ ) catch |err| {
+ log.warn("error replying to kitty clipboard write err={}", .{err});
+ };
+ },
}
}
/// This starts a clipboard request, with some basic validation. For example,
/// an OSC 52 request is not actually requested if OSC 52 is disabled.
///
-/// Returns true if the request was started, false if it was not (e.g., clipboard
-/// doesn't contain text for paste requests). This allows performable keybinds
-/// to pass through when the action cannot be performed.
+/// The result reports whether the request was started; requests that
+/// weren't started never complete. Callers own reacting to that, e.g.
+/// performable paste keybinds pass through and Kitty clipboard reads
+/// answer the program.
fn startClipboardRequest(
self: *Surface,
loc: apprt.Clipboard,
req: apprt.ClipboardRequest,
-) !bool {
- switch (req) {
- .paste => {}, // always allowed
+) !apprt.ClipboardReadResult {
+ const effective_req: apprt.ClipboardRequest = switch (req) {
+ .paste => |clipboard| effective: {
+ // Snapshot the mode before asking the apprt for clipboard data.
+ // Event pastes request only a MIME listing, while ordinary
+ // pastes request the text representation as before.
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ const event = self.io.terminal.modes.get(.kitty_paste_events);
+ self.renderer_state.mutex.unlock(global.io());
+
+ break :effective if (event)
+ .{ .list = clipboard }
+ else
+ req;
+ },
+ else => req,
+ };
+
+ switch (effective_req) {
+ .paste, .list => {}, // always allowed
.osc_52_read => if (self.config.clipboard_read == .deny) {
log.info(
"application attempted to read clipboard, but 'clipboard-read' is set to deny",
.{},
);
- return false;
+ return .unsupported;
},
- // No clipboard write code paths travel through this function
+ // The clipboard access policies were already applied by
+ // kittyClipboardRead and kittyClipboardWrite, which own
+ // replying on denial.
+ .kitty_read, .kitty_write => {},
+
+ // OSC 52 writes don't travel through this function; they go
+ // straight to the apprt setClipboard API.
.osc_52_write => unreachable,
}
- return try self.rt_surface.clipboardRequest(loc, req);
+ return try self.rt_surface.clipboardRequest(loc, effective_req);
}
fn completeClipboardPaste(
@@ -5849,8 +6119,8 @@ fn completeClipboardPaste(
if (data.len == 0) return;
const encode_opts: input.paste.Options = encode_opts: {
- self.renderer_state.mutex.lock();
- defer self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
const opts: input.paste.Options = .fromTerminal(&self.io.terminal);
// If we have paste protection enabled, we detect unsafe pastes and return
@@ -5920,6 +6190,63 @@ fn completeClipboardPaste(
};
}
+/// Send a Kitty clipboard-protocol paste event when mode 5522 is enabled.
+/// The event only lists the available MIME types; it does not read any of
+/// their data. The shared terminal paste implementation generates and records
+/// the one-time password used by the program's follow-up OSC 5522 read.
+fn completeClipboardPasteEvent(
+ self: *Surface,
+ clipboard: apprt.Clipboard,
+ available: []const []const u8,
+) !bool {
+ if (self.readonly) return false;
+
+ const kitty_clipboard = terminal.kitty.clipboard;
+ const location: terminal.clipboard.Location = switch (clipboard) {
+ .standard => .standard,
+ .selection => .selection,
+ .primary => .primary,
+ };
+
+ // The protocol implementation caps listings at this size too. Cap here
+ // so the temporary Content array stays on the stack.
+ var contents_buf: [kitty_clipboard.max_listing_mimes]terminal.clipboard.Content = undefined;
+ const contents_len = @min(available.len, contents_buf.len);
+ for (available[0..contents_len], contents_buf[0..contents_len]) |mime, *content| {
+ content.* = .{ .mime = mime, .data = "" };
+ }
+
+ var aw: std.Io.Writer.Allocating = .init(self.alloc);
+ defer aw.deinit();
+
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
+
+ const pasted = try terminal.paste.paste(.{
+ .terminal = &self.io.terminal,
+ .alloc = self.alloc,
+ .writer = &aw.writer,
+ .kitty_clipboard = .{
+ .grants = &self.io.terminal_stream.handler.kitty_clipboard_grants,
+ .io = global.io(),
+ },
+ }, .{
+ .source = .{ .clipboard = location },
+ .contents = .{ .memory = contents_buf[0..contents_len] },
+ // A paste event discloses no clipboard data, so unsafe-text
+ // confirmation does not apply. If mode 5522 is reset, the empty
+ // stand-in representations cause the shared helper to write nothing.
+ .allow_unsafe = true,
+ });
+ if (!pasted) return false;
+
+ self.queueIo(.{ .write_alloc = .{
+ .alloc = self.alloc,
+ .data = try aw.toOwnedSlice(),
+ } }, .locked);
+ return true;
+}
+
fn completeClipboardReadOSC52(
self: *Surface,
data: []const u8,
@@ -5940,8 +6267,8 @@ fn completeClipboardReadOSC52(
// This must hold the base64 encoded data PLUS the OSC code surrounding it.
const enc = std.base64.standard.Encoder;
const size = enc.calcSize(data.len);
- var buf = try self.alloc.alloc(u8, size + 9); // const for OSC
- defer self.alloc.free(buf);
+ const buf = try self.alloc.alloc(u8, size + 9); // const for OSC
+ errdefer self.alloc.free(buf);
const kind: u8 = switch (clipboard_type) {
.standard => 'c',
@@ -5959,10 +6286,177 @@ fn completeClipboardReadOSC52(
const encoded = enc.encode(buf[prefix.len..], data);
assert(encoded.len == size);
- self.queueIo(try termio.Message.writeReq(
- self.alloc,
- buf,
- ), .unlocked);
+ self.queueIo(.{ .write_alloc = .{
+ .alloc = self.alloc,
+ .data = buf,
+ } }, .unlocked);
+}
+
+/// Handle a Kitty clipboard protocol (OSC 5522) read request forwarded
+/// by the IO thread. This takes ownership of the request state.
+fn kittyClipboardRead(
+ self: *Surface,
+ req: *apprt.ClipboardRequest.KittyRead,
+) !void {
+ // A read denied by policy answers EPERM so clients degrade
+ // gracefully instead of waiting on a response that never comes.
+ if (self.config.clipboard_read == .deny) {
+ defer req.destroy();
+ log.info("application attempted to read clipboard, but 'clipboard-read' is set to deny", .{});
+ try self.kittyClipboardStatus(.read, req.id, req.terminator, .EPERM);
+ return;
+ }
+
+ const result = self.startClipboardRequest(
+ req.location,
+ .{ .kitty_read = req },
+ ) catch |err| {
+ defer req.destroy();
+ self.kittyClipboardStatus(.read, req.id, req.terminator, .EIO) catch {};
+ return err;
+ };
+
+ switch (result) {
+ // The request completes asynchronously.
+ .started => {},
+
+ // The clipboard has nothing we can serve, which is a
+ // successful read that serves no representations. This never
+ // prompts even under an ask policy since there are no contents
+ // to disclose.
+ .unavailable => {
+ defer req.destroy();
+ try self.completeKittyClipboardRead(req, &.{}, &.{});
+ },
+
+ // The apprt can't serve this clipboard at all, e.g. an
+ // unsupported primary selection.
+ .unsupported => {
+ defer req.destroy();
+ try self.kittyClipboardStatus(.read, req.id, req.terminator, .ENOSYS);
+ },
+ }
+}
+
+/// Handle a committed Kitty clipboard protocol (OSC 5522) write
+/// transaction forwarded by the IO thread. This takes ownership of the
+/// request state.
+fn kittyClipboardWrite(
+ self: *Surface,
+ req: *apprt.ClipboardRequest.KittyWrite,
+) !void {
+ // A write denied by policy answers EPERM so clients degrade
+ // gracefully instead of waiting on a response that never comes.
+ // The IO thread already fails transactions that begin under a
+ // deny policy, but the policy may have changed mid-transaction.
+ if (self.config.clipboard_write == .deny) {
+ defer req.destroy();
+ log.info("application attempted to write clipboard, but 'clipboard-write' is set to deny", .{});
+ try self.kittyClipboardStatus(.write, req.id, req.terminator, .EPERM);
+ return;
+ }
+
+ const result = self.startClipboardRequest(
+ req.location,
+ .{ .kitty_write = req },
+ ) catch |err| {
+ defer req.destroy();
+ self.kittyClipboardStatus(.write, req.id, req.terminator, .EIO) catch {};
+ return err;
+ };
+
+ switch (result) {
+ // The request completes asynchronously.
+ .started => {},
+
+ // The apprt can't write this clipboard at all, e.g. an
+ // unsupported primary selection. Writes carry their own
+ // contents so there is no meaningful unavailable state; treat
+ // it the same.
+ .unavailable, .unsupported => {
+ defer req.destroy();
+ try self.kittyClipboardStatus(.write, req.id, req.terminator, .ENOSYS);
+ },
+ }
+}
+
+/// Reply to a Kitty clipboard request with a single status packet.
+fn kittyClipboardStatus(
+ self: *Surface,
+ op: terminal.kitty.clipboard.Operation,
+ id: []const u8,
+ terminator: terminal.osc.Terminator,
+ status: terminal.kitty.clipboard.Status,
+) error{ OutOfMemory, WriteFailed }!void {
+ var aw: std.Io.Writer.Allocating = .init(self.alloc);
+ defer aw.deinit();
+ try (terminal.kitty.clipboard.Response{
+ .op = op,
+ .status = status,
+ .id = id,
+ .terminator = terminator,
+ }).encode(&aw.writer);
+
+ self.queueIo(.{ .write_alloc = .{
+ .alloc = self.alloc,
+ .data = try aw.toOwnedSlice(),
+ } }, .unlocked);
+}
+
+/// Complete a Kitty clipboard protocol read with the clipboard
+/// contents.
+fn completeKittyClipboardRead(
+ self: *Surface,
+ req: *const apprt.ClipboardRequest.KittyRead,
+ contents: []const terminal.clipboard.Content,
+ available: []const []const u8,
+) !void {
+ const kitty_clipboard = terminal.kitty.clipboard;
+
+ // Serve the requested representations in request order under their
+ // requested names. Text-like MIME aliases all match the canonical
+ // text representation, since that is the only name the apprt
+ // serves text under. Requested types without a representation are
+ // simply never served, which is how the protocol communicates an
+ // unavailable representation.
+ var contents_buf: [kitty_clipboard.max_read_mimes]terminal.clipboard.Content = undefined;
+ var contents_len: usize = 0;
+ for (req.mimes) |mime| {
+ const data: []const u8 = data: {
+ for (contents) |content| {
+ if (std.mem.eql(u8, content.mime, mime)) break :data content.data;
+ if (terminal.clipboard.isTextMime(mime) and
+ terminal.clipboard.isTextMime(content.mime))
+ {
+ break :data content.data;
+ }
+ }
+
+ continue;
+ };
+
+ contents_buf[contents_len] = .{ .mime = mime, .data = data };
+ contents_len += 1;
+ }
+
+ // Encode the full success sequence: the OK packet, the targets
+ // listing if it was requested, DATA chunks for each served
+ // representation, and the final DONE packet.
+ var aw: std.Io.Writer.Allocating = .init(self.alloc);
+ defer aw.deinit();
+ try (kitty_clipboard.ReadSuccess{
+ .primary = req.location == .primary,
+ .id = req.id,
+ .list = req.list,
+ .available = available,
+ .contents = contents_buf[0..contents_len],
+ .terminator = req.terminator,
+ }).encode(&aw.writer);
+
+ self.queueIo(.{ .write_alloc = .{
+ .alloc = self.alloc,
+ .data = try aw.toOwnedSlice(),
+ } }, .unlocked);
}
fn showDesktopNotification(self: *Surface, title: [:0]const u8, body: [:0]const u8) !void {
@@ -5970,12 +6464,12 @@ fn showDesktopNotification(self: *Surface, title: [:0]const u8, body: [:0]const
// how fast identical notifications can be sent sequentially.
const hash_algorithm = std.hash.Wyhash;
- const now = try std.time.Instant.now();
+ const now: std.Io.Timestamp = .now(global.io(), .awake);
// Set a limit of one desktop notification per second so that the OS
// doesn't kill us when we run out of resources.
if (self.app.last_notification_time) |last| {
- if (now.since(last) < 1 * std.time.ns_per_s) {
+ if (last.durationTo(now).toSeconds() < 1) {
log.warn("rate limiting desktop notifications", .{});
return;
}
@@ -5992,7 +6486,7 @@ fn showDesktopNotification(self: *Surface, title: [:0]const u8, body: [:0]const
// notifications with identical content.
if (self.app.last_notification_time) |last| {
if (self.app.last_notification_digest == new_digest) {
- if (now.since(last) < 5 * std.time.ns_per_s) {
+ if (last.durationTo(now).toSeconds() < 5) {
log.warn("suppressing identical desktop notification", .{});
return;
}
@@ -6034,3 +6528,18 @@ fn presentSurface(self: *Surface) !void {
pub fn getProcessInfo(self: *Surface, comptime info: ProcessInfo) ?ProcessInfo.Type(info) {
return self.io.getProcessInfo(info);
}
+
+test "queueIo frees allocated writes in readonly mode" {
+ const testing = std.testing;
+
+ const surface = try testing.allocator.create(Surface);
+ defer testing.allocator.destroy(surface);
+ surface.readonly = true;
+
+ // queueIo must free allocated writes in read-only mode.
+ const data = try testing.allocator.dupe(u8, "\x1b]lGhostty\x1b\\");
+ surface.queueIo(.{ .write_alloc = .{
+ .alloc = testing.allocator,
+ .data = data,
+ } }, .unlocked);
+}