Files
ghostty/src/termio/stream_handler.zig
Jeffrey C. Ollie a2ef0ca751 Address review comments.
- Cap the total number of requests at twice the maximum number of
  keys (currently 263, so 526 requests). Basically you can set and then
  query every key in one message. This is an absurdly high number
  but should prevent serious DOS attacks.
- Clarify meaning of new hex color codes.
- Better handle sending messages to the renderer in a way that should
  prevent deadlocks.
- Handle 0-255 palette color requests by creatively using non-exhautive
  enums.
- Fix an error in the query reply.
2024-08-19 00:22:09 -05:00

1429 lines
54 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const xev = @import("xev");
const apprt = @import("../apprt.zig");
const build_config = @import("../build_config.zig");
const configpkg = @import("../config.zig");
const renderer = @import("../renderer.zig");
const termio = @import("../termio.zig");
const terminal = @import("../terminal/main.zig");
const terminfo = @import("../terminfo/main.zig");
const posix = std.posix;
const log = std.log.scoped(.io_handler);
/// True if we should disable the kitty keyboard protocol. We have to
/// disable this on GLFW because GLFW input events don't support the
/// correct granularity of events.
const disable_kitty_keyboard_protocol = apprt.runtime == apprt.glfw;
/// 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.
pub const StreamHandler = struct {
alloc: Allocator,
grid_size: *renderer.GridSize,
terminal: *terminal.Terminal,
/// Mailbox for data to the termio thread.
termio_mailbox: *termio.Mailbox,
/// Mailbox for the surface.
surface_mailbox: apprt.surface.Mailbox,
/// The shared render state
renderer_state: *renderer.State,
/// The mailbox for notifying the renderer of things.
renderer_mailbox: *renderer.Thread.Mailbox,
/// A handle to wake up the renderer. This hints to the renderer that that
/// a repaint should happen.
renderer_wakeup: xev.Async,
/// The default cursor state. This is used with CSI q. This is
/// set to true when we're currently in the default cursor state.
default_cursor: bool = true,
default_cursor_style: terminal.CursorStyle,
default_cursor_blink: ?bool,
default_cursor_color: ?terminal.color.RGB,
/// Actual cursor color. This can be changed with OSC 12.
cursor_color: ?terminal.color.RGB,
/// The default foreground and background color are those set by the user's
/// config file. These can be overridden by terminal applications using OSC
/// 10 and OSC 11, respectively.
default_foreground_color: terminal.color.RGB,
default_background_color: terminal.color.RGB,
/// The actual foreground and background color. Normally this will be the
/// same as the default foreground and background color, unless changed by a
/// terminal application.
foreground_color: terminal.color.RGB,
background_color: terminal.color.RGB,
/// The response to use for ENQ requests. The memory is owned by
/// whoever owns StreamHandler.
enquiry_response: []const u8,
/// The color reporting format for OSC requests.
osc_color_report_format: configpkg.Config.OSCColorReportFormat,
//---------------------------------------------------------------
// Internal state
/// The APC command handler maintains the APC state. APC is like
/// CSI or OSC, but it is a private escape sequence that is used
/// to send commands to the terminal emulator. This is used by
/// the kitty graphics protocol.
apc: terminal.apc.Handler = .{},
/// The DCS handler maintains DCS state. DCS is like CSI or OSC,
/// but requires more stateful parsing. This is used by functionality
/// such as XTGETTCAP.
dcs: terminal.dcs.Handler = .{},
/// This is set to true when a message was written to the termio
/// mailbox. This can be used by callers to determine if they need
/// to wake up the termio thread.
termio_messaged: bool = false,
/// This is set to true when we've seen a title escape sequence. We use
/// this to determine if we need to default the window title.
seen_title: bool = false,
pub fn deinit(self: *StreamHandler) void {
self.apc.deinit();
self.dcs.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.
pub inline fn queueRender(self: *StreamHandler) !void {
try self.renderer_wakeup.notify();
}
/// Change the configuration for this handler.
pub fn changeConfig(self: *StreamHandler, config: *termio.DerivedConfig) void {
self.osc_color_report_format = config.osc_color_report_format;
self.enquiry_response = config.enquiry_response;
self.default_foreground_color = config.foreground.toTerminalRGB();
self.default_background_color = config.background.toTerminalRGB();
self.default_cursor_style = config.cursor_style;
self.default_cursor_blink = config.cursor_blink;
self.default_cursor_color = if (!config.cursor_invert and config.cursor_color != null)
config.cursor_color.?.toTerminalRGB()
else
null;
// If our cursor is the default, then we update it immediately.
if (self.default_cursor) self.setCursorStyle(.default) catch |err| {
log.warn("failed to set default cursor style: {}", .{err});
};
}
inline fn surfaceMessageWriter(
self: *StreamHandler,
msg: apprt.surface.Message,
) void {
// See messageWriter which has similar logic and explains why
// we may have to do this.
if (self.surface_mailbox.push(msg, .{ .instant = {} }) == 0) {
self.renderer_state.mutex.unlock();
defer self.renderer_state.mutex.lock();
_ = self.surface_mailbox.push(msg, .{ .forever = {} });
}
}
inline fn messageWriter(self: *StreamHandler, msg: termio.Message) void {
self.termio_mailbox.send(msg, self.renderer_state.mutex);
self.termio_messaged = true;
}
pub fn dcsHook(self: *StreamHandler, dcs: terminal.DCS) !void {
var cmd = self.dcs.hook(self.alloc, dcs) orelse return;
defer cmd.deinit();
try self.dcsCommand(&cmd);
}
pub fn dcsPut(self: *StreamHandler, byte: u8) !void {
var cmd = self.dcs.put(byte) orelse return;
defer cmd.deinit();
try self.dcsCommand(&cmd);
}
pub fn dcsUnhook(self: *StreamHandler) !void {
var cmd = self.dcs.unhook() orelse return;
defer cmd.deinit();
try self.dcsCommand(&cmd);
}
fn dcsCommand(self: *StreamHandler, cmd: *terminal.dcs.Command) !void {
// log.warn("DCS command: {}", .{cmd});
switch (cmd.*) {
.tmux => |tmux| {
// TODO: process it
log.warn("tmux control mode event unimplemented cmd={}", .{tmux});
},
.xtgettcap => |*gettcap| {
const map = comptime terminfo.ghostty.xtgettcapMap();
while (gettcap.next()) |key| {
const response = map.get(key) orelse continue;
self.messageWriter(.{ .write_stable = response });
}
},
.decrqss => |decrqss| {
var response: [128]u8 = undefined;
var stream = std.io.fixedBufferStream(&response);
const writer = stream.writer();
// Offset the stream position to just past the response prefix.
// We will write the "payload" (if any) below. If no payload is
// written then we send an invalid DECRPSS response.
const prefix_fmt = "\x1bP{d}$r";
const prefix_len = std.fmt.comptimePrint(prefix_fmt, .{0}).len;
stream.pos = prefix_len;
switch (decrqss) {
// Invalid or unhandled request
.none => {},
.sgr => {
const buf = try self.terminal.printAttributes(stream.buffer[stream.pos..]);
// printAttributes wrote into our buffer, so adjust the stream
// position
stream.pos += buf.len;
try writer.writeByte('m');
},
.decscusr => {
const blink = self.terminal.modes.get(.cursor_blinking);
const style: u8 = switch (self.terminal.screen.cursor.cursor_style) {
.block => if (blink) 1 else 2,
.underline => if (blink) 3 else 4,
.bar => if (blink) 5 else 6,
};
try writer.print("{d} q", .{style});
},
.decstbm => {
try writer.print("{d};{d}r", .{
self.terminal.scrolling_region.top + 1,
self.terminal.scrolling_region.bottom + 1,
});
},
.decslrm => {
// We only send a valid response when left and right
// margin mode (DECLRMM) is enabled.
if (self.terminal.modes.get(.enable_left_and_right_margin)) {
try writer.print("{d};{d}s", .{
self.terminal.scrolling_region.left + 1,
self.terminal.scrolling_region.right + 1,
});
}
},
}
// Our response is valid if we have a response payload
const valid = stream.pos > prefix_len;
// Write the terminator
try writer.writeAll("\x1b\\");
// Write the response prefix into the buffer
_ = try std.fmt.bufPrint(response[0..prefix_len], prefix_fmt, .{@intFromBool(valid)});
const msg = try termio.Message.writeReq(self.alloc, response[0..stream.pos]);
self.messageWriter(msg);
},
}
}
pub fn apcStart(self: *StreamHandler) !void {
self.apc.start();
}
pub fn apcPut(self: *StreamHandler, byte: u8) !void {
self.apc.feed(self.alloc, byte);
}
pub fn apcEnd(self: *StreamHandler) !void {
var cmd = self.apc.end() orelse return;
defer cmd.deinit(self.alloc);
// log.warn("APC command: {}", .{cmd});
switch (cmd) {
.kitty => |*kitty_cmd| {
if (self.terminal.kittyGraphics(self.alloc, kitty_cmd)) |resp| {
var buf: [1024]u8 = undefined;
var buf_stream = std.io.fixedBufferStream(&buf);
try resp.encode(buf_stream.writer());
const final = buf_stream.getWritten();
if (final.len > 2) {
// log.warn("kitty graphics response: {s}", .{std.fmt.fmtSliceHexLower(final)});
self.messageWriter(try termio.Message.writeReq(self.alloc, final));
}
}
},
}
}
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, count: u16) !void {
for (0..count) |_| {
const x = self.terminal.screen.cursor.x;
try self.terminal.horizontalTab();
if (x == self.terminal.screen.cursor.x) break;
}
}
pub fn horizontalTabBack(self: *StreamHandler, count: u16) !void {
for (0..count) |_| {
const x = self.terminal.screen.cursor.x;
try self.terminal.horizontalTabBack();
if (x == self.terminal.screen.cursor.x) break;
}
}
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, carriage: bool) !void {
self.terminal.cursorDown(amount);
if (carriage) self.terminal.carriageReturn();
}
pub fn setCursorUp(self: *StreamHandler, amount: u16, carriage: bool) !void {
self.terminal.cursorUp(amount);
if (carriage) self.terminal.carriageReturn();
}
pub fn setCursorCol(self: *StreamHandler, col: u16) !void {
self.terminal.setCursorPos(self.terminal.screen.cursor.y + 1, col);
}
pub fn setCursorColRelative(self: *StreamHandler, offset: u16) !void {
self.terminal.setCursorPos(
self.terminal.screen.cursor.y + 1,
self.terminal.screen.cursor.x + 1 +| offset,
);
}
pub fn setCursorRow(self: *StreamHandler, row: u16) !void {
self.terminal.setCursorPos(row, self.terminal.screen.cursor.x + 1);
}
pub fn setCursorRowRelative(self: *StreamHandler, offset: u16) !void {
self.terminal.setCursorPos(
self.terminal.screen.cursor.y + 1 +| offset,
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, protected: bool) !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, protected);
}
pub fn eraseLine(self: *StreamHandler, mode: terminal.EraseLine, protected: bool) !void {
self.terminal.eraseLine(mode, protected);
}
pub fn deleteChars(self: *StreamHandler, count: usize) !void {
self.terminal.deleteChars(count);
}
pub fn eraseChars(self: *StreamHandler, count: usize) !void {
self.terminal.eraseChars(count);
}
pub fn insertLines(self: *StreamHandler, count: usize) !void {
self.terminal.insertLines(count);
}
pub fn insertBlanks(self: *StreamHandler, count: usize) !void {
self.terminal.insertBlanks(count);
}
pub fn deleteLines(self: *StreamHandler, count: usize) !void {
self.terminal.deleteLines(count);
}
pub fn reverseIndex(self: *StreamHandler) !void {
self.terminal.reverseIndex();
}
pub fn index(self: *StreamHandler) !void {
try self.terminal.index();
}
pub fn nextLine(self: *StreamHandler) !void {
try self.terminal.index();
self.terminal.carriageReturn();
}
pub fn setTopAndBottomMargin(self: *StreamHandler, top: u16, bot: u16) !void {
self.terminal.setTopAndBottomMargin(top, bot);
}
pub fn setLeftAndRightMarginAmbiguous(self: *StreamHandler) !void {
if (self.terminal.modes.get(.enable_left_and_right_margin)) {
try self.setLeftAndRightMargin(0, 0);
} else {
try self.saveCursor();
}
}
pub fn setLeftAndRightMargin(self: *StreamHandler, left: u16, right: u16) !void {
self.terminal.setLeftAndRightMargin(left, right);
}
pub fn setModifyKeyFormat(self: *StreamHandler, format: terminal.ModifyKeyFormat) !void {
self.terminal.flags.modify_other_keys_2 = false;
switch (format) {
.other_keys => |v| switch (v) {
.numeric => self.terminal.flags.modify_other_keys_2 = true,
else => {},
},
else => {},
}
}
pub fn requestMode(self: *StreamHandler, mode_raw: u16, ansi: bool) !void {
// Get the mode value and respond.
const code: u8 = code: {
const mode = terminal.modes.modeFromInt(mode_raw, ansi) orelse break :code 0;
if (self.terminal.modes.get(mode)) break :code 1;
break :code 2;
};
var msg: termio.Message = .{ .write_small = .{} };
const resp = try std.fmt.bufPrint(
&msg.write_small.data,
"\x1B[{s}{};{}$y",
.{
if (ansi) "" else "?",
mode_raw,
code,
},
);
msg.write_small.len = @intCast(resp.len);
self.messageWriter(msg);
}
pub fn saveMode(self: *StreamHandler, mode: terminal.Mode) !void {
// log.debug("save mode={}", .{mode});
self.terminal.modes.save(mode);
}
pub fn restoreMode(self: *StreamHandler, mode: terminal.Mode) !void {
// For restore mode we have to restore but if we set it, we
// always have to call setMode because setting some modes have
// side effects and we want to make sure we process those.
const v = self.terminal.modes.restore(mode);
// log.debug("restore mode={} v={}", .{ mode, v });
try self.setMode(mode, v);
}
pub fn setMode(self: *StreamHandler, mode: terminal.Mode, enabled: bool) !void {
// Note: this function doesn't need to grab the render state or
// terminal locks because it is only called from process() which
// grabs the lock.
// If we are setting cursor blinking, we ignore it if we have
// a default cursor blink setting set. This is a really weird
// behavior so this comment will go deep into trying to explain it.
//
// There are two ways to set cursor blinks: DECSCUSR (CSI _ q)
// and DEC mode 12. DECSCUSR is the modern approach and has a
// way to revert to the "default" (as defined by the terminal)
// cursor style and blink by doing "CSI 0 q". DEC mode 12 controls
// blinking and is either on or off and has no way to set a
// default. DEC mode 12 is also the more antiquated approach.
//
// The problem is that if the user specifies a desired default
// cursor blink with `cursor-style-blink`, the moment a running
// program uses DEC mode 12, the cursor blink can never be reset
// to the default without an explicit DECSCUSR. But if a program
// is using mode 12, it is by definition not using DECSCUSR.
// This makes for somewhat annoying interactions where a poorly
// (or legacy) behaved program will stop blinking, and it simply
// never restarts.
//
// To get around this, we have a special case where if the user
// specifies some explicit default cursor blink desire, we ignore
// DEC mode 12. We allow DECSCUSR to still set the cursor blink
// because programs using DECSCUSR usually are well behaved and
// reset the cursor blink to the default when they exit.
//
// To be extra safe, users can also add a manual `CSI 0 q` to
// their shell config when they render prompts to ensure the
// cursor is exactly as they request.
if (mode == .cursor_blinking and
self.default_cursor_blink != null)
{
return;
}
// We first always set the raw mode on our mode state.
self.terminal.modes.set(mode, enabled);
// And then some modes require additional processing.
switch (mode) {
// Just noting here that autorepeat has no effect on
// the terminal. xterm ignores this mode and so do we.
// We know about just so that we don't log that it is
// an unknown mode.
.autorepeat => {},
// Schedule a render since we changed colors
.reverse_colors => {
self.terminal.flags.dirty.reverse_colors = true;
try self.queueRender();
},
// Origin resets cursor pos. This is called whether or not
// we're enabling or disabling origin mode and whether or
// not the value changed.
.origin => self.terminal.setCursorPos(1, 1),
.enable_left_and_right_margin => if (!enabled) {
// When we disable left/right margin mode we need to
// reset the left/right margins.
self.terminal.scrolling_region.left = 0;
self.terminal.scrolling_region.right = self.terminal.cols - 1;
},
.alt_screen => {
const opts: terminal.Terminal.AlternateScreenOptions = .{
.cursor_save = false,
.clear_on_enter = false,
};
if (enabled)
self.terminal.alternateScreen(opts)
else
self.terminal.primaryScreen(opts);
// Schedule a render since we changed screens
try self.queueRender();
},
.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();
},
// Force resize back to the window size
.enable_mode_3 => 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",
),
// We need to start a timer to prevent the emulator being hung
// forever.
.synchronized_output => {
if (enabled) self.messageWriter(.{ .start_synchronized_output = {} });
try self.queueRender();
},
.linefeed => {
self.messageWriter(.{ .linefeed_mode = enabled });
},
.in_band_size_reports => if (enabled) self.messageWriter(.{
.size_report = .mode_2048,
}),
.mouse_event_x10 => {
if (enabled) {
self.terminal.flags.mouse_event = .x10;
try self.setMouseShape(.default);
} else {
self.terminal.flags.mouse_event = .none;
try self.setMouseShape(.text);
}
},
.mouse_event_normal => {
if (enabled) {
self.terminal.flags.mouse_event = .normal;
try self.setMouseShape(.default);
} else {
self.terminal.flags.mouse_event = .none;
try self.setMouseShape(.text);
}
},
.mouse_event_button => {
if (enabled) {
self.terminal.flags.mouse_event = .button;
try self.setMouseShape(.default);
} else {
self.terminal.flags.mouse_event = .none;
try self.setMouseShape(.text);
}
},
.mouse_event_any => {
if (enabled) {
self.terminal.flags.mouse_event = .any;
try self.setMouseShape(.default);
} else {
self.terminal.flags.mouse_event = .none;
try self.setMouseShape(.text);
}
},
.mouse_format_utf8 => self.terminal.flags.mouse_format = if (enabled) .utf8 else .x10,
.mouse_format_sgr => self.terminal.flags.mouse_format = if (enabled) .sgr else .x10,
.mouse_format_urxvt => self.terminal.flags.mouse_format = if (enabled) .urxvt else .x10,
.mouse_format_sgr_pixels => self.terminal.flags.mouse_format = if (enabled) .sgr_pixels else .x10,
else => {},
}
}
pub fn setMouseShiftCapture(self: *StreamHandler, v: bool) !void {
self.terminal.flags.mouse_shift_capture = if (v) .true else .false;
}
pub fn setAttribute(self: *StreamHandler, attr: terminal.Attribute) !void {
switch (attr) {
.unknown => |unk| log.warn("unimplemented or unknown SGR attribute: {any}", .{unk}),
else => self.terminal.setAttribute(attr) catch |err|
log.warn("error setting attribute {}: {}", .{ attr, err }),
}
}
pub fn startHyperlink(self: *StreamHandler, uri: []const u8, id: ?[]const u8) !void {
try self.terminal.screen.startHyperlink(uri, id);
}
pub fn endHyperlink(self: *StreamHandler) !void {
self.terminal.screen.endHyperlink();
}
pub fn deviceAttributes(
self: *StreamHandler,
req: terminal.DeviceAttributeReq,
params: []const u16,
) !void {
_ = params;
// For the below, we quack as a VT220. We don't quack as
// a 420 because we don't support DCS sequences.
switch (req) {
.primary => self.messageWriter(.{
.write_stable = "\x1B[?62;22c",
}),
.secondary => self.messageWriter(.{
.write_stable = "\x1B[>1;10;0c",
}),
else => log.warn("unimplemented device attributes req: {}", .{req}),
}
}
pub fn deviceStatusReport(
self: *StreamHandler,
req: terminal.device_status.Request,
) !void {
switch (req) {
.operating_status => self.messageWriter(.{ .write_stable = "\x1B[0n" }),
.cursor_position => {
const pos: struct {
x: usize,
y: usize,
} = if (self.terminal.modes.get(.origin)) .{
.x = self.terminal.screen.cursor.x -| self.terminal.scrolling_region.left,
.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(resp.len);
self.messageWriter(msg);
},
.color_scheme => self.surfaceMessageWriter(.{ .report_color_scheme = {} }),
}
}
pub fn setCursorStyle(
self: *StreamHandler,
style: terminal.CursorStyleReq,
) !void {
// Assume we're setting to a non-default.
self.default_cursor = false;
switch (style) {
.default => {
self.default_cursor = true;
self.terminal.screen.cursor.cursor_style = self.default_cursor_style;
self.terminal.modes.set(
.cursor_blinking,
self.default_cursor_blink orelse true,
);
},
.blinking_block => {
self.terminal.screen.cursor.cursor_style = .block;
self.terminal.modes.set(.cursor_blinking, true);
},
.steady_block => {
self.terminal.screen.cursor.cursor_style = .block;
self.terminal.modes.set(.cursor_blinking, false);
},
.blinking_underline => {
self.terminal.screen.cursor.cursor_style = .underline;
self.terminal.modes.set(.cursor_blinking, true);
},
.steady_underline => {
self.terminal.screen.cursor.cursor_style = .underline;
self.terminal.modes.set(.cursor_blinking, false);
},
.blinking_bar => {
self.terminal.screen.cursor.cursor_style = .bar;
self.terminal.modes.set(.cursor_blinking, true);
},
.steady_bar => {
self.terminal.screen.cursor.cursor_style = .bar;
self.terminal.modes.set(.cursor_blinking, false);
},
else => log.warn("unimplemented cursor style: {}", .{style}),
}
}
pub fn setProtectedMode(self: *StreamHandler, mode: terminal.ProtectedMode) !void {
self.terminal.setProtectedMode(mode);
}
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 tabReset(self: *StreamHandler) !void {
self.terminal.tabReset();
}
pub fn saveCursor(self: *StreamHandler) !void {
self.terminal.saveCursor();
}
pub fn restoreCursor(self: *StreamHandler) !void {
try self.terminal.restoreCursor();
}
pub fn enquiry(self: *StreamHandler) !void {
log.debug("sending enquiry response={s}", .{self.enquiry_response});
self.messageWriter(try termio.Message.writeReq(self.alloc, self.enquiry_response));
}
pub fn scrollDown(self: *StreamHandler, count: usize) !void {
self.terminal.scrollDown(count);
}
pub fn scrollUp(self: *StreamHandler, count: usize) !void {
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();
try self.setMouseShape(.text);
}
pub fn queryKittyKeyboard(self: *StreamHandler) !void {
if (comptime disable_kitty_keyboard_protocol) return;
log.debug("querying kitty keyboard mode", .{});
var data: termio.Message.WriteReq.Small.Array = undefined;
const resp = try std.fmt.bufPrint(&data, "\x1b[?{}u", .{
self.terminal.screen.kitty_keyboard.current().int(),
});
self.messageWriter(.{
.write_small = .{
.data = data,
.len = @intCast(resp.len),
},
});
}
pub fn pushKittyKeyboard(
self: *StreamHandler,
flags: terminal.kitty.KeyFlags,
) !void {
if (comptime disable_kitty_keyboard_protocol) return;
log.debug("pushing kitty keyboard mode: {}", .{flags});
self.terminal.screen.kitty_keyboard.push(flags);
}
pub fn popKittyKeyboard(self: *StreamHandler, n: u16) !void {
if (comptime disable_kitty_keyboard_protocol) return;
log.debug("popping kitty keyboard mode n={}", .{n});
self.terminal.screen.kitty_keyboard.pop(@intCast(n));
}
pub fn setKittyKeyboard(
self: *StreamHandler,
mode: terminal.kitty.KeySetMode,
flags: terminal.kitty.KeyFlags,
) !void {
if (comptime disable_kitty_keyboard_protocol) return;
log.debug("setting kitty keyboard mode: {} {}", .{ mode, flags });
self.terminal.screen.kitty_keyboard.set(mode, flags);
}
pub fn reportXtversion(
self: *StreamHandler,
) !void {
log.debug("reporting XTVERSION: ghostty {s}", .{build_config.version_string});
var buf: [288]u8 = undefined;
const resp = try std.fmt.bufPrint(
&buf,
"\x1BP>|{s} {s}\x1B\\",
.{
"ghostty",
build_config.version_string,
},
);
const msg = try termio.Message.writeReq(self.alloc, resp);
self.messageWriter(msg);
}
//-------------------------------------------------------------------------
// 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;
}
@memcpy(buf[0..title.len], title);
buf[title.len] = 0;
// Mark that we've seen a title
self.seen_title = true;
self.surfaceMessageWriter(.{ .set_title = buf });
}
pub fn setMouseShape(
self: *StreamHandler,
shape: terminal.MouseShape,
) !void {
// Avoid changing the shape it it is already set to avoid excess
// cross-thread messaging.
if (self.terminal.mouse_shape == shape) return;
self.terminal.mouse_shape = shape;
self.surfaceMessageWriter(.{ .set_mouse_shape = shape });
}
pub fn clipboardContents(self: *StreamHandler, kind: u8, data: []const u8) !void {
// Note: we ignore the "kind" field and always use the standard clipboard.
// iTerm also appears to do this but other terminals seem to only allow
// certain. Let's investigate more.
const clipboard_type: apprt.Clipboard = switch (kind) {
'c' => .standard,
's' => .selection,
'p' => .primary,
else => .standard,
};
// Get clipboard contents
if (data.len == 1 and data[0] == '?') {
self.surfaceMessageWriter(.{ .clipboard_read = clipboard_type });
return;
}
// Write clipboard contents
self.surfaceMessageWriter(.{
.clipboard_write = .{
.req = try apprt.surface.Message.WriteReq.init(
self.alloc,
data,
),
.clipboard_type = clipboard_type,
},
});
}
pub fn promptStart(self: *StreamHandler, aid: ?[]const u8, redraw: bool) !void {
_ = aid;
self.terminal.markSemanticPrompt(.prompt);
self.terminal.flags.shell_redraws_prompt = redraw;
}
pub fn promptContinuation(self: *StreamHandler, aid: ?[]const u8) !void {
_ = aid;
self.terminal.markSemanticPrompt(.prompt_continuation);
}
pub fn promptEnd(self: *StreamHandler) !void {
self.terminal.markSemanticPrompt(.input);
}
pub fn endOfInput(self: *StreamHandler) !void {
self.terminal.markSemanticPrompt(.command);
}
pub fn reportPwd(self: *StreamHandler, url: []const u8) !void {
if (builtin.os.tag == .windows) {
log.warn("reportPwd unimplemented on windows", .{});
return;
}
const uri = std.Uri.parse(url) catch |e| {
log.warn("invalid url in OSC 7: {}", .{e});
return;
};
if (!std.mem.eql(u8, "file", uri.scheme) and
!std.mem.eql(u8, "kitty-shell-cwd", uri.scheme))
{
log.warn("OSC 7 scheme must be file, got: {s}", .{uri.scheme});
return;
}
// OSC 7 is a little sketchy because anyone can send any value from
// any host (such an SSH session). The best practice terminals follow
// is to valid the hostname to be local.
const host_valid = host_valid: {
const host_component = uri.host orelse break :host_valid false;
// Get the raw string of the URI. Its unclear to me if the various
// tags of this enum guarantee no percent-encoding so we just
// check all of it. This isn't a performance critical path.
const host = switch (host_component) {
.raw => |v| v,
.percent_encoded => |v| v,
};
if (host.len == 0 or std.mem.eql(u8, "localhost", host)) {
break :host_valid true;
}
// Otherwise, it must match our hostname.
var buf: [posix.HOST_NAME_MAX]u8 = undefined;
const hostname = posix.gethostname(&buf) catch |err| {
log.warn("failed to get hostname for OSC 7 validation: {}", .{err});
break :host_valid false;
};
break :host_valid std.mem.eql(u8, host, hostname);
};
if (!host_valid) {
log.warn("OSC 7 host must be local", .{});
return;
}
// We need to unescape the path. We first try to unescape onto
// the stack and fall back to heap allocation if we have to.
var pathBuf: [1024]u8 = undefined;
const path, const heap = path: {
// Get the raw string of the URI. Its unclear to me if the various
// tags of this enum guarantee no percent-encoding so we just
// check all of it. This isn't a performance critical path.
const path = switch (uri.path) {
.raw => |v| v,
.percent_encoded => |v| v,
};
// If the path doesn't have any escapes, we can use it directly.
if (std.mem.indexOfScalar(u8, path, '%') == null)
break :path .{ path, false };
// First try to stack-allocate
var fba = std.heap.FixedBufferAllocator.init(&pathBuf);
if (std.fmt.allocPrint(fba.allocator(), "{raw}", .{uri.path})) |v|
break :path .{ v, false }
else |_| {}
// Fall back to heap
if (std.fmt.allocPrint(self.alloc, "{raw}", .{uri.path})) |v|
break :path .{ v, true }
else |_| {}
// Fall back to using it directly...
log.warn("failed to unescape OSC 7 path, using it directly path={s}", .{path});
break :path .{ path, false };
};
defer if (heap) self.alloc.free(path);
log.debug("terminal pwd: {s}", .{path});
try self.terminal.setPwd(path);
// If we haven't seen a title, use our pwd as the title.
if (!self.seen_title) {
try self.changeWindowTitle(path);
self.seen_title = false;
}
}
/// Implements OSC 4, OSC 10, and OSC 11, which reports palette color,
/// default foreground color, and background color respectively.
pub fn reportColor(
self: *StreamHandler,
kind: terminal.osc.Command.ColorKind,
terminator: terminal.osc.Terminator,
) !void {
if (self.osc_color_report_format == .none) return;
const color = switch (kind) {
.palette => |i| self.terminal.color_palette.colors[i],
.foreground => self.foreground_color,
.background => self.background_color,
.cursor => self.cursor_color orelse self.foreground_color,
};
var msg: termio.Message = .{ .write_small = .{} };
const resp = switch (self.osc_color_report_format) {
.@"16-bit" => switch (kind) {
.palette => |i| try std.fmt.bufPrint(
&msg.write_small.data,
"\x1B]{s};{d};rgb:{x:0>4}/{x:0>4}/{x:0>4}{s}",
.{
kind.code(),
i,
@as(u16, color.r) * 257,
@as(u16, color.g) * 257,
@as(u16, color.b) * 257,
terminator.string(),
},
),
else => try std.fmt.bufPrint(
&msg.write_small.data,
"\x1B]{s};rgb:{x:0>4}/{x:0>4}/{x:0>4}{s}",
.{
kind.code(),
@as(u16, color.r) * 257,
@as(u16, color.g) * 257,
@as(u16, color.b) * 257,
terminator.string(),
},
),
},
.@"8-bit" => switch (kind) {
.palette => |i| try std.fmt.bufPrint(
&msg.write_small.data,
"\x1B]{s};{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}{s}",
.{
kind.code(),
i,
@as(u16, color.r),
@as(u16, color.g),
@as(u16, color.b),
terminator.string(),
},
),
else => try std.fmt.bufPrint(
&msg.write_small.data,
"\x1B]{s};rgb:{x:0>2}/{x:0>2}/{x:0>2}{s}",
.{
kind.code(),
@as(u16, color.r),
@as(u16, color.g),
@as(u16, color.b),
terminator.string(),
},
),
},
.none => unreachable, // early return above
};
msg.write_small.len = @intCast(resp.len);
self.messageWriter(msg);
}
pub fn setColor(
self: *StreamHandler,
kind: terminal.osc.Command.ColorKind,
value: []const u8,
) !void {
const color = try terminal.color.RGB.parse(value);
switch (kind) {
.palette => |i| {
self.terminal.flags.dirty.palette = true;
self.terminal.color_palette.colors[i] = color;
self.terminal.color_palette.mask.set(i);
},
.foreground => {
self.foreground_color = color;
_ = self.renderer_mailbox.push(.{
.foreground_color = color,
}, .{ .forever = {} });
},
.background => {
self.background_color = color;
_ = self.renderer_mailbox.push(.{
.background_color = color,
}, .{ .forever = {} });
},
.cursor => {
self.cursor_color = color;
_ = self.renderer_mailbox.push(.{
.cursor_color = color,
}, .{ .forever = {} });
},
}
}
pub fn resetColor(
self: *StreamHandler,
kind: terminal.osc.Command.ColorKind,
value: []const u8,
) !void {
switch (kind) {
.palette => {
const mask = &self.terminal.color_palette.mask;
if (value.len == 0) {
// Find all bit positions in the mask which are set and
// reset those indices to the default palette
var it = mask.iterator(.{});
while (it.next()) |i| {
self.terminal.flags.dirty.palette = true;
self.terminal.color_palette.colors[i] = self.terminal.default_palette[i];
mask.unset(i);
}
} else {
var it = std.mem.tokenizeScalar(u8, value, ';');
while (it.next()) |param| {
// Skip invalid parameters
const i = std.fmt.parseUnsigned(u8, param, 10) catch continue;
if (mask.isSet(i)) {
self.terminal.flags.dirty.palette = true;
self.terminal.color_palette.colors[i] = self.terminal.default_palette[i];
mask.unset(i);
}
}
}
},
.foreground => {
self.foreground_color = self.default_foreground_color;
_ = self.renderer_mailbox.push(.{
.foreground_color = self.foreground_color,
}, .{ .forever = {} });
},
.background => {
self.background_color = self.default_background_color;
_ = self.renderer_mailbox.push(.{
.background_color = self.background_color,
}, .{ .forever = {} });
},
.cursor => {
self.cursor_color = self.default_cursor_color;
_ = self.renderer_mailbox.push(.{
.cursor_color = self.cursor_color,
}, .{ .forever = {} });
},
}
}
pub fn showDesktopNotification(
self: *StreamHandler,
title: []const u8,
body: []const u8,
) !void {
var message = apprt.surface.Message{ .desktop_notification = undefined };
const title_len = @min(title.len, message.desktop_notification.title.len);
@memcpy(message.desktop_notification.title[0..title_len], title[0..title_len]);
message.desktop_notification.title[title_len] = 0;
const body_len = @min(body.len, message.desktop_notification.body.len);
@memcpy(message.desktop_notification.body[0..body_len], body[0..body_len]);
message.desktop_notification.body[body_len] = 0;
self.surfaceMessageWriter(message);
}
/// Send a report to the pty.
pub fn sendSizeReport(self: *StreamHandler, style: terminal.SizeReportStyle) void {
switch (style) {
.csi_14_t => self.messageWriter(.{ .size_report = .csi_14_t }),
.csi_16_t => self.messageWriter(.{ .size_report = .csi_16_t }),
.csi_18_t => self.messageWriter(.{ .size_report = .csi_18_t }),
.csi_21_t => self.surfaceMessageWriter(.{ .report_title = .csi_21_t }),
}
}
pub fn sendKittyColorReport(self: *StreamHandler, request: terminal.osc.Command.KittyColorProtocol) !void {
var buf = std.ArrayList(u8).init(self.alloc);
errdefer buf.deinit();
const writer = buf.writer();
try writer.writeAll("\x1b[21");
for (request.list.items) |item| {
switch (item) {
.query => |key| {
const i = @intFromEnum(key);
const color = switch (key) {
.foreground => self.foreground_color,
.background => self.background_color,
.cursor => self.cursor_color,
else => color: {
if (i > 255) {
log.warn("ignoring unsupported kitty color protocol key: {s}", .{@tagName(key)});
continue;
}
break :color self.terminal.color_palette.colors[i];
},
} orelse {
log.warn("no color configured for: {s}", .{@tagName(key)});
continue;
};
if (i <= 255)
try writer.print(
";{d}=rgb:{x:0>2}/{x:0>2}/{x:0>2}",
.{ i, color.r, color.g, color.b },
)
else
try writer.print(
";{s}=rgb:{x:0>2}/{x:0>2}/{x:0>2}",
.{ @tagName(key), color.r, color.g, color.b },
);
},
.set => |v| switch (v.key) {
.foreground => {
self.foreground_color = v.color;
// See messageWriter which has similar logic and
// explains why we may have to do this.
const msg: renderer.Message = .{
.foreground_color = v.color,
};
if (self.renderer_mailbox.push(msg, .{ .instant = {} }) == 0) {
self.renderer_state.mutex.unlock();
defer self.renderer_state.mutex.lock();
_ = self.renderer_mailbox.push(msg, .{ .forever = {} });
}
},
.background => {
self.background_color = v.color;
// See messageWriter which has similar logic and
// explains why we may have to do this.
const msg: renderer.Message = .{
.background_color = v.color,
};
if (self.renderer_mailbox.push(msg, .{ .instant = {} }) == 0) {
self.renderer_state.mutex.unlock();
defer self.renderer_state.mutex.lock();
_ = self.renderer_mailbox.push(msg, .{ .forever = {} });
}
_ = self.renderer_mailbox.push(.{
.background_color = v.color,
}, .{ .forever = {} });
},
.cursor => {
self.cursor_color = v.color;
// See messageWriter which has similar logic and
// explains why we may have to do this.
const msg: renderer.Message = .{
.cursor_color = v.color,
};
if (self.renderer_mailbox.push(msg, .{ .instant = {} }) == 0) {
self.renderer_state.mutex.unlock();
defer self.renderer_state.mutex.lock();
_ = self.renderer_mailbox.push(msg, .{ .forever = {} });
}
},
else => {
const i = @intFromEnum(v.key);
if (i > 255) {
log.warn("ignoring unsupported kitty color protocol key: {s}", .{@tagName(v.key)});
continue;
}
self.terminal.flags.dirty.palette = true;
self.terminal.color_palette.colors[i] = v.color;
self.terminal.color_palette.mask.unset(i);
},
},
.reset => |key| switch (key) {
.foreground => {
self.foreground_color = self.default_foreground_color;
// See messageWriter which has similar logic and
// explains why we may have to do this.
const msg: renderer.Message = .{
.foreground_color = self.default_foreground_color,
};
if (self.renderer_mailbox.push(msg, .{ .instant = {} }) == 0) {
self.renderer_state.mutex.unlock();
defer self.renderer_state.mutex.lock();
_ = self.renderer_mailbox.push(msg, .{ .forever = {} });
}
},
.background => {
self.background_color = self.default_background_color;
// See messageWriter which has similar logic and
// explains why we may have to do this.
const msg: renderer.Message = .{
.background_color = self.default_background_color,
};
if (self.renderer_mailbox.push(msg, .{ .instant = {} }) == 0) {
self.renderer_state.mutex.unlock();
defer self.renderer_state.mutex.lock();
_ = self.renderer_mailbox.push(msg, .{ .forever = {} });
}
},
.cursor => {
self.cursor_color = self.default_cursor_color;
// See messageWriter which has similar logic and
// explains why we may have to do this.
const msg: renderer.Message = .{
.cursor_color = self.default_cursor_color,
};
if (self.renderer_mailbox.push(msg, .{ .instant = {} }) == 0) {
self.renderer_state.mutex.unlock();
defer self.renderer_state.mutex.lock();
_ = self.renderer_mailbox.push(msg, .{ .forever = {} });
}
},
else => {
const i = @intFromEnum(key);
if (i > 255) {
log.warn("ignoring unsupported kitty color protocol key: {s}", .{@tagName(key)});
continue;
}
self.terminal.flags.dirty.palette = true;
self.terminal.color_palette.colors[i] = self.terminal.default_palette[i];
self.terminal.color_palette.mask.unset(i);
},
},
}
}
try writer.writeAll(request.terminator.string());
const msg = termio.Message{
.write_alloc = .{
.alloc = self.alloc,
.data = try buf.toOwnedSlice(),
},
};
self.messageWriter(msg);
}
};