diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/termio/Termio.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/termio/Termio.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/termio/Termio.zig | 773 |
1 files changed, 773 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/termio/Termio.zig b/vendor/libghostty-vt/src/termio/Termio.zig new file mode 100644 index 00000000..75ccb94b --- /dev/null +++ b/vendor/libghostty-vt/src/termio/Termio.zig @@ -0,0 +1,773 @@ +//! Primary terminal IO ("termio") state. This maintains the terminal state, +//! pty, subprocess, etc. This is flexible enough to be used in environments +//! that don't have a pty and simply provides the input/output using raw +//! bytes. +pub const Termio = @This(); + +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 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 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 log = std.log.scoped(.io_exec); + +/// Mutex state argument for queueMessage. +pub const MutexState = enum { locked, unlocked }; + +/// Allocator +alloc: Allocator, + +/// This is the implementation responsible for io. +backend: termio.Backend, + +/// The derived configuration for this termio implementation. +config: DerivedConfig, + +/// The terminal emulator internal state. This is the abstract "terminal" +/// that manages input, grid updating, etc. and is renderer-agnostic. It +/// just stores internal state about a grid. +terminal: terminalpkg.Terminal, + +/// The shared render state +renderer_state: *renderer.State, + +/// A handle to wake up the renderer. This hints to the renderer that +/// a repaint should happen. +renderer_wakeup: xev.Async, + +/// The mailbox for notifying the renderer of things. +renderer_mailbox: *renderer.Thread.Mailbox, + +/// The mailbox for communicating with the surface. +surface_mailbox: apprt.surface.Mailbox, + +/// The cached size info +size: renderer.Size, + +/// The mailbox implementation to use. +mailbox: termio.Mailbox, + +/// The stream parser. This parses the stream of escape codes and so on +/// from the child process and calls callbacks in the stream handler. +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, + +/// 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. +thread_enter_state: ?*ThreadEnterState = null, + +/// The state we need to keep around only until we enter the IO +/// thread. Then we can throw it all away. +const ThreadEnterState = struct { + arena: ArenaAllocator, + + /// Initial input to send to the subprocess after starting. This + /// memory is freed once the subprocess start is attempted, even + /// if it fails, because Exec only starts once. + input: configpkg.io.RepeatableReadableIO, + + pub fn create( + alloc: Allocator, + config: *const configpkg.Config, + ) !?*ThreadEnterState { + // If we have no input then we have no thread enter state + if (config.input.list.items.len == 0) return null; + + // Create our arena allocator + var arena = ArenaAllocator.init(alloc); + errdefer arena.deinit(); + const arena_alloc = arena.allocator(); + + // Allocate our ThreadEnterState + const ptr = try arena_alloc.create(ThreadEnterState); + + // Copy the input from the config + const input = try config.input.cloneParsed(arena_alloc); + + // Return the initialized state + ptr.* = .{ + .arena = arena, + .input = input, + }; + return ptr; + } + + pub fn destroy(self: *ThreadEnterState) void { + self.arena.deinit(); + } + + /// Prepare the inputs for use. Allocations happen on the arena. + pub fn prepareInput( + self: *ThreadEnterState, + ) (Allocator.Error || error{InputNotFound})![]const Input { + const alloc = self.arena.allocator(); + + var input = try alloc.alloc( + Input, + 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) }, + .path => |path| file: { + const f = std.fs.cwd().openFile( + path, + .{}, + ) catch |err| { + log.warn("failed to open input file={s} err={}", .{ + path, + err, + }); + return error.InputNotFound; + }; + + break :file .{ .file = f }; + }, + }; + } + + return input; + } + + const Input = union(enum) { + string: []const u8, + file: std.fs.File, + }; +}; + +/// The configuration for this IO that is derived from the main +/// configuration. This must be exported so that we don't need to +/// pass around Config pointers which makes memory management a pain. +pub const DerivedConfig = struct { + arena: ArenaAllocator, + + palette: terminalpkg.color.Palette, + image_storage_limit: usize, + cursor_style: terminalpkg.CursorStyle, + cursor_blink: ?bool, + cursor_color: ?configpkg.Config.TerminalColor, + foreground: configpkg.Config.Color, + background: configpkg.Config.Color, + osc_color_report_format: configpkg.Config.OSCColorReportFormat, + clipboard_write: configpkg.ClipboardAccess, + enquiry_response: []const u8, + conditional_state: configpkg.ConditionalState, + + pub fn init( + alloc_gpa: Allocator, + config: *const configpkg.Config, + ) !DerivedConfig { + var arena = ArenaAllocator.init(alloc_gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + const palette: terminalpkg.color.Palette = palette: { + if (config.@"palette-generate") generate: { + if (config.palette.mask.findFirstSet() == null) { + // If the user didn't set any values manually, then + // we're using the default palette and we don't need + // to apply the generation code to it. + break :generate; + } + + break :palette terminalpkg.color.generate256Color(config.palette.value, config.palette.mask, config.background.toTerminalRGB(), config.foreground.toTerminalRGB(), config.@"palette-harmonious"); + } + + break :palette config.palette.value; + }; + + return .{ + .palette = palette, + .image_storage_limit = config.@"image-storage-limit", + .cursor_style = config.@"cursor-style", + .cursor_blink = config.@"cursor-style-blink", + .cursor_color = config.@"cursor-color", + .foreground = config.foreground, + .background = config.background, + .osc_color_report_format = config.@"osc-color-report-format", + .clipboard_write = config.@"clipboard-write", + .enquiry_response = try alloc.dupe(u8, config.@"enquiry-response"), + .conditional_state = config._conditional_state, + + // This has to be last so that we copy AFTER the arena allocations + // above happen (Zig assigns in order). + .arena = arena, + }; + } + + pub fn deinit(self: *DerivedConfig) void { + self.arena.deinit(); + } +}; + +/// Initialize the termio state. +/// +/// This will also start the child process if the termio is configured +/// to run a child process. +pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void { + // The default terminal modes based on our config. + const default_modes: terminalpkg.ModePacked = modes: { + var modes: terminalpkg.ModePacked = .{}; + + // Setup our initial grapheme cluster support if enabled. We use a + // switch to ensure we get a compiler error if more cases are added. + switch (opts.full_config.@"grapheme-width-method") { + .unicode => modes.grapheme_cluster = true, + .legacy => {}, + } + + // Set default cursor blink settings + modes.cursor_blinking = opts.config.cursor_blink orelse true; + + break :modes modes; + }; + + // Create our terminal + var term = try terminalpkg.Terminal.init(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", + .default_modes = default_modes, + .colors = .{ + .background = .init(opts.config.background.toTerminalRGB()), + .foreground = .init(opts.config.foreground.toTerminalRGB()), + .cursor = cursor: { + const color = opts.config.cursor_color orelse break :cursor .unset; + const rgb = color.toTerminalRGB() orelse break :cursor .unset; + break :cursor .init(rgb); + }, + .palette = .init(opts.config.palette), + }, + }; + }); + errdefer term.deinit(alloc); + + // Set the image size limits + var it = term.screens.all.iterator(); + while (it.next()) |entry| { + const screen: *terminalpkg.Screen = entry.value.*; + try screen.kitty_images.setLimit( + alloc, + screen, + opts.config.image_storage_limit, + ); + } + + // Set our default cursor style + term.screens.active.cursor.cursor_style = opts.config.cursor_style; + + // Setup our terminal size in pixels for certain requests. + term.width_px = term.cols * opts.size.cell.width; + term.height_px = term.rows * opts.size.cell.height; + + // Setup our backend. + var backend = opts.backend; + backend.initTerminal(&term); + + // Create our stream handler. This points to memory in self so it + // isn't safe to use until self.* is set. + const handler: StreamHandler = .{ + .alloc = alloc, + .termio_mailbox = &self.mailbox, + .surface_mailbox = opts.surface_mailbox, + .renderer_state = opts.renderer_state, + .renderer_wakeup = opts.renderer_wakeup, + .renderer_mailbox = opts.renderer_mailbox, + .size = &self.size, + .terminal = &self.terminal, + .osc_color_report_format = opts.config.osc_color_report_format, + .clipboard_write = opts.config.clipboard_write, + .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( + alloc, + opts.full_config, + ); + + self.* = .{ + .alloc = alloc, + .terminal = term, + .config = opts.config, + .renderer_state = opts.renderer_state, + .renderer_wakeup = opts.renderer_wakeup, + .renderer_mailbox = opts.renderer_mailbox, + .surface_mailbox = opts.surface_mailbox, + .size = opts.size, + .backend = backend, + .mailbox = opts.mailbox, + .terminal_stream = .initAlloc(alloc, handler), + .thread_enter_state = thread_enter_state, + }; +} + +pub fn deinit(self: *Termio) void { + self.backend.deinit(); + self.terminal.deinit(self.alloc); + self.config.deinit(); + self.mailbox.deinit(self.alloc); + + // Clear any StreamHandler state + self.terminal_stream.deinit(); + + // Clear any initial state if we have it + if (self.thread_enter_state) |v| v.destroy(); +} + +pub fn threadEnter( + self: *Termio, + thread: *termio.Thread, + data: *ThreadData, +) !void { + // Always free our thread enter state when we're done. + defer if (self.thread_enter_state) |v| { + v.destroy(); + self.thread_enter_state = null; + }; + + // If we have thread enter state then we're going to validate + // and set that all up now so that we can error before we actually + // start the command and pty. + const inputs: ?[]const ThreadEnterState.Input = if (self.thread_enter_state) |v| + try v.prepareInput() + else + null; + + data.* = .{ + .alloc = self.alloc, + .loop = &thread.loop, + .renderer_state = self.renderer_state, + .surface_mailbox = self.surface_mailbox, + .mailbox = &self.mailbox, + .backend = undefined, // Backend must replace this on threadEnter + }; + + // Setup our backend + try self.backend.threadEnter(self.alloc, self, data); + errdefer self.backend.threadExit(data); + + // If we have inputs, then queue them all up. + for (inputs orelse &.{}) |input| switch (input) { + .string => |v| self.queueWrite(data, v, false) catch |err| { + log.warn("failed to queue input string err={}", .{err}); + return error.InputFailed; + }, + .file => |f| self.queueWrite( + data, + f.readToEndAlloc( + 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; + }, + }; +} + +pub fn threadExit(self: *Termio, data: *ThreadData) void { + self.backend.threadExit(data); +} + +/// Send a message to the mailbox. Depending on the mailbox type in use +/// this may process now or it may just enqueue and process later. +/// +/// This will also notify the mailbox thread to process the message. If +/// you're sending a lot of messages, it may be more efficient to use +/// the mailbox directly and then call notify separately. +pub fn queueMessage( + self: *Termio, + msg: termio.Message, + mutex: MutexState, +) void { + self.mailbox.send(msg, switch (mutex) { + .locked => self.renderer_state.mutex, + .unlocked => null, + }); + self.mailbox.notify(); +} + +/// Queue a write directly to the pty. +/// +/// If you're using termio.Thread, this must ONLY be called from the +/// mailbox thread. If you're not on the thread, use queueMessage with +/// mailbox messages instead. +/// +/// If you're not using termio.Thread, this is not threadsafe. +pub inline fn queueWrite( + self: *Termio, + td: *ThreadData, + data: []const u8, + linefeed: bool, +) !void { + try self.backend.queueWrite(self.alloc, td, data, linefeed); +} + +/// Update the configuration. +pub fn changeConfig(self: *Termio, td: *ThreadData, config: *DerivedConfig) !void { + // 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(); + + // Deinit our old config. We do this in the lock because the + // stream handler may be referencing the old config (i.e. enquiry resp) + self.config.deinit(); + self.config = config.*; + + // Update our stream handler. The stream handler uses the same + // renderer mutex so this is safe to do despite being executed + // from another thread. + self.terminal_stream.handler.changeConfig(&self.config); + td.backend.changeConfig(&self.config); + + // Update the configuration that we know about. + // + // Specific things we don't update: + // - 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); + self.terminal.flags.dirty.palette = true; + + // Update all our other colors + self.terminal.colors.background.default = config.background.toTerminalRGB(); + self.terminal.colors.foreground.default = config.foreground.toTerminalRGB(); + self.terminal.colors.cursor.default = cursor: { + const color = config.cursor_color orelse break :cursor null; + break :cursor color.toTerminalRGB() orelse break :cursor null; + }; + + // Set the image size limits + var it = self.terminal.screens.all.iterator(); + while (it.next()) |entry| { + const screen: *terminalpkg.Screen = entry.value.*; + try screen.kitty_images.setLimit( + self.alloc, + screen, + config.image_storage_limit, + ); + } +} + +/// Resize the terminal. +pub fn resize( + self: *Termio, + td: *ThreadData, + size: renderer.Size, +) !void { + self.size = size; + const grid_size = size.grid(); + + // Update the size of our pty. + try self.backend.resize(grid_size, size.terminal()); + + // Enter the critical area that we want to keep small + { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + // Update the size of our terminal state + try self.terminal.resize( + self.alloc, + grid_size.columns, + grid_size.rows, + ); + + // 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); + } + } + + // Mail the renderer so that it can update the GPU and re-render + _ = self.renderer_mailbox.push(.{ .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(); + try self.sizeReportLocked(td, style); +} + +fn sizeReportLocked(self: *Termio, td: *ThreadData, style: termio.Message.SizeReport) !void { + const grid_size = self.size.grid(); + const report_size: terminalpkg.size_report.Size = .{ + .rows = grid_size.rows, + .columns = grid_size.columns, + .cell_width = self.size.cell.width, + .cell_height = self.size.cell.height, + }; + + // 1024 bytes should be enough for size report since report + // in columns and pixels. + var buf: [1024]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try terminalpkg.size_report.encode( + &writer, + style, + report_size, + ); + + try self.queueWrite(td, writer.buffered(), false); +} + +/// 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.terminal.modes.set(.synchronized_output, false); + self.renderer_wakeup.notify() catch {}; +} + +/// Clear the screen. +pub fn clearScreen(self: *Termio, td: *ThreadData, history: bool) !void { + { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + // 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 + // knowledge of where the cursor is and causes rendering issues. So, + // for alt screen, we do nothing. + if (self.terminal.screens.active_key == .alternate) return; + + // Clear our selection + self.terminal.screens.active.clearSelection(); + + // Clear our scrollback + if (history) self.terminal.eraseDisplay(.scrollback, false); + + // If we're not at a prompt, we just delete above the cursor. + if (!self.terminal.cursorIsAtPrompt()) { + if (self.terminal.screens.active.cursor.y > 0) { + self.terminal.screens.active.eraseActive( + self.terminal.screens.active.cursor.y - 1, + ); + } + + // Clear all Kitty graphics state for this screen. This copies + // Kitty's behavior when Cmd+K deletes all Kitty graphics. I + // didn't spend time researching whether it only deletes Kitty + // graphics that are placed above the cursor or if it deletes + // 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.screens.active.alloc, + &self.terminal, + .{ .all = true }, + ); + + return; + } + + // At a prompt, we want to first fully clear the screen, and then after + // send a FF (0x0C) to the shell so that it can repaint the screen. + // Mark the current row as a not a prompt so we can properly + // clear the full screen in the next eraseDisplay call. + // TODO: fix this + // self.terminal.markSemanticPrompt(.command); + // assert(!self.terminal.cursorIsAtPrompt()); + self.terminal.eraseDisplay(.complete, false); + } + + // If we reached here it means we're at a prompt, so we send a form-feed. + try self.queueWrite(td, &[_]u8{0x0C}, false); +} + +/// Scroll the viewport +pub fn scrollViewport( + self: *Termio, + scroll: terminalpkg.Terminal.ScrollViewport, +) void { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + 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.terminal.screens.active.scroll(.{ .delta_prompt = delta }); + } + + try self.renderer_wakeup.notify(); +} + +/// 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(); + const focus_event = self.renderer_state.terminal.modes.get(.focus_event); + self.renderer_state.mutex.unlock(); + + // If we have focus events enabled, we send the focus event. + if (focus_event) { + var buf: [terminalpkg.focus.max_encode_size]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + terminalpkg.focus.encode(&writer, if (focused) .gained else .lost) catch |err| { + log.err("error encoding focus event err={}", .{err}); + return; + }; + try self.queueWrite(td, writer.buffered(), false); + } + + // We always notify our backend of focus changes. + try self.backend.focusGained(td, focused); +} + +/// Process output from the pty. This is the manual API that users can +/// call with pty data but it is also called by the read thread when using +/// an exec subprocess. +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.processOutputLocked(buf); +} + +/// Process output from readdata but the lock is already held. +fn processOutputLocked(self: *Termio, buf: []const u8) void { + // Schedule a render. We can call this first because we have the lock. + self.terminal_stream.handler.queueRender() catch unreachable; + + // Whenever a character is typed, we ensure the cursor is in the + // 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: { + if (self.last_cursor_reset) |last| { + if (now.since(last) <= (500 * std.time.ns_per_ms)) { + break :cursor_reset; + } + } + + self.last_cursor_reset = now; + _ = self.renderer_mailbox.push(.{ + .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 + // process a byte at a time alternating between the inspector handler + // and the termio handler. This is very slow compared to our optimizations + // below but at least users only pay for it if they're using the inspector. + if (self.renderer_state.inspector) |insp| { + for (buf, 0..) |byte, i| { + insp.recordPtyRead( + self.alloc, + &self.terminal, + buf[i .. i + 1], + ) catch |err| { + log.err("error recording pty read in inspector err={}", .{err}); + }; + + self.terminal_stream.next(byte); + } + } else { + self.terminal_stream.nextSlice(buf); + } + + // If our stream handling caused messages to be sent to the mailbox + // thread, then we need to wake it up so that it processes them. + if (self.terminal_stream.handler.termio_messaged) { + self.terminal_stream.handler.termio_messaged = false; + self.mailbox.notify(); + } +} + +/// Sends a DSR response for the current color scheme to the pty. +pub fn colorSchemeReport(self: *Termio, td: *ThreadData, force: bool) !void { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + try self.colorSchemeReportLocked(td, force); +} + +pub fn colorSchemeReportLocked(self: *Termio, td: *ThreadData, force: bool) !void { + if (!force and !self.renderer_state.terminal.modes.get(.report_color_scheme)) { + return; + } + const output = switch (self.config.conditional_state.theme) { + .light => "\x1B[?997;2n", + .dark => "\x1B[?997;1n", + }; + try self.queueWrite(td, output, false); +} + +/// ThreadData is the data created and stored in the termio thread +/// when the thread is started and destroyed when the thread is +/// stopped. +/// +/// All of the fields in this struct should only be read/written by +/// the termio thread. As such, a lock is not necessary. +pub const ThreadData = struct { + /// Allocator used for the event data + alloc: Allocator, + + /// The event loop associated with this thread. This is owned by + /// the Thread but we have a pointer so we can queue new work to it. + loop: *xev.Loop, + + /// The shared render state + renderer_state: *renderer.State, + + /// Mailboxes for different threads + surface_mailbox: apprt.surface.Mailbox, + + /// Data associated with the backend implementation (i.e. pty/exec state) + backend: termio.backend.ThreadData, + mailbox: *termio.Mailbox, + + pub fn deinit(self: *ThreadData) void { + self.backend.deinit(self.alloc); + self.* = undefined; + } +}; + +/// Get information about the process(es) attached to the backend. Returns +/// `null` if there was an error getting the information or the information is +/// not available on a particular platform. +pub fn getProcessInfo(self: *Termio, comptime info: ProcessInfo) ?ProcessInfo.Type(info) { + return self.backend.getProcessInfo(info); +} |
