mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-19 10:16:12 +03:00
615 lines
21 KiB
Zig
615 lines
21 KiB
Zig
//! 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 builtin = @import("builtin");
|
|
const build_config = @import("../build_config.zig");
|
|
const assert = std.debug.assert;
|
|
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 Command = @import("../Command.zig");
|
|
const Pty = @import("../pty.zig").Pty;
|
|
const SegmentedPool = @import("../segmented_pool.zig").SegmentedPool;
|
|
const StreamHandler = @import("stream_handler.zig").StreamHandler;
|
|
const terminal = @import("../terminal/main.zig");
|
|
const terminfo = @import("../terminfo/main.zig");
|
|
const xev = @import("xev");
|
|
const renderer = @import("../renderer.zig");
|
|
const apprt = @import("../apprt.zig");
|
|
const fastmem = @import("../fastmem.zig");
|
|
const internal_os = @import("../os/main.zig");
|
|
const windows = internal_os.windows;
|
|
const configpkg = @import("../config.zig");
|
|
const shell_integration = @import("shell_integration.zig");
|
|
|
|
const log = std.log.scoped(.io_exec);
|
|
|
|
/// Allocator
|
|
alloc: Allocator,
|
|
|
|
/// This is the pty fd created for the subcommand.
|
|
subprocess: termio.Exec,
|
|
|
|
/// 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: terminal.Terminal,
|
|
|
|
/// The shared render state
|
|
renderer_state: *renderer.State,
|
|
|
|
/// A handle to wake up the renderer. This hints to the renderer that 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 grid size whenever a resize is called.
|
|
grid_size: renderer.GridSize,
|
|
|
|
/// The writer implementation to use.
|
|
writer: termio.Writer,
|
|
|
|
/// The pointer to the read data. This is only valid while the termio thread
|
|
/// is alive. This is protected by the renderer state lock.
|
|
read_data: ?*ReadData = null,
|
|
|
|
/// 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: terminal.color.Palette,
|
|
image_storage_limit: usize,
|
|
cursor_style: terminal.CursorStyle,
|
|
cursor_blink: ?bool,
|
|
cursor_color: ?configpkg.Config.Color,
|
|
foreground: configpkg.Config.Color,
|
|
background: configpkg.Config.Color,
|
|
osc_color_report_format: configpkg.Config.OSCColorReportFormat,
|
|
term: []const u8,
|
|
grapheme_width_method: configpkg.Config.GraphemeWidthMethod,
|
|
abnormal_runtime_threshold_ms: u32,
|
|
wait_after_command: bool,
|
|
enquiry_response: []const u8,
|
|
|
|
pub fn init(
|
|
alloc_gpa: Allocator,
|
|
config: *const configpkg.Config,
|
|
) !DerivedConfig {
|
|
var arena = ArenaAllocator.init(alloc_gpa);
|
|
errdefer arena.deinit();
|
|
const alloc = arena.allocator();
|
|
|
|
return .{
|
|
.palette = config.palette.value,
|
|
.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",
|
|
.term = try alloc.dupe(u8, config.term),
|
|
.grapheme_width_method = config.@"grapheme-width-method",
|
|
.abnormal_runtime_threshold_ms = config.@"abnormal-command-exit-runtime",
|
|
.wait_after_command = config.@"wait-after-command",
|
|
.enquiry_response = try alloc.dupe(u8, config.@"enquiry-response"),
|
|
|
|
// 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(alloc: Allocator, opts: termio.Options) !Termio {
|
|
// Create our terminal
|
|
var term = try terminal.Terminal.init(alloc, .{
|
|
.cols = opts.grid_size.columns,
|
|
.rows = opts.grid_size.rows,
|
|
.max_scrollback = opts.full_config.@"scrollback-limit",
|
|
});
|
|
errdefer term.deinit(alloc);
|
|
term.default_palette = opts.config.palette;
|
|
term.color_palette.colors = opts.config.palette;
|
|
|
|
// 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.config.grapheme_width_method) {
|
|
.unicode => term.modes.set(.grapheme_cluster, true),
|
|
.legacy => {},
|
|
}
|
|
|
|
// Set the image size limits
|
|
try term.screen.kitty_images.setLimit(
|
|
alloc,
|
|
&term.screen,
|
|
opts.config.image_storage_limit,
|
|
);
|
|
try term.secondary_screen.kitty_images.setLimit(
|
|
alloc,
|
|
&term.secondary_screen,
|
|
opts.config.image_storage_limit,
|
|
);
|
|
|
|
// Set default cursor blink settings
|
|
term.modes.set(
|
|
.cursor_blinking,
|
|
opts.config.cursor_blink orelse true,
|
|
);
|
|
|
|
// Set our default cursor style
|
|
term.screen.cursor.cursor_style = opts.config.cursor_style;
|
|
|
|
// Setup our reader.
|
|
// TODO: for manual, we need to set the terminal width/height
|
|
var subprocess = try termio.Exec.init(alloc, opts, &term);
|
|
errdefer subprocess.deinit();
|
|
|
|
return .{
|
|
.alloc = alloc,
|
|
.terminal = term,
|
|
.subprocess = subprocess,
|
|
.config = opts.config,
|
|
.renderer_state = opts.renderer_state,
|
|
.renderer_wakeup = opts.renderer_wakeup,
|
|
.renderer_mailbox = opts.renderer_mailbox,
|
|
.surface_mailbox = opts.surface_mailbox,
|
|
.grid_size = opts.grid_size,
|
|
.writer = opts.writer,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *Termio) void {
|
|
self.subprocess.deinit();
|
|
self.terminal.deinit(self.alloc);
|
|
self.config.deinit();
|
|
self.writer.deinit(self.alloc);
|
|
}
|
|
|
|
pub fn threadEnter(self: *Termio, thread: *termio.Thread, data: *ThreadData) !void {
|
|
const alloc = self.alloc;
|
|
|
|
// Setup our data that is used for callbacks
|
|
var read_data_ptr = try alloc.create(ReadData);
|
|
errdefer alloc.destroy(read_data_ptr);
|
|
|
|
// Wakeup watcher for the writer thread.
|
|
var wakeup = try xev.Async.init();
|
|
errdefer wakeup.deinit();
|
|
|
|
// Create our stream handler
|
|
const handler: StreamHandler = handler: {
|
|
const default_cursor_color = if (self.config.cursor_color) |col|
|
|
col.toTerminalRGB()
|
|
else
|
|
null;
|
|
|
|
break :handler .{
|
|
.alloc = self.alloc,
|
|
.writer = &self.writer,
|
|
.surface_mailbox = self.surface_mailbox,
|
|
.renderer_state = self.renderer_state,
|
|
.renderer_wakeup = self.renderer_wakeup,
|
|
.renderer_mailbox = self.renderer_mailbox,
|
|
.grid_size = &self.grid_size,
|
|
.terminal = &self.terminal,
|
|
.osc_color_report_format = self.config.osc_color_report_format,
|
|
.enquiry_response = self.config.enquiry_response,
|
|
.default_foreground_color = self.config.foreground.toTerminalRGB(),
|
|
.default_background_color = self.config.background.toTerminalRGB(),
|
|
.default_cursor_style = self.config.cursor_style,
|
|
.default_cursor_blink = self.config.cursor_blink,
|
|
.default_cursor_color = default_cursor_color,
|
|
.cursor_color = default_cursor_color,
|
|
.foreground_color = self.config.foreground.toTerminalRGB(),
|
|
.background_color = self.config.background.toTerminalRGB(),
|
|
};
|
|
};
|
|
|
|
// Setup our event data before we start
|
|
read_data_ptr.* = .{
|
|
.renderer_state = self.renderer_state,
|
|
.renderer_wakeup = self.renderer_wakeup,
|
|
.renderer_mailbox = self.renderer_mailbox,
|
|
.terminal_stream = .{
|
|
.handler = handler,
|
|
.parser = .{
|
|
.osc_parser = .{
|
|
// Populate the OSC parser allocator (optional) because
|
|
// we want to support large OSC payloads such as OSC 52.
|
|
.alloc = self.alloc,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
errdefer read_data_ptr.deinit();
|
|
|
|
// Setup our thread data
|
|
data.* = .{
|
|
.alloc = alloc,
|
|
.loop = &thread.loop,
|
|
.renderer_state = self.renderer_state,
|
|
.surface_mailbox = self.surface_mailbox,
|
|
.read_data = read_data_ptr,
|
|
.writer = &self.writer,
|
|
|
|
// Placeholder until setup below
|
|
.reader = .{ .manual = {} },
|
|
};
|
|
|
|
// Setup our reader
|
|
try self.subprocess.threadEnter(alloc, self, data);
|
|
|
|
// Store our read data pointer
|
|
self.renderer_state.mutex.lock();
|
|
defer self.renderer_state.mutex.unlock();
|
|
self.read_data = read_data_ptr;
|
|
}
|
|
|
|
pub fn threadExit(self: *Termio, data: *ThreadData) void {
|
|
self.subprocess.threadExit(data);
|
|
|
|
// Clear our read data pointer
|
|
self.renderer_state.mutex.lock();
|
|
defer self.renderer_state.mutex.unlock();
|
|
self.read_data = null;
|
|
}
|
|
|
|
/// Send a message using the writer. Depending on the writer type in
|
|
/// use this may process now or it may just enqueue and process later.
|
|
///
|
|
/// This will also notify the writer thread to process the message. If
|
|
/// you're sending a lot of messages, it may be more efficient to use
|
|
/// the writer directly and then call notify separately.
|
|
pub fn queueMessage(
|
|
self: *Termio,
|
|
msg: termio.Message,
|
|
mutex: enum { locked, unlocked },
|
|
) void {
|
|
self.writer.send(msg, switch (mutex) {
|
|
.locked => self.renderer_state.mutex,
|
|
.unlocked => null,
|
|
});
|
|
self.writer.notify();
|
|
}
|
|
|
|
/// Queue a write directly to the pty.
|
|
///
|
|
/// If you're using termio.Thread, this must ONLY be called from the
|
|
/// writer thread. If you're not on the thread, use queueMessage with
|
|
/// writer 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.subprocess.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.
|
|
td.read_data.terminal_stream.handler.changeConfig(&self.config);
|
|
td.reader.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. Note this will only apply to new colors drawn
|
|
// since we decode all palette colors to RGB on usage.
|
|
self.terminal.default_palette = config.palette;
|
|
|
|
// Update the active palette, except for any colors that were modified with
|
|
// OSC 4
|
|
for (0..config.palette.len) |i| {
|
|
if (!self.terminal.color_palette.mask.isSet(i)) {
|
|
self.terminal.color_palette.colors[i] = config.palette[i];
|
|
self.terminal.flags.dirty.palette = true;
|
|
}
|
|
}
|
|
|
|
// Set the image size limits
|
|
try self.terminal.screen.kitty_images.setLimit(
|
|
self.alloc,
|
|
&self.terminal.screen,
|
|
config.image_storage_limit,
|
|
);
|
|
try self.terminal.secondary_screen.kitty_images.setLimit(
|
|
self.alloc,
|
|
&self.terminal.secondary_screen,
|
|
config.image_storage_limit,
|
|
);
|
|
}
|
|
|
|
/// Resize the terminal.
|
|
pub fn resize(
|
|
self: *Termio,
|
|
grid_size: renderer.GridSize,
|
|
screen_size: renderer.ScreenSize,
|
|
padding: renderer.Padding,
|
|
) !void {
|
|
// Update the size of our pty.
|
|
const padded_size = screen_size.subPadding(padding);
|
|
try self.subprocess.resize(grid_size, padded_size);
|
|
|
|
// Update our cached grid size
|
|
self.grid_size = grid_size;
|
|
|
|
// 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 = padded_size.width;
|
|
self.terminal.height_px = padded_size.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);
|
|
|
|
// Wake up our renderer so any changes will be shown asap
|
|
self.renderer_wakeup.notify() catch {};
|
|
}
|
|
}
|
|
|
|
/// 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.active_screen == .alternate) return;
|
|
|
|
// 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.screen.cursor.y > 0) {
|
|
self.terminal.screen.eraseRows(
|
|
.{ .active = .{ .y = 0 } },
|
|
.{ .active = .{ .y = self.terminal.screen.cursor.y - 1 } },
|
|
);
|
|
}
|
|
|
|
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.
|
|
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: terminal.Terminal.ScrollViewport) !void {
|
|
self.renderer_state.mutex.lock();
|
|
defer self.renderer_state.mutex.unlock();
|
|
try 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.screen.scroll(.{ .delta_prompt = delta });
|
|
}
|
|
|
|
try self.renderer_wakeup.notify();
|
|
}
|
|
|
|
/// Called when the child process exited abnormally but before
|
|
/// the surface is notified.
|
|
pub fn childExitedAbnormally(self: *Termio, exit_code: u32, runtime_ms: u64) !void {
|
|
self.renderer_state.mutex.lock();
|
|
defer self.renderer_state.mutex.unlock();
|
|
const t = self.renderer_state.terminal;
|
|
try self.subprocess.childExitedAbnormally(self.alloc, t, exit_code, runtime_ms);
|
|
}
|
|
|
|
/// 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();
|
|
|
|
// If we don't have read data, we can't process it.
|
|
const rd = self.read_data orelse return error.ReadDataNull;
|
|
processOutputLocked(rd, buf);
|
|
}
|
|
|
|
/// Process output when you ahve the read data pointer.
|
|
pub fn processOutputReadData(rd: *ReadData, buf: []const u8) void {
|
|
rd.renderer_state.mutex.lock();
|
|
defer rd.renderer_state.mutex.unlock();
|
|
processOutputLocked(rd, buf);
|
|
}
|
|
|
|
/// Process output from readdata but the lock is already held.
|
|
fn processOutputLocked(rd: *ReadData, buf: []const u8) void {
|
|
// Schedule a render. We can call this first because we have the lock.
|
|
rd.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 (rd.last_cursor_reset) |last| {
|
|
if (now.since(last) <= (500 / std.time.ns_per_ms)) {
|
|
break :cursor_reset;
|
|
}
|
|
}
|
|
|
|
rd.last_cursor_reset = now;
|
|
_ = rd.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 (rd.renderer_state.inspector) |insp| {
|
|
for (buf, 0..) |byte, i| {
|
|
insp.recordPtyRead(buf[i .. i + 1]) catch |err| {
|
|
log.err("error recording pty read in inspector err={}", .{err});
|
|
};
|
|
|
|
rd.terminal_stream.next(byte) catch |err|
|
|
log.err("error processing terminal data: {}", .{err});
|
|
}
|
|
} else {
|
|
rd.terminal_stream.nextSlice(buf) catch |err|
|
|
log.err("error processing terminal data: {}", .{err});
|
|
}
|
|
|
|
// If our stream handling caused messages to be sent to the writer
|
|
// thread, then we need to wake it up so that it processes them.
|
|
if (rd.terminal_stream.handler.writer_messaged) {
|
|
rd.terminal_stream.handler.writer_messaged = false;
|
|
// TODO
|
|
// rd.writer_wakeup.notify() catch |err| {
|
|
// log.warn("failed to wake up writer thread err={}", .{err});
|
|
// };
|
|
}
|
|
}
|
|
|
|
/// 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 reader implementation (i.e. pty/exec state)
|
|
reader: termio.reader.ThreadData,
|
|
read_data: *ReadData,
|
|
writer: *termio.Writer,
|
|
|
|
pub fn deinit(self: *ThreadData) void {
|
|
self.reader.deinit(self.alloc);
|
|
self.read_data.deinit();
|
|
self.alloc.destroy(self.read_data);
|
|
self.* = undefined;
|
|
}
|
|
};
|
|
|
|
/// The data required for the read thread.
|
|
pub const ReadData = struct {
|
|
/// 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: terminal.Stream(StreamHandler),
|
|
|
|
/// The shared render state
|
|
renderer_state: *renderer.State,
|
|
|
|
/// A handle to wake up the renderer. This hints to the renderer that that
|
|
/// a repaint should happen.
|
|
renderer_wakeup: xev.Async,
|
|
|
|
/// The mailbox for notifying the renderer of things.
|
|
renderer_mailbox: *renderer.Thread.Mailbox,
|
|
|
|
/// Last time the cursor was reset. This is used to prevent message
|
|
/// flooding with cursor resets.
|
|
last_cursor_reset: ?std.time.Instant = null,
|
|
|
|
pub fn deinit(self: *ReadData) void {
|
|
// Clear any StreamHandler state
|
|
self.terminal_stream.handler.deinit();
|
|
self.terminal_stream.deinit();
|
|
}
|
|
};
|