mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
983 lines
32 KiB
Zig
983 lines
32 KiB
Zig
//! Implementation of IO that uses child exec to talk to the child process.
|
|
pub const Exec = @This();
|
|
|
|
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
const assert = std.debug.assert;
|
|
const Allocator = std.mem.Allocator;
|
|
const termio = @import("../termio.zig");
|
|
const Command = @import("../Command.zig");
|
|
const Window = @import("../Window.zig");
|
|
const Pty = @import("../Pty.zig");
|
|
const SegmentedPool = @import("../segmented_pool.zig").SegmentedPool;
|
|
const terminal = @import("../terminal/main.zig");
|
|
const xev = @import("xev");
|
|
const renderer = @import("../renderer.zig");
|
|
const tracy = @import("tracy");
|
|
const trace = tracy.trace;
|
|
const fastmem = @import("../fastmem.zig");
|
|
|
|
const log = std.log.scoped(.io_exec);
|
|
|
|
const c = @cImport({
|
|
@cInclude("errno.h");
|
|
@cInclude("signal.h");
|
|
@cInclude("unistd.h");
|
|
});
|
|
|
|
/// Allocator
|
|
alloc: Allocator,
|
|
|
|
/// This is the pty fd created for the subcommand.
|
|
subprocess: Subprocess,
|
|
|
|
/// 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 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,
|
|
|
|
/// The mailbox for communicating with the window.
|
|
window_mailbox: Window.Mailbox,
|
|
|
|
/// The cached grid size whenever a resize is called.
|
|
grid_size: renderer.GridSize,
|
|
|
|
/// The data associated with the currently running thread.
|
|
data: ?*EventData,
|
|
|
|
/// Initialize the exec implementation. This will also start the child
|
|
/// process.
|
|
pub fn init(alloc: Allocator, opts: termio.Options) !Exec {
|
|
// Create our terminal
|
|
var term = try terminal.Terminal.init(
|
|
alloc,
|
|
opts.grid_size.columns,
|
|
opts.grid_size.rows,
|
|
);
|
|
errdefer term.deinit(alloc);
|
|
term.color_palette = opts.config.palette.value;
|
|
|
|
var subprocess = try Subprocess.init(alloc, opts);
|
|
errdefer subprocess.deinit();
|
|
|
|
return Exec{
|
|
.alloc = alloc,
|
|
.terminal = term,
|
|
.terminal_stream = undefined,
|
|
.subprocess = subprocess,
|
|
.renderer_state = opts.renderer_state,
|
|
.renderer_wakeup = opts.renderer_wakeup,
|
|
.renderer_mailbox = opts.renderer_mailbox,
|
|
.window_mailbox = opts.window_mailbox,
|
|
.grid_size = opts.grid_size,
|
|
.data = null,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *Exec) void {
|
|
self.subprocess.deinit(self.alloc);
|
|
|
|
// Clean up our other members
|
|
self.terminal.deinit(self.alloc);
|
|
}
|
|
|
|
pub fn threadEnter(self: *Exec, thread: *termio.Thread) !ThreadData {
|
|
assert(self.data == null);
|
|
const alloc = self.alloc;
|
|
|
|
// Start our subprocess
|
|
try self.subprocess.start(alloc);
|
|
errdefer self.subprocess.stop();
|
|
const master_fd = self.subprocess.pty.?.master;
|
|
|
|
// Setup our data that is used for callbacks
|
|
var ev_data_ptr = try alloc.create(EventData);
|
|
errdefer alloc.destroy(ev_data_ptr);
|
|
|
|
// Setup our stream so that we can write.
|
|
var stream = xev.Stream.initFd(master_fd);
|
|
errdefer stream.deinit();
|
|
|
|
// Wakeup watcher for the writer thread.
|
|
var wakeup = try xev.Async.init();
|
|
errdefer wakeup.deinit();
|
|
|
|
// Setup our event data before we start
|
|
ev_data_ptr.* = .{
|
|
.writer_mailbox = thread.mailbox,
|
|
.writer_wakeup = thread.wakeup,
|
|
.renderer_state = self.renderer_state,
|
|
.renderer_wakeup = self.renderer_wakeup,
|
|
.renderer_mailbox = self.renderer_mailbox,
|
|
.data_stream = stream,
|
|
.loop = &thread.loop,
|
|
.terminal_stream = .{
|
|
.handler = .{
|
|
.alloc = self.alloc,
|
|
.ev = ev_data_ptr,
|
|
.terminal = &self.terminal,
|
|
.grid_size = &self.grid_size,
|
|
.window_mailbox = self.window_mailbox,
|
|
},
|
|
},
|
|
};
|
|
errdefer ev_data_ptr.deinit(self.alloc);
|
|
|
|
// Store our data so our callbacks can access it
|
|
self.data = ev_data_ptr;
|
|
|
|
// Start our reader thread
|
|
const read_thread = try std.Thread.spawn(
|
|
.{},
|
|
ReadThread.threadMain,
|
|
.{ master_fd, ev_data_ptr },
|
|
);
|
|
read_thread.setName("io-reader") catch {};
|
|
|
|
// Return our thread data
|
|
return ThreadData{
|
|
.alloc = alloc,
|
|
.ev = ev_data_ptr,
|
|
.read_thread = read_thread,
|
|
};
|
|
}
|
|
|
|
pub fn threadExit(self: *Exec, data: ThreadData) void {
|
|
// Clear out our data since we're not active anymore.
|
|
self.data = null;
|
|
|
|
// Stop our subprocess
|
|
self.subprocess.stop();
|
|
|
|
// Wait for our reader thread to end
|
|
data.read_thread.join();
|
|
}
|
|
|
|
/// Resize the terminal.
|
|
pub fn resize(
|
|
self: *Exec,
|
|
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);
|
|
}
|
|
}
|
|
|
|
pub inline fn queueWrite(self: *Exec, data: []const u8) !void {
|
|
const ev = self.data.?;
|
|
|
|
// We go through and chunk the data if necessary to fit into
|
|
// our cached buffers that we can queue to the stream.
|
|
var i: usize = 0;
|
|
while (i < data.len) {
|
|
const req = try ev.write_req_pool.get();
|
|
const buf = try ev.write_buf_pool.get();
|
|
const end = @min(data.len, i + buf.len);
|
|
fastmem.copy(u8, buf, data[i..end]);
|
|
ev.data_stream.queueWrite(
|
|
ev.loop,
|
|
&ev.write_queue,
|
|
req,
|
|
.{ .slice = buf[0..(end - i)] },
|
|
EventData,
|
|
ev,
|
|
ttyWrite,
|
|
);
|
|
|
|
i = end;
|
|
}
|
|
}
|
|
|
|
const ThreadData = struct {
|
|
/// Allocator used for the event data
|
|
alloc: Allocator,
|
|
|
|
/// The data that is attached to the callbacks.
|
|
ev: *EventData,
|
|
|
|
/// Our read thread
|
|
read_thread: std.Thread,
|
|
|
|
pub fn deinit(self: *ThreadData) void {
|
|
self.ev.deinit(self.alloc);
|
|
self.alloc.destroy(self.ev);
|
|
self.* = undefined;
|
|
}
|
|
};
|
|
|
|
const EventData = struct {
|
|
// The preallocation size for the write request pool. This should be big
|
|
// enough to satisfy most write requests. It must be a power of 2.
|
|
const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5);
|
|
|
|
/// Mailbox for data to the writer thread.
|
|
writer_mailbox: *termio.Mailbox,
|
|
writer_wakeup: xev.Async,
|
|
|
|
/// 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,
|
|
|
|
/// The data stream is the main IO for the pty.
|
|
data_stream: xev.Stream,
|
|
|
|
/// The event loop,
|
|
loop: *xev.Loop,
|
|
|
|
/// The write queue for the data stream.
|
|
write_queue: xev.Stream.WriteQueue = .{},
|
|
|
|
/// This is the pool of available (unused) write requests. If you grab
|
|
/// one from the pool, you must put it back when you're done!
|
|
write_req_pool: SegmentedPool(xev.Stream.WriteRequest, WRITE_REQ_PREALLOC) = .{},
|
|
|
|
/// The pool of available buffers for writing to the pty.
|
|
write_buf_pool: SegmentedPool([64]u8, WRITE_REQ_PREALLOC) = .{},
|
|
|
|
/// Last time the cursor was reset. This is used to prevent message
|
|
/// flooding with cursor resets.
|
|
last_cursor_reset: u64 = 0,
|
|
|
|
pub fn deinit(self: *EventData, alloc: Allocator) void {
|
|
// Clear our write pools. We know we aren't ever going to do
|
|
// any more IO since we stop our data stream below so we can just
|
|
// drop this.
|
|
self.write_req_pool.deinit(alloc);
|
|
self.write_buf_pool.deinit(alloc);
|
|
|
|
// Stop our data stream
|
|
self.data_stream.deinit();
|
|
}
|
|
|
|
/// This queues a render operation with the renderer thread. The render
|
|
/// isn't guaranteed to happen immediately but it will happen as soon as
|
|
/// practical.
|
|
inline fn queueRender(self: *EventData) !void {
|
|
try self.renderer_wakeup.notify();
|
|
}
|
|
};
|
|
|
|
fn ttyWrite(
|
|
ev_: ?*EventData,
|
|
_: *xev.Loop,
|
|
_: *xev.Completion,
|
|
_: xev.Stream,
|
|
_: xev.WriteBuffer,
|
|
r: xev.Stream.WriteError!usize,
|
|
) xev.CallbackAction {
|
|
const ev = ev_.?;
|
|
ev.write_req_pool.put();
|
|
ev.write_buf_pool.put();
|
|
|
|
const d = r catch |err| {
|
|
log.err("write error: {}", .{err});
|
|
return .disarm;
|
|
};
|
|
_ = d;
|
|
//log.info("WROTE: {d}", .{status});
|
|
|
|
return .disarm;
|
|
}
|
|
|
|
const Subprocess = struct {
|
|
cwd: ?[]const u8,
|
|
env: std.process.EnvMap,
|
|
path: []const u8,
|
|
argv0_override: ?[]const u8,
|
|
grid_size: renderer.GridSize,
|
|
screen_size: renderer.ScreenSize,
|
|
|
|
pty: ?Pty = null,
|
|
command: ?Command = null,
|
|
|
|
pub fn init(alloc: Allocator, opts: termio.Options) !Subprocess {
|
|
// Determine the path to the binary we're executing
|
|
const path = (try Command.expandPath(alloc, opts.config.command orelse "sh")) orelse
|
|
return error.CommandNotFound;
|
|
errdefer alloc.free(path);
|
|
|
|
// On macOS, we launch the program as a login shell. This is a Mac-specific
|
|
// behavior (see other terminals). Terminals in general should NOT be
|
|
// spawning login shells because well... we're not "logging in." The solution
|
|
// is to put dotfiles in "rc" variants rather than "_login" variants. But,
|
|
// history!
|
|
const argv0_override: ?[]const u8 = if (comptime builtin.target.isDarwin()) argv0: {
|
|
// Get rid of the path
|
|
const argv0 = if (std.mem.lastIndexOf(u8, path, "/")) |idx|
|
|
path[idx + 1 ..]
|
|
else
|
|
path;
|
|
|
|
// Copy it with a hyphen so its a login shell
|
|
const argv0_buf = try alloc.alloc(u8, argv0.len + 1);
|
|
argv0_buf[0] = '-';
|
|
std.mem.copy(u8, argv0_buf[1..], argv0);
|
|
break :argv0 argv0_buf;
|
|
} else null;
|
|
errdefer if (argv0_override) |buf| alloc.free(buf);
|
|
|
|
// Set our env vars
|
|
var env = try std.process.getEnvMap(alloc);
|
|
errdefer env.deinit();
|
|
try env.put("TERM", "xterm-256color");
|
|
try env.put("COLORTERM", "truecolor");
|
|
|
|
return .{
|
|
.env = env,
|
|
.cwd = opts.config.@"working-directory",
|
|
.path = path,
|
|
.argv0_override = argv0_override,
|
|
.grid_size = opts.grid_size,
|
|
.screen_size = opts.screen_size,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *Subprocess, alloc: Allocator) void {
|
|
self.stop();
|
|
self.env.deinit();
|
|
alloc.free(self.path);
|
|
if (self.argv0_override) |v| alloc.free(v);
|
|
self.* = undefined;
|
|
}
|
|
|
|
pub fn start(self: *Subprocess, alloc: Allocator) !void {
|
|
// Create our pty
|
|
var pty = try Pty.open(.{
|
|
.ws_row = @intCast(u16, self.grid_size.rows),
|
|
.ws_col = @intCast(u16, self.grid_size.columns),
|
|
.ws_xpixel = @intCast(u16, self.screen_size.width),
|
|
.ws_ypixel = @intCast(u16, self.screen_size.height),
|
|
});
|
|
self.pty = pty;
|
|
errdefer {
|
|
pty.deinit();
|
|
self.pty = null;
|
|
}
|
|
|
|
const args = &[_][]const u8{self.argv0_override orelse self.path};
|
|
|
|
// Build our subcommand
|
|
var cmd: Command = .{
|
|
.path = self.path,
|
|
.args = args,
|
|
.env = &self.env,
|
|
.cwd = self.cwd,
|
|
.stdin = .{ .handle = pty.slave },
|
|
.stdout = .{ .handle = pty.slave },
|
|
.stderr = .{ .handle = pty.slave },
|
|
.pre_exec = (struct {
|
|
fn callback(cmd: *Command) void {
|
|
const p = cmd.getData(Pty) orelse unreachable;
|
|
p.childPreExec() catch |err|
|
|
log.err("error initializing child: {}", .{err});
|
|
}
|
|
}).callback,
|
|
.data = &self.pty.?,
|
|
};
|
|
try cmd.start(alloc);
|
|
errdefer killCommand(cmd);
|
|
log.info("started subcommand path={s} pid={?}", .{ self.path, cmd.pid });
|
|
|
|
self.command = cmd;
|
|
}
|
|
|
|
pub fn stop(self: *Subprocess) void {
|
|
// Close our PTY
|
|
if (self.pty) |*pty| {
|
|
pty.deinit();
|
|
self.pty = null;
|
|
}
|
|
|
|
// Kill our command
|
|
if (self.command) |*cmd| {
|
|
killCommand(cmd) catch |err|
|
|
log.err("error sending SIGHUP to command, may hang: {}", .{err});
|
|
_ = cmd.wait(false) catch |err|
|
|
log.err("error waiting for command to exit: {}", .{err});
|
|
self.command = null;
|
|
}
|
|
}
|
|
|
|
pub fn resize(
|
|
self: *Subprocess,
|
|
grid_size: renderer.GridSize,
|
|
screen_size: renderer.ScreenSize,
|
|
) !void {
|
|
self.grid_size = grid_size;
|
|
self.screen_size = screen_size;
|
|
|
|
if (self.pty) |pty| {
|
|
try pty.setSize(.{
|
|
.ws_row = @intCast(u16, grid_size.rows),
|
|
.ws_col = @intCast(u16, grid_size.columns),
|
|
.ws_xpixel = @intCast(u16, screen_size.width),
|
|
.ws_ypixel = @intCast(u16, screen_size.height),
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Kill the underlying subprocess. This sends a SIGHUP to the child
|
|
/// process. This doesn't wait for the child process to be exited.
|
|
fn killCommand(command: *Command) !void {
|
|
if (command.pid) |pid| {
|
|
const pgid_: ?c.pid_t = pgid: {
|
|
const pgid = c.getpgid(pid);
|
|
|
|
// Don't know why it would be zero but its not a valid pid
|
|
if (pgid == 0) break :pgid null;
|
|
|
|
// If the pid doesn't exist then... okay.
|
|
if (pgid == c.ESRCH) break :pgid null;
|
|
|
|
// If we have an error...
|
|
if (pgid < 0) {
|
|
log.warn("error getting pgid for kill", .{});
|
|
break :pgid null;
|
|
}
|
|
|
|
break :pgid pgid;
|
|
};
|
|
|
|
if (pgid_) |pgid| {
|
|
if (c.killpg(pgid, c.SIGHUP) < 0) {
|
|
log.warn("error killing process group pgid={}", .{pgid});
|
|
return error.KillFailed;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/// The read thread sits in a loop doing the following pseudo code:
|
|
///
|
|
/// while (true) { blocking_read(); exit_if_eof(); process(); }
|
|
///
|
|
/// Almost all terminal-modifying activity is from the pty read, so
|
|
/// putting this on a dedicated thread keeps performance very predictable
|
|
/// while also almost optimal. "Locking is fast, lock contention is slow."
|
|
/// and since we rarely have contention, this is fast.
|
|
///
|
|
/// This is also empirically fast compared to putting the read into
|
|
/// an async mechanism like io_uring/epoll because the reads are generally
|
|
/// small.
|
|
const ReadThread = struct {
|
|
/// The main entrypoint for the thread.
|
|
fn threadMain(fd: std.os.fd_t, ev: *EventData) void {
|
|
var buf: [1024]u8 = undefined;
|
|
while (true) {
|
|
const n = std.os.read(fd, &buf) catch |err| {
|
|
switch (err) {
|
|
// This means our pty is closed. We're probably
|
|
// gracefully shutting down.
|
|
error.NotOpenForReading => log.info("io reader exiting", .{}),
|
|
|
|
else => log.err("READ ERROR err={}", .{err}),
|
|
}
|
|
return;
|
|
};
|
|
|
|
// log.info("DATA: {d}", .{n});
|
|
@call(.always_inline, process, .{ ev, buf[0..n] });
|
|
}
|
|
}
|
|
|
|
fn process(
|
|
ev: *EventData,
|
|
buf: []const u8,
|
|
) void {
|
|
const zone = trace(@src());
|
|
defer zone.end();
|
|
|
|
// log.info("DATA: {d}", .{n});
|
|
// log.info("DATA: {any}", .{buf[0..@intCast(usize, n)]});
|
|
|
|
// 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
|
|
// TODO
|
|
// const now = t.loop().now();
|
|
// if (now - ev.last_cursor_reset > 500) {
|
|
// ev.last_cursor_reset = now;
|
|
// _ = ev.renderer_mailbox.push(.{
|
|
// .reset_cursor_blink = {},
|
|
// }, .{ .forever = {} });
|
|
// }
|
|
|
|
// We are modifying terminal state from here on out
|
|
ev.renderer_state.mutex.lock();
|
|
defer ev.renderer_state.mutex.unlock();
|
|
|
|
// Schedule a render
|
|
ev.queueRender() catch unreachable;
|
|
|
|
// Process the terminal data. This is an extremely hot part of the
|
|
// terminal emulator, so we do some abstraction leakage to avoid
|
|
// function calls and unnecessary logic.
|
|
//
|
|
// The ground state is the only state that we can see and print/execute
|
|
// ASCII, so we only execute this hot path if we're already in the ground
|
|
// state.
|
|
//
|
|
// Empirically, this alone improved throughput of large text output by ~20%.
|
|
var i: usize = 0;
|
|
const end = buf.len;
|
|
if (ev.terminal_stream.parser.state == .ground) {
|
|
for (buf[i..end]) |ch| {
|
|
switch (terminal.parse_table.table[ch][@enumToInt(terminal.Parser.State.ground)].action) {
|
|
// Print, call directly.
|
|
.print => ev.terminal_stream.handler.print(@intCast(u21, ch)) catch |err|
|
|
log.err("error processing terminal data: {}", .{err}),
|
|
|
|
// C0 execute, let our stream handle this one but otherwise
|
|
// continue since we're guaranteed to be back in ground.
|
|
.execute => ev.terminal_stream.execute(ch) catch |err|
|
|
log.err("error processing terminal data: {}", .{err}),
|
|
|
|
// Otherwise, break out and go the slow path until we're
|
|
// back in ground. There is a slight optimization here where
|
|
// could try to find the next transition to ground but when
|
|
// I implemented that it didn't materially change performance.
|
|
else => break,
|
|
}
|
|
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
if (i < end) {
|
|
ev.terminal_stream.nextSlice(buf[i..end]) 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 (ev.terminal_stream.handler.writer_messaged) {
|
|
ev.terminal_stream.handler.writer_messaged = false;
|
|
ev.writer_wakeup.notify() catch |err| {
|
|
log.warn("failed to wake up writer thread err={}", .{err});
|
|
};
|
|
}
|
|
}
|
|
};
|
|
|
|
/// This is used as the handler for the terminal.Stream type. This is
|
|
/// stateful and is expected to live for the entire lifetime of the terminal.
|
|
/// It is NOT VALID to stop a stream handler, create a new one, and use that
|
|
/// unless all of the member fields are copied.
|
|
const StreamHandler = struct {
|
|
ev: *EventData,
|
|
alloc: Allocator,
|
|
grid_size: *renderer.GridSize,
|
|
terminal: *terminal.Terminal,
|
|
window_mailbox: Window.Mailbox,
|
|
|
|
/// This is set to true when a message was written to the writer
|
|
/// mailbox. This can be used by callers to determine if they need
|
|
/// to wake up the writer.
|
|
writer_messaged: bool = false,
|
|
|
|
inline fn queueRender(self: *StreamHandler) !void {
|
|
try self.ev.queueRender();
|
|
}
|
|
|
|
inline fn messageWriter(self: *StreamHandler, msg: termio.Message) void {
|
|
_ = self.ev.writer_mailbox.push(msg, .{ .forever = {} });
|
|
self.writer_messaged = true;
|
|
}
|
|
|
|
pub fn print(self: *StreamHandler, ch: u21) !void {
|
|
try self.terminal.print(ch);
|
|
}
|
|
|
|
pub fn printRepeat(self: *StreamHandler, count: usize) !void {
|
|
try self.terminal.printRepeat(count);
|
|
}
|
|
|
|
pub fn bell(self: StreamHandler) !void {
|
|
_ = self;
|
|
log.info("BELL", .{});
|
|
}
|
|
|
|
pub fn backspace(self: *StreamHandler) !void {
|
|
self.terminal.backspace();
|
|
}
|
|
|
|
pub fn horizontalTab(self: *StreamHandler) !void {
|
|
try self.terminal.horizontalTab();
|
|
}
|
|
|
|
pub fn linefeed(self: *StreamHandler) !void {
|
|
// Small optimization: call index instead of linefeed because they're
|
|
// identical and this avoids one layer of function call overhead.
|
|
try self.terminal.index();
|
|
}
|
|
|
|
pub fn carriageReturn(self: *StreamHandler) !void {
|
|
self.terminal.carriageReturn();
|
|
}
|
|
|
|
pub fn setCursorLeft(self: *StreamHandler, amount: u16) !void {
|
|
self.terminal.cursorLeft(amount);
|
|
}
|
|
|
|
pub fn setCursorRight(self: *StreamHandler, amount: u16) !void {
|
|
self.terminal.cursorRight(amount);
|
|
}
|
|
|
|
pub fn setCursorDown(self: *StreamHandler, amount: u16) !void {
|
|
self.terminal.cursorDown(amount);
|
|
}
|
|
|
|
pub fn setCursorUp(self: *StreamHandler, amount: u16) !void {
|
|
self.terminal.cursorUp(amount);
|
|
}
|
|
|
|
pub fn setCursorCol(self: *StreamHandler, col: u16) !void {
|
|
self.terminal.setCursorColAbsolute(col);
|
|
}
|
|
|
|
pub fn setCursorRow(self: *StreamHandler, row: u16) !void {
|
|
if (self.terminal.modes.origin) {
|
|
// TODO
|
|
log.err("setCursorRow: implement origin mode", .{});
|
|
unreachable;
|
|
}
|
|
|
|
self.terminal.setCursorPos(row, self.terminal.screen.cursor.x + 1);
|
|
}
|
|
|
|
pub fn setCursorPos(self: *StreamHandler, row: u16, col: u16) !void {
|
|
self.terminal.setCursorPos(row, col);
|
|
}
|
|
|
|
pub fn eraseDisplay(self: *StreamHandler, mode: terminal.EraseDisplay) !void {
|
|
if (mode == .complete) {
|
|
// Whenever we erase the full display, scroll to bottom.
|
|
try self.terminal.scrollViewport(.{ .bottom = {} });
|
|
try self.queueRender();
|
|
}
|
|
|
|
self.terminal.eraseDisplay(mode);
|
|
}
|
|
|
|
pub fn eraseLine(self: *StreamHandler, mode: terminal.EraseLine) !void {
|
|
self.terminal.eraseLine(mode);
|
|
}
|
|
|
|
pub fn deleteChars(self: *StreamHandler, count: usize) !void {
|
|
try self.terminal.deleteChars(count);
|
|
}
|
|
|
|
pub fn eraseChars(self: *StreamHandler, count: usize) !void {
|
|
self.terminal.eraseChars(count);
|
|
}
|
|
|
|
pub fn insertLines(self: *StreamHandler, count: usize) !void {
|
|
try self.terminal.insertLines(count);
|
|
}
|
|
|
|
pub fn insertBlanks(self: *StreamHandler, count: usize) !void {
|
|
self.terminal.insertBlanks(count);
|
|
}
|
|
|
|
pub fn deleteLines(self: *StreamHandler, count: usize) !void {
|
|
try self.terminal.deleteLines(count);
|
|
}
|
|
|
|
pub fn reverseIndex(self: *StreamHandler) !void {
|
|
try self.terminal.reverseIndex();
|
|
}
|
|
|
|
pub fn index(self: *StreamHandler) !void {
|
|
try self.terminal.index();
|
|
}
|
|
|
|
pub fn nextLine(self: *StreamHandler) !void {
|
|
self.terminal.carriageReturn();
|
|
try self.terminal.index();
|
|
}
|
|
|
|
pub fn setTopAndBottomMargin(self: *StreamHandler, top: u16, bot: u16) !void {
|
|
self.terminal.setScrollingRegion(top, bot);
|
|
}
|
|
|
|
pub fn setMode(self: *StreamHandler, mode: terminal.Mode, enabled: bool) !void {
|
|
switch (mode) {
|
|
.cursor_keys => {
|
|
self.terminal.modes.cursor_keys = enabled;
|
|
},
|
|
|
|
.reverse_colors => {
|
|
self.terminal.modes.reverse_colors = enabled;
|
|
|
|
// Schedule a render since we changed colors
|
|
try self.queueRender();
|
|
},
|
|
|
|
.origin => {
|
|
self.terminal.modes.origin = enabled;
|
|
self.terminal.setCursorPos(1, 1);
|
|
},
|
|
|
|
.autowrap => {
|
|
self.terminal.modes.autowrap = enabled;
|
|
},
|
|
|
|
.cursor_visible => {
|
|
self.ev.renderer_state.cursor.visible = enabled;
|
|
},
|
|
|
|
.alt_screen_save_cursor_clear_enter => {
|
|
const opts: terminal.Terminal.AlternateScreenOptions = .{
|
|
.cursor_save = true,
|
|
.clear_on_enter = true,
|
|
};
|
|
|
|
if (enabled)
|
|
self.terminal.alternateScreen(opts)
|
|
else
|
|
self.terminal.primaryScreen(opts);
|
|
|
|
// Schedule a render since we changed screens
|
|
try self.queueRender();
|
|
},
|
|
|
|
.bracketed_paste => self.terminal.modes.bracketed_paste = enabled,
|
|
|
|
.enable_mode_3 => {
|
|
// Disable deccolm
|
|
self.terminal.setDeccolmSupported(enabled);
|
|
|
|
// Force resize back to the window size
|
|
self.terminal.resize(self.alloc, self.grid_size.columns, self.grid_size.rows) catch |err|
|
|
log.err("error updating terminal size: {}", .{err});
|
|
},
|
|
|
|
.@"132_column" => try self.terminal.deccolm(
|
|
self.alloc,
|
|
if (enabled) .@"132_cols" else .@"80_cols",
|
|
),
|
|
|
|
.mouse_event_x10 => self.terminal.modes.mouse_event = if (enabled) .x10 else .none,
|
|
.mouse_event_normal => self.terminal.modes.mouse_event = if (enabled) .normal else .none,
|
|
.mouse_event_button => self.terminal.modes.mouse_event = if (enabled) .button else .none,
|
|
.mouse_event_any => self.terminal.modes.mouse_event = if (enabled) .any else .none,
|
|
|
|
.mouse_format_utf8 => self.terminal.modes.mouse_format = if (enabled) .utf8 else .x10,
|
|
.mouse_format_sgr => self.terminal.modes.mouse_format = if (enabled) .sgr else .x10,
|
|
.mouse_format_urxvt => self.terminal.modes.mouse_format = if (enabled) .urxvt else .x10,
|
|
.mouse_format_sgr_pixels => self.terminal.modes.mouse_format = if (enabled) .sgr_pixels else .x10,
|
|
|
|
else => if (enabled) log.warn("unimplemented mode: {}", .{mode}),
|
|
}
|
|
}
|
|
|
|
pub fn setAttribute(self: *StreamHandler, attr: terminal.Attribute) !void {
|
|
switch (attr) {
|
|
.unknown => |unk| log.warn("unimplemented or unknown attribute: {any}", .{unk}),
|
|
|
|
else => self.terminal.setAttribute(attr) catch |err|
|
|
log.warn("error setting attribute {}: {}", .{ attr, err }),
|
|
}
|
|
}
|
|
|
|
pub fn deviceAttributes(
|
|
self: *StreamHandler,
|
|
req: terminal.DeviceAttributeReq,
|
|
params: []const u16,
|
|
) !void {
|
|
_ = params;
|
|
|
|
switch (req) {
|
|
// VT220
|
|
.primary => self.messageWriter(.{ .write_stable = "\x1B[?62;c" }),
|
|
else => log.warn("unimplemented device attributes req: {}", .{req}),
|
|
}
|
|
}
|
|
|
|
pub fn deviceStatusReport(
|
|
self: *StreamHandler,
|
|
req: terminal.DeviceStatusReq,
|
|
) !void {
|
|
switch (req) {
|
|
.operating_status => self.messageWriter(.{ .write_stable = "\x1B[0n" }),
|
|
|
|
.cursor_position => {
|
|
const pos: struct {
|
|
x: usize,
|
|
y: usize,
|
|
} = if (self.terminal.modes.origin) .{
|
|
// TODO: what do we do if cursor is outside scrolling region?
|
|
.x = self.terminal.screen.cursor.x,
|
|
.y = self.terminal.screen.cursor.y -| self.terminal.scrolling_region.top,
|
|
} else .{
|
|
.x = self.terminal.screen.cursor.x,
|
|
.y = self.terminal.screen.cursor.y,
|
|
};
|
|
|
|
// Response always is at least 4 chars, so this leaves the
|
|
// remainder for the row/column as base-10 numbers. This
|
|
// will support a very large terminal.
|
|
var msg: termio.Message = .{ .write_small = .{} };
|
|
const resp = try std.fmt.bufPrint(&msg.write_small.data, "\x1B[{};{}R", .{
|
|
pos.y + 1,
|
|
pos.x + 1,
|
|
});
|
|
msg.write_small.len = @intCast(u8, resp.len);
|
|
|
|
self.messageWriter(msg);
|
|
},
|
|
|
|
else => log.warn("unimplemented device status req: {}", .{req}),
|
|
}
|
|
}
|
|
|
|
pub fn setCursorStyle(
|
|
self: *StreamHandler,
|
|
style: terminal.CursorStyle,
|
|
) !void {
|
|
self.ev.renderer_state.cursor.style = style;
|
|
}
|
|
|
|
pub fn decaln(self: *StreamHandler) !void {
|
|
try self.terminal.decaln();
|
|
}
|
|
|
|
pub fn tabClear(self: *StreamHandler, cmd: terminal.TabClear) !void {
|
|
self.terminal.tabClear(cmd);
|
|
}
|
|
|
|
pub fn tabSet(self: *StreamHandler) !void {
|
|
self.terminal.tabSet();
|
|
}
|
|
|
|
pub fn saveCursor(self: *StreamHandler) !void {
|
|
self.terminal.saveCursor();
|
|
}
|
|
|
|
pub fn restoreCursor(self: *StreamHandler) !void {
|
|
self.terminal.restoreCursor();
|
|
}
|
|
|
|
pub fn enquiry(self: *StreamHandler) !void {
|
|
self.messageWriter(.{ .write_stable = "" });
|
|
}
|
|
|
|
pub fn scrollDown(self: *StreamHandler, count: usize) !void {
|
|
try self.terminal.scrollDown(count);
|
|
}
|
|
|
|
pub fn scrollUp(self: *StreamHandler, count: usize) !void {
|
|
try self.terminal.scrollUp(count);
|
|
}
|
|
|
|
pub fn setActiveStatusDisplay(
|
|
self: *StreamHandler,
|
|
req: terminal.StatusDisplay,
|
|
) !void {
|
|
self.terminal.status_display = req;
|
|
}
|
|
|
|
pub fn configureCharset(
|
|
self: *StreamHandler,
|
|
slot: terminal.CharsetSlot,
|
|
set: terminal.Charset,
|
|
) !void {
|
|
self.terminal.configureCharset(slot, set);
|
|
}
|
|
|
|
pub fn invokeCharset(
|
|
self: *StreamHandler,
|
|
active: terminal.CharsetActiveSlot,
|
|
slot: terminal.CharsetSlot,
|
|
single: bool,
|
|
) !void {
|
|
self.terminal.invokeCharset(active, slot, single);
|
|
}
|
|
|
|
pub fn fullReset(
|
|
self: *StreamHandler,
|
|
) !void {
|
|
self.terminal.fullReset();
|
|
}
|
|
|
|
//-------------------------------------------------------------------------
|
|
// OSC
|
|
|
|
pub fn changeWindowTitle(self: *StreamHandler, title: []const u8) !void {
|
|
var buf: [256]u8 = undefined;
|
|
if (title.len >= buf.len) {
|
|
log.warn("change title requested larger than our buffer size, ignoring", .{});
|
|
return;
|
|
}
|
|
|
|
std.mem.copy(u8, &buf, title);
|
|
buf[title.len] = 0;
|
|
|
|
_ = self.window_mailbox.push(.{
|
|
.set_title = buf,
|
|
}, .{ .forever = {} });
|
|
}
|
|
|
|
pub fn clipboardContents(self: *StreamHandler, kind: u8, data: []const u8) !void {
|
|
// Note: we ignore the "kind" field and always use the primary clipboard.
|
|
// iTerm also appears to do this but other terminals seem to only allow
|
|
// certain. Let's investigate more.
|
|
|
|
// Get clipboard contents
|
|
if (data.len == 1 and data[0] == '?') {
|
|
_ = self.window_mailbox.push(.{
|
|
.clipboard_read = kind,
|
|
}, .{ .forever = {} });
|
|
return;
|
|
}
|
|
|
|
// Write clipboard contents
|
|
_ = self.window_mailbox.push(.{
|
|
.clipboard_write = try Window.Message.WriteReq.init(
|
|
self.alloc,
|
|
data,
|
|
),
|
|
}, .{ .forever = {} });
|
|
}
|
|
};
|