aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/termio/Termio.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/termio/Termio.zig
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/termio/Termio.zig')
-rw-r--r--vendor/libghostty-vt/src/termio/Termio.zig206
1 files changed, 137 insertions, 69 deletions
diff --git a/vendor/libghostty-vt/src/termio/Termio.zig b/vendor/libghostty-vt/src/termio/Termio.zig
index 1b08c5e9..d08f2e5f 100644
--- a/vendor/libghostty-vt/src/termio/Termio.zig
+++ b/vendor/libghostty-vt/src/termio/Termio.zig
@@ -8,18 +8,20 @@ const std = @import("std");
const assert = @import("../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
-const EnvMap = std.process.EnvMap;
+const EnvMap = std.process.Environ.Map;
const posix = std.posix;
const termio = @import("../termio.zig");
const StreamHandler = @import("stream_handler.zig").StreamHandler;
const terminalpkg = @import("../terminal/main.zig");
-const xev = @import("../global.zig").xev;
+const global = @import("../global.zig");
+const xev = global.xev;
const renderer = @import("../renderer.zig");
const apprt = @import("../apprt.zig");
const internal_os = @import("../os/main.zig");
const windows = internal_os.windows;
const configpkg = @import("../config.zig");
const ProcessInfo = @import("../pty.zig").ProcessInfo;
+const compat_file = @import("../lib/compat/file.zig");
const log = std.log.scoped(.io_exec);
@@ -65,7 +67,7 @@ terminal_stream: StreamHandler.Stream,
/// Last time the cursor was reset. This is used to prevent message
/// flooding with cursor resets.
-last_cursor_reset: ?std.time.Instant = null,
+last_cursor_reset: ?std.Io.Timestamp = null,
/// State we have for thread enter. This may be null if we don't need
/// to keep track of any state or if its already been freed.
@@ -117,15 +119,18 @@ const ThreadEnterState = struct {
) (Allocator.Error || error{InputNotFound})![]const Input {
const alloc = self.arena.allocator();
- var input = try alloc.alloc(
- Input,
+ var inputs: std.ArrayList(Input) = try .initCapacity(
+ alloc,
self.input.list.items.len,
);
- for (self.input.list.items, 0..) |item, i| {
- input[i] = switch (item) {
- .raw => |v| .{ .string = try alloc.dupe(u8, v) },
+ errdefer for (inputs.items) |item| item.deinit();
+
+ for (self.input.list.items) |item| {
+ inputs.appendAssumeCapacity(switch (item) {
+ .raw => |v| .{ .string = v },
.path => |path| file: {
- const f = std.fs.cwd().openFile(
+ const f = std.Io.Dir.cwd().openFile(
+ global.io(),
path,
.{},
) catch |err| {
@@ -138,15 +143,22 @@ const ThreadEnterState = struct {
break :file .{ .file = f };
},
- };
+ });
}
- return input;
+ return inputs.items;
}
const Input = union(enum) {
string: []const u8,
- file: std.fs.File,
+ file: std.Io.File,
+
+ fn deinit(self: Input) void {
+ switch (self) {
+ .string => {},
+ .file => |f| f.close(global.io()),
+ }
+ }
};
};
@@ -165,6 +177,7 @@ pub const DerivedConfig = struct {
background: configpkg.Config.Color,
osc_color_report_format: configpkg.Config.OSCColorReportFormat,
clipboard_write: configpkg.ClipboardAccess,
+ clipboard_write_limit: usize,
enquiry_response: []const u8,
conditional_state: configpkg.ConditionalState,
@@ -201,6 +214,7 @@ pub const DerivedConfig = struct {
.background = config.background,
.osc_color_report_format = config.@"osc-color-report-format",
.clipboard_write = config.@"clipboard-write",
+ .clipboard_write_limit = config.@"clipboard-write-limit-bytes".value,
.enquiry_response = try alloc.dupe(u8, config.@"enquiry-response"),
.conditional_state = config._conditional_state,
@@ -238,13 +252,16 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
};
// Create our terminal
- var term = try terminalpkg.Terminal.init(alloc, opts: {
+ var term = try terminalpkg.Terminal.init(global.io(), alloc, opts: {
const grid_size = opts.size.grid();
break :opts .{
.cols = grid_size.columns,
.rows = grid_size.rows,
- .max_scrollback = opts.full_config.@"scrollback-limit",
+ .max_scrollback_bytes = opts.full_config.@"scrollback-limit-bytes".optional(),
+ .max_scrollback_lines = opts.full_config.@"scrollback-limit-lines".optional(),
.default_modes = default_modes,
+ .default_cursor_style = opts.config.cursor_style,
+ .default_cursor_blink = opts.config.cursor_blink,
.colors = .{
.background = .init(opts.config.background.toTerminalRGB()),
.foreground = .init(opts.config.foreground.toTerminalRGB()),
@@ -253,16 +270,17 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
const rgb = color.toTerminalRGB() orelse break :cursor .unset;
break :cursor .init(rgb);
},
- .palette = .init(opts.config.palette),
+ .palette = .default,
},
.kitty_image_storage_limit = opts.config.image_storage_limit,
- .kitty_image_loading_limits = .all,
+ .kitty_image_loading_limits = .allWithTempDir(global.tmpDirPath()),
};
});
errdefer term.deinit(alloc);
- // Set our default cursor style
- term.screens.active.cursor.cursor_style = opts.config.cursor_style;
+ // The default palette may be an allocator-owned copy, so it is set
+ // once the terminal owns its memory and can release it on deinit.
+ try term.colors.palette.changeDefault(alloc, opts.config.palette);
// Setup our terminal size in pixels for certain requests.
term.width_px = term.cols * opts.size.cell.width;
@@ -285,9 +303,8 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
.terminal = &self.terminal,
.osc_color_report_format = opts.config.osc_color_report_format,
.clipboard_write = opts.config.clipboard_write,
+ .clipboard_write_limit = opts.config.clipboard_write_limit,
.enquiry_response = opts.config.enquiry_response,
- .default_cursor_style = opts.config.cursor_style,
- .default_cursor_blink = opts.config.cursor_blink,
};
const thread_enter_state = try ThreadEnterState.create(
@@ -306,7 +323,10 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
.size = opts.size,
.backend = backend,
.mailbox = opts.mailbox,
- .terminal_stream = .initAlloc(alloc, handler),
+ .terminal_stream = .init(.{
+ .allocator = alloc,
+ .handler = handler,
+ }),
.thread_enter_state = thread_enter_state,
};
}
@@ -342,6 +362,9 @@ pub fn threadEnter(
try v.prepareInput()
else
null;
+ defer if (inputs) |items| {
+ for (items) |input| input.deinit();
+ };
data.* = .{
.alloc = self.alloc,
@@ -362,19 +385,21 @@ pub fn threadEnter(
log.warn("failed to queue input string err={}", .{err});
return error.InputFailed;
},
- .file => |f| self.queueWrite(
- data,
- f.readToEndAlloc(
+ .file => |f| {
+ const contents = compat_file.readToEndAlloc(
+ f,
self.alloc,
10 * 1024 * 1024, // 10 MiB max
) catch |err| {
log.warn("failed to read input file err={}", .{err});
return error.InputFailed;
- },
- false,
- ) catch |err| {
- log.warn("failed to queue input file err={}", .{err});
- return error.InputFailed;
+ };
+ defer self.alloc.free(contents);
+
+ self.queueWrite(data, contents, false) catch |err| {
+ log.warn("failed to queue input file err={}", .{err});
+ return error.InputFailed;
+ };
},
};
}
@@ -422,8 +447,8 @@ pub fn changeConfig(self: *Termio, td: *ThreadData, config: *DerivedConfig) !voi
// The remainder of this function is modifying terminal state or
// the read thread data, all of which requires holding the renderer
// state 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());
// Deinit our old config. We do this in the lock because the
// stream handler may be referencing the old config (i.e. enquiry resp)
@@ -442,8 +467,16 @@ pub fn changeConfig(self: *Termio, td: *ThreadData, config: *DerivedConfig) !voi
// - command, working-directory: we never restart the underlying
// process so we don't care or need to know about these.
- // Update the default palette.
- self.terminal.colors.palette.changeDefault(config.palette);
+ // Update the default palette. A config change must not fail here, so
+ // if we can't allocate the copy of the configured palette we fall back
+ // to the built-in default, which never allocates.
+ self.terminal.colors.palette.changeDefault(
+ self.alloc,
+ config.palette,
+ ) catch |err| {
+ log.warn("error changing default palette, using built-in default err={}", .{err});
+ self.terminal.colors.palette.resetDefault(self.alloc);
+ };
self.terminal.flags.dirty.palette = true;
// Update all our other colors
@@ -455,8 +488,8 @@ pub fn changeConfig(self: *Termio, td: *ThreadData, config: *DerivedConfig) !voi
};
// Set the image limits
- try self.terminal.setKittyGraphicsSizeLimit(self.alloc, config.image_storage_limit);
- self.terminal.setKittyGraphicsLoadingLimits(.all);
+ self.terminal.setKittyGraphicsSizeLimit(self.alloc, config.image_storage_limit);
+ self.terminal.setKittyGraphicsLoadingLimits(.allWithTempDir(global.tmpDirPath()));
}
/// Resize the terminal.
@@ -473,24 +506,22 @@ pub fn resize(
// Enter the critical area that we want to keep small
{
- 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 the size of our terminal state
try self.terminal.resize(
self.alloc,
- grid_size.columns,
- grid_size.rows,
+ .{
+ .cols = grid_size.columns,
+ .rows = grid_size.rows,
+ .cell_size_px = .{
+ .width = self.size.cell.width,
+ .height = self.size.cell.height,
+ },
+ },
);
- // Update our pixel sizes
- self.terminal.width_px = grid_size.columns * self.size.cell.width;
- self.terminal.height_px = grid_size.rows * self.size.cell.height;
-
- // Disable synchronized output mode so that we show changes
- // immediately for a resize. This is allowed by the spec.
- self.terminal.modes.set(.synchronized_output, false);
-
// If we have size reporting enabled we need to send a report.
if (self.terminal.modes.get(.in_band_size_reports)) {
try self.sizeReportLocked(td, .mode_2048);
@@ -498,14 +529,14 @@ pub fn resize(
}
// Mail the renderer so that it can update the GPU and re-render
- _ = self.renderer_mailbox.push(.{ .resize = size }, .{ .forever = {} });
+ _ = self.renderer_mailbox.push(global.io(), .{ .resize = size }, .{ .forever = {} });
self.renderer_wakeup.notify() catch {};
}
/// Make a size report.
pub fn sizeReport(self: *Termio, td: *ThreadData, style: termio.Message.SizeReport) !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());
try self.sizeReportLocked(td, style);
}
@@ -534,8 +565,8 @@ fn sizeReportLocked(self: *Termio, td: *ThreadData, style: termio.Message.SizeRe
/// Reset the synchronized output mode. This is usually called by timer
/// expiration from the termio thread.
pub fn resetSynchronizedOutput(self: *Termio) 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());
self.terminal.modes.set(.synchronized_output, false);
self.renderer_wakeup.notify() catch {};
}
@@ -543,8 +574,8 @@ pub fn resetSynchronizedOutput(self: *Termio) void {
/// Clear the screen.
pub fn clearScreen(self: *Termio, td: *ThreadData, history: 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 we're on the alternate screen, we do not clear. Since this is an
// emulator-level screen clear, this messes up the running programs
@@ -573,6 +604,7 @@ pub fn clearScreen(self: *Termio, td: *ThreadData, history: bool) !void {
// all of them. We delete all of them for now but if this behavior
// isn't fully correct we should fix this later.
self.terminal.screens.active.kitty_images.delete(
+ self.terminal.io(),
self.terminal.screens.active.alloc,
&self.terminal,
.{ .all = true },
@@ -600,16 +632,16 @@ pub fn scrollViewport(
self: *Termio,
scroll: terminalpkg.Terminal.ScrollViewport,
) 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());
self.terminal.scrollViewport(scroll);
}
/// Jump the viewport to the prompt.
pub fn jumpToPrompt(self: *Termio, delta: isize) !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());
self.terminal.screens.active.scroll(.{ .delta_prompt = delta });
}
@@ -618,9 +650,9 @@ pub fn jumpToPrompt(self: *Termio, delta: isize) !void {
/// Called when focus is gained or lost (when focus events are enabled)
pub fn focusGained(self: *Termio, td: *ThreadData, focused: bool) !void {
- self.renderer_state.mutex.lock();
+ self.renderer_state.mutex.lockUncancelable(global.io());
const focus_event = self.renderer_state.terminal.modes.get(.focus_event);
- self.renderer_state.mutex.unlock();
+ self.renderer_state.mutex.unlock(global.io());
// If we have focus events enabled, we send the focus event.
if (focus_event) {
@@ -643,8 +675,8 @@ pub fn focusGained(self: *Termio, td: *ThreadData, focused: bool) !void {
pub fn processOutput(self: *Termio, buf: []const u8) void {
// We are modifying terminal state from here on out and we need
// the lock to grab our read data.
- 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.processOutputLocked(buf);
}
@@ -657,19 +689,18 @@ fn processOutputLocked(self: *Termio, buf: []const u8) void {
// non-blink state so it is rendered if visible. If we're under
// HEAVY read load, we don't want to send a ton of these so we
// use a timer under the covers
- if (std.time.Instant.now()) |now| cursor_reset: {
+ const now = std.Io.Timestamp.now(global.io(), .awake);
+ cursor_reset: {
if (self.last_cursor_reset) |last| {
- if (now.since(last) <= (500 * std.time.ns_per_ms)) {
+ if (last.durationTo(now).toMilliseconds() <= 500) {
break :cursor_reset;
}
}
self.last_cursor_reset = now;
- _ = self.renderer_mailbox.push(.{
+ _ = self.renderer_mailbox.push(global.io(), .{
.reset_cursor_blink = {},
}, .{ .instant = {} });
- } else |err| {
- log.warn("failed to get current time err={}", .{err});
}
// If we have an inspector, we enter SLOW MODE because we need to
@@ -701,9 +732,22 @@ fn processOutputLocked(self: *Termio, buf: []const u8) void {
}
/// Sends a DSR response for the current color scheme to the pty.
+/// Record a Kitty clipboard protocol session grant so future requests
+/// carrying the password skip the permission prompt.
+pub fn kittyClipboardGrant(
+ self: *Termio,
+ pw: []const u8,
+ dir: terminalpkg.kitty.clipboard.Grants.Direction,
+) error{OutOfMemory}!void {
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
+
+ try self.terminal_stream.handler.kittyClipboardGrant(pw, dir);
+}
+
pub fn colorSchemeReport(self: *Termio, td: *ThreadData, force: 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());
try self.colorSchemeReportLocked(td, force);
}
@@ -723,6 +767,30 @@ pub fn colorSchemeReportLocked(self: *Termio, td: *ThreadData, force: bool) !voi
try self.queueWrite(td, writer.buffered(), false);
}
+/// Sends a visibility report to the pty. Unforced reports are only sent while
+/// DEC mode 2033 is enabled.
+pub fn visibilityReport(
+ self: *Termio,
+ td: *ThreadData,
+ visible: bool,
+ force: bool,
+) !void {
+ self.renderer_state.mutex.lockUncancelable(global.io());
+ defer self.renderer_state.mutex.unlock(global.io());
+
+ if (!force and !self.renderer_state.terminal.modes.get(.report_visibility)) {
+ return;
+ }
+
+ var buf: [terminalpkg.device_status.max_visibility_report_encode_size]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ try terminalpkg.device_status.encodeVisibilityReport(
+ &writer,
+ if (visible) .potentially_visible else .not_visible,
+ );
+ try self.queueWrite(td, writer.buffered(), false);
+}
+
/// ThreadData is the data created and stored in the termio thread
/// when the thread is started and destroyed when the thread is
/// stopped.