diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-10 21:51:57 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-10 21:51:57 +0300 |
| commit | 425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch) | |
| tree | e52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/termio/Termio.zig | |
| parent | 90e947a6e26e02f10ec7f1694c4eb5b78a86406b (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.zig | 206 |
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. |
