mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
1277 lines
46 KiB
Zig
1277 lines
46 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,
|
|
|
|
// Below here, the cursor styles aren't represented by
|
|
// DECSCUSR so we map it to some other style.
|
|
.block_hollow => if (blink) 1 else 2,
|
|
};
|
|
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 }),
|
|
}
|
|
}
|
|
};
|