mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Implement the XTWINOPS (CSI t) control sequences that "make sense".
These sequences were implemented: CSI 14 t - report the text area size in pixels CSI 16 t - report the cell size in pixels CSI 18 t - report the text area size in cells CSI 21 t - report the window title These sequences were not implemented because they manuipulate the window state in ways that we do not want. CSI 1 t CSI 2 t CSI 3 ; x ; y t CSI 4 ; height ; width ; t CSI 5 t CSI 6 t CSI 7 t CSI 8 ; height ; width ; t CSI 9 ; 0 t CSI 9 ; 1 t CSI 9 ; 2 t CSI 9 ; 3 t CSI 10 ; 0 t CSI 10 ; 1 t CSI 10 ; 2 t CSI 24 t These sequences were not implemented because they do not make sense in a Wayland context: CSI 11 t CSI 13 t CSI 14 ; 2 t These sequences were not implemented because they provide information about the screen that is unnecessary. CSI 15 t CSI 19 t These sequences were not implemeted because Ghostty does not maintain an icon title for windows. CSI 20 t CSI 22 ; 0 t CSI 22 ; 1 t CSI 23 ; 0 t CSI 23 ; 1 t These sequences were not implemented because of the additional complexity of maintaining a stack of window titles. CSI 22 ; 2 t CSI 23 ; 2 t
This commit is contained in:
@ -408,6 +408,7 @@ typedef void (*ghostty_runtime_wakeup_cb)(void*);
|
||||
typedef const ghostty_config_t (*ghostty_runtime_reload_config_cb)(void*);
|
||||
typedef void (*ghostty_runtime_open_config_cb)(void*);
|
||||
typedef void (*ghostty_runtime_set_title_cb)(void*, const char*);
|
||||
typedef const char* (*ghostty_runtime_get_title_cb)(void*);
|
||||
typedef void (*ghostty_runtime_set_mouse_shape_cb)(void*,
|
||||
ghostty_mouse_shape_e);
|
||||
typedef void (*ghostty_runtime_set_mouse_visibility_cb)(void*, bool);
|
||||
@ -462,6 +463,7 @@ typedef struct {
|
||||
ghostty_runtime_reload_config_cb reload_config_cb;
|
||||
ghostty_runtime_open_config_cb open_config_cb;
|
||||
ghostty_runtime_set_title_cb set_title_cb;
|
||||
ghostty_runtime_get_title_cb get_title_cb;
|
||||
ghostty_runtime_set_mouse_shape_cb set_mouse_shape_cb;
|
||||
ghostty_runtime_set_mouse_visibility_cb set_mouse_visibility_cb;
|
||||
ghostty_runtime_read_clipboard_cb read_clipboard_cb;
|
||||
|
@ -70,6 +70,7 @@ extension Ghostty {
|
||||
reload_config_cb: { userdata in App.reloadConfig(userdata) },
|
||||
open_config_cb: { userdata in App.openConfig(userdata) },
|
||||
set_title_cb: { userdata, title in App.setTitle(userdata, title: title) },
|
||||
get_title_cb: { userdata in App.title(userdata) },
|
||||
set_mouse_shape_cb: { userdata, shape in App.setMouseShape(userdata, shape: shape) },
|
||||
set_mouse_visibility_cb: { userdata, visible in App.setMouseVisibility(userdata, visible: visible) },
|
||||
read_clipboard_cb: { userdata, loc, state in App.readClipboard(userdata, location: loc, state: state) },
|
||||
|
@ -713,6 +713,26 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
try self.rt_surface.setTitle(slice);
|
||||
},
|
||||
|
||||
.report_title => |style| {
|
||||
const title: ?[:0]const u8 = if (@hasDecl(apprt.runtime.Surface, "getTitle"))
|
||||
self.rt_surface.getTitle()
|
||||
else
|
||||
// If the apprt does not implement getTitle, report a
|
||||
// blank title.
|
||||
"";
|
||||
|
||||
const data = switch (style) {
|
||||
.csi_21_t => try std.fmt.allocPrint(self.alloc, "\x1b]l{s}\x1b\\", .{title orelse ""}),
|
||||
};
|
||||
|
||||
self.io.queueMessage(.{
|
||||
.write_alloc = .{
|
||||
.alloc = self.alloc,
|
||||
.data = data,
|
||||
},
|
||||
}, .unlocked);
|
||||
},
|
||||
|
||||
.set_mouse_shape => |shape| {
|
||||
log.debug("changing mouse shape: {}", .{shape});
|
||||
try self.rt_surface.setMouseShape(shape);
|
||||
|
@ -55,6 +55,9 @@ pub const App = struct {
|
||||
/// Called to set the title of the window.
|
||||
set_title: *const fn (SurfaceUD, [*]const u8) callconv(.C) void,
|
||||
|
||||
/// Called to get the title of the window.
|
||||
get_title: ?*const fn (SurfaceUD) callconv(.C) ?[*]const u8 = null,
|
||||
|
||||
/// Called to set the cursor shape.
|
||||
set_mouse_shape: *const fn (SurfaceUD, terminal.MouseShape) callconv(.C) void,
|
||||
|
||||
@ -543,6 +546,17 @@ pub const Surface = struct {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn getTitle(self: *Surface) ?[:0]const u8 {
|
||||
const func = self.app.opts.get_title orelse {
|
||||
log.info("runtime embedder does not support get_title", .{});
|
||||
return null;
|
||||
};
|
||||
|
||||
const result = func(self.userdata);
|
||||
if (result == null) return null;
|
||||
return std.mem.sliceTo(@as([*:0]const u8, @ptrCast(result.?)), 0);
|
||||
}
|
||||
|
||||
pub fn setMouseShape(self: *Surface, shape: terminal.MouseShape) !void {
|
||||
self.app.opts.set_mouse_shape(
|
||||
self.userdata,
|
||||
|
@ -360,6 +360,11 @@ pub const Surface = struct {
|
||||
/// The monitor dimensions so we can toggle fullscreen on and off.
|
||||
monitor_dims: MonitorDimensions,
|
||||
|
||||
/// Save the title text so that we can return it later when requested.
|
||||
/// This is allocated from the heap so it must be freed when we deinit the
|
||||
/// surface.
|
||||
title_text: ?[:0]const u8 = null,
|
||||
|
||||
pub const Options = struct {};
|
||||
|
||||
/// Initialize the surface into the given self pointer. This gives a
|
||||
@ -463,6 +468,8 @@ pub const Surface = struct {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Surface) void {
|
||||
if (self.title_text) |t| self.core_surface.alloc.free(t);
|
||||
|
||||
// Remove ourselves from the list of known surfaces in the app.
|
||||
self.app.app.deleteSurface(self);
|
||||
|
||||
@ -609,7 +616,14 @@ pub const Surface = struct {
|
||||
|
||||
/// Set the title of the window.
|
||||
pub fn setTitle(self: *Surface, slice: [:0]const u8) !void {
|
||||
self.window.setTitle(slice.ptr);
|
||||
if (self.title_text) |t| self.core_surface.alloc.free(t);
|
||||
self.title_text = try self.core_surface.alloc.dupeZ(u8, slice);
|
||||
self.window.setTitle(self.title_text.?.ptr);
|
||||
}
|
||||
|
||||
/// Return the title of the window.
|
||||
pub fn getTitle(self: *Surface) ?[:0]const u8 {
|
||||
return self.title_text;
|
||||
}
|
||||
|
||||
/// Set the shape of the cursor.
|
||||
|
@ -935,6 +935,10 @@ pub fn setTitle(self: *Surface, slice: [:0]const u8) !void {
|
||||
self.updateTitleLabels();
|
||||
}
|
||||
|
||||
pub fn getTitle(self: *Surface) ?[:0]const u8 {
|
||||
return self.title_text;
|
||||
}
|
||||
|
||||
pub fn setMouseShape(
|
||||
self: *Surface,
|
||||
shape: terminal.MouseShape,
|
||||
|
@ -18,6 +18,9 @@ pub const Message = union(enum) {
|
||||
/// of any length
|
||||
set_title: [256]u8,
|
||||
|
||||
/// Report the window title back to the terminal
|
||||
report_title: ReportTitleStyle,
|
||||
|
||||
/// Set the mouse shape.
|
||||
set_mouse_shape: terminal.MouseShape,
|
||||
|
||||
@ -57,6 +60,10 @@ pub const Message = union(enum) {
|
||||
|
||||
/// Report the color scheme
|
||||
report_color_scheme: void,
|
||||
|
||||
pub const ReportTitleStyle = enum {
|
||||
csi_21_t,
|
||||
};
|
||||
};
|
||||
|
||||
/// A surface mailbox.
|
||||
|
@ -3,7 +3,7 @@ const builtin = @import("builtin");
|
||||
pub usingnamespace @import("sanitize.zig");
|
||||
|
||||
const charsets = @import("charsets.zig");
|
||||
const stream = @import("stream.zig");
|
||||
pub const stream = @import("stream.zig");
|
||||
const ansi = @import("ansi.zig");
|
||||
const csi = @import("csi.zig");
|
||||
const hyperlink = @import("hyperlink.zig");
|
||||
|
@ -24,6 +24,13 @@ const log = std.log.scoped(.stream);
|
||||
/// do something else.
|
||||
const debug = false;
|
||||
|
||||
pub const ReportStyle = enum {
|
||||
csi_14_t,
|
||||
csi_16_t,
|
||||
csi_18_t,
|
||||
csi_21_t,
|
||||
};
|
||||
|
||||
/// Returns a type that can process a stream of tty control characters.
|
||||
/// This will call various callback functions on type T. Type T only has to
|
||||
/// implement the callbacks it cares about; any unimplemented callbacks will
|
||||
@ -1109,6 +1116,75 @@ pub fn Stream(comptime Handler: type) type {
|
||||
),
|
||||
},
|
||||
|
||||
// XTWINOPS
|
||||
't' => switch (input.intermediates.len) {
|
||||
0 => {
|
||||
if (input.params.len > 0) {
|
||||
switch (input.params[0]) {
|
||||
14 => if (input.params.len == 1) {
|
||||
// report the text area size in pixels
|
||||
if (@hasDecl(T, "sendReport")) {
|
||||
self.handler.sendReport(.csi_14_t);
|
||||
} else log.warn(
|
||||
"ignoring unimplemented CSI 14 t",
|
||||
.{},
|
||||
);
|
||||
} else log.warn(
|
||||
"ignoring CSI 14 t with extra parameters: {}",
|
||||
.{input},
|
||||
),
|
||||
16 => if (input.params.len == 1) {
|
||||
// report cell size in pixels
|
||||
if (@hasDecl(T, "sendReport")) {
|
||||
self.handler.sendReport(.csi_16_t);
|
||||
} else log.warn(
|
||||
"ignoring unimplemented CSI 16 t",
|
||||
.{},
|
||||
);
|
||||
} else log.warn(
|
||||
"ignoring CSI 16 t with extra parameters: {s}",
|
||||
.{input},
|
||||
),
|
||||
18 => if (input.params.len == 1) {
|
||||
// report screen size in characters
|
||||
if (@hasDecl(T, "sendReport")) {
|
||||
self.handler.sendReport(.csi_18_t);
|
||||
} else log.warn(
|
||||
"ignoring unimplemented CSI 18 t",
|
||||
.{},
|
||||
);
|
||||
} else log.warn(
|
||||
"ignoring CSI 18 t with extra parameters: {s}",
|
||||
.{input},
|
||||
),
|
||||
21 => if (input.params.len == 1) {
|
||||
// report window title
|
||||
if (@hasDecl(T, "sendReport")) {
|
||||
self.handler.sendReport(.csi_21_t);
|
||||
} else log.warn(
|
||||
"ignoring unimplemented CSI 21 t",
|
||||
.{},
|
||||
);
|
||||
} else log.warn(
|
||||
"ignoring CSI 21 t with extra parameters: {s}",
|
||||
.{input},
|
||||
),
|
||||
else => log.warn(
|
||||
"ignoring CSI t with unimplemented parameter: {s}",
|
||||
.{input},
|
||||
),
|
||||
}
|
||||
} else log.err(
|
||||
"ignoring CSI t with no parameters: {s}",
|
||||
.{input},
|
||||
);
|
||||
},
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI t with intermediates: {s}",
|
||||
.{input},
|
||||
),
|
||||
},
|
||||
|
||||
'u' => switch (input.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "restoreCursor"))
|
||||
try self.handler.restoreCursor()
|
||||
@ -2047,3 +2123,42 @@ test "stream: csi param too long" {
|
||||
var s: Stream(H) = .{ .handler = .{} };
|
||||
try s.nextSlice("\x1B[1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111C");
|
||||
}
|
||||
|
||||
test "stream: send report with CSI t" {
|
||||
const H = struct {
|
||||
style: ?ReportStyle = null,
|
||||
|
||||
pub fn sendReport(self: *@This(), style: ReportStyle) void {
|
||||
self.style = style;
|
||||
}
|
||||
};
|
||||
|
||||
var s: Stream(H) = .{ .handler = .{} };
|
||||
|
||||
try s.nextSlice("\x1b[14t");
|
||||
try testing.expectEqual(ReportStyle.csi_14_t, s.handler.style);
|
||||
|
||||
try s.nextSlice("\x1b[16t");
|
||||
try testing.expectEqual(ReportStyle.csi_16_t, s.handler.style);
|
||||
|
||||
try s.nextSlice("\x1b[18t");
|
||||
try testing.expectEqual(ReportStyle.csi_18_t, s.handler.style);
|
||||
|
||||
try s.nextSlice("\x1b[21t");
|
||||
try testing.expectEqual(ReportStyle.csi_21_t, s.handler.style);
|
||||
}
|
||||
|
||||
test "stream: invalid CSI t" {
|
||||
const H = struct {
|
||||
style: ?ReportStyle = null,
|
||||
|
||||
pub fn sendReport(self: *@This(), style: ReportStyle) void {
|
||||
self.style = style;
|
||||
}
|
||||
};
|
||||
|
||||
var s: Stream(H) = .{ .handler = .{} };
|
||||
|
||||
try s.nextSlice("\x1b[19t");
|
||||
try testing.expectEqual(null, s.handler.style);
|
||||
}
|
||||
|
@ -383,32 +383,58 @@ pub fn resize(
|
||||
|
||||
// If we have size reporting enabled we need to send a report.
|
||||
if (self.terminal.modes.get(.in_band_size_reports)) {
|
||||
try self.sizeReportLocked(td);
|
||||
try self.sizeReportLocked(td, .mode_2048);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Make a mode 2048 in-band size report.
|
||||
pub fn sizeReport(self: *Termio, td: *ThreadData) !void {
|
||||
/// Make a size report.
|
||||
pub fn sizeReport(self: *Termio, td: *ThreadData, style: termio.Message.SizeReport) !void {
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
try self.sizeReportLocked(td);
|
||||
try self.sizeReportLocked(td, style);
|
||||
}
|
||||
|
||||
fn sizeReportLocked(self: *Termio, td: *ThreadData) !void {
|
||||
fn sizeReportLocked(self: *Termio, td: *ThreadData, style: termio.Message.SizeReport) !void {
|
||||
// 1024 bytes should be enough for size report since report
|
||||
// in columns and pixels.
|
||||
var buf: [1024]u8 = undefined;
|
||||
const message = try std.fmt.bufPrint(
|
||||
&buf,
|
||||
"\x1B[48;{};{};{};{}t",
|
||||
.{
|
||||
self.grid_size.rows,
|
||||
self.grid_size.columns,
|
||||
self.terminal.height_px,
|
||||
self.terminal.width_px,
|
||||
},
|
||||
);
|
||||
const message = switch (style) {
|
||||
.mode_2048 => try std.fmt.bufPrint(
|
||||
&buf,
|
||||
"\x1B[48;{};{};{};{}t",
|
||||
.{
|
||||
self.grid_size.rows,
|
||||
self.grid_size.columns,
|
||||
self.terminal.height_px,
|
||||
self.terminal.width_px,
|
||||
},
|
||||
),
|
||||
.csi_14_t => try std.fmt.bufPrint(
|
||||
&buf,
|
||||
"\x1b[4;{};{}t",
|
||||
.{
|
||||
self.terminal.height_px,
|
||||
self.terminal.width_px,
|
||||
},
|
||||
),
|
||||
.csi_16_t => try std.fmt.bufPrint(
|
||||
&buf,
|
||||
"\x1b[6;{};{}t",
|
||||
.{
|
||||
self.terminal.height_px / self.grid_size.rows,
|
||||
self.terminal.width_px / self.grid_size.columns,
|
||||
},
|
||||
),
|
||||
.csi_18_t => try std.fmt.bufPrint(
|
||||
&buf,
|
||||
"\x1b[8;{};{}t",
|
||||
.{
|
||||
self.grid_size.rows,
|
||||
self.grid_size.columns,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
try self.queueWrite(td, message, false);
|
||||
}
|
||||
|
@ -267,7 +267,7 @@ fn drainMailbox(
|
||||
},
|
||||
.inspector => |v| self.flags.has_inspector = v,
|
||||
.resize => |v| self.handleResize(cb, v),
|
||||
.size_report => try io.sizeReport(data),
|
||||
.size_report => |v| try io.sizeReport(data, v),
|
||||
.clear_screen => |v| try io.clearScreen(data, v.history),
|
||||
.scroll_viewport => |v| try io.scrollViewport(v),
|
||||
.jump_to_prompt => |v| try io.jumpToPrompt(v),
|
||||
|
@ -42,9 +42,10 @@ pub const Message = union(enum) {
|
||||
/// Resize the window.
|
||||
resize: Resize,
|
||||
|
||||
/// Request a size report is sent to the pty (in-band size report,
|
||||
/// mode 2048: https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83)
|
||||
size_report: void,
|
||||
/// Request a size report is sent to the pty ([in-band
|
||||
/// size report, mode 2048](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83) and
|
||||
/// [XTWINOPS](https://invisible-island.net/xterm/ctlseqs/ctlseqs.htmli#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-Ps;Ps;Ps-t.1EB0)).
|
||||
size_report: SizeReport,
|
||||
|
||||
/// Clear the screen.
|
||||
clear_screen: struct {
|
||||
@ -94,6 +95,14 @@ pub const Message = union(enum) {
|
||||
.alloc => |v| Message{ .write_alloc = v },
|
||||
};
|
||||
}
|
||||
|
||||
/// The types of size reports that we support
|
||||
pub const SizeReport = enum {
|
||||
mode_2048,
|
||||
csi_14_t,
|
||||
csi_16_t,
|
||||
csi_18_t,
|
||||
};
|
||||
};
|
||||
|
||||
/// Creates a union that can be used to accommodate data that fit within an array,
|
||||
|
@ -598,7 +598,7 @@ pub const StreamHandler = struct {
|
||||
},
|
||||
|
||||
.in_band_size_reports => if (enabled) self.messageWriter(.{
|
||||
.size_report = {},
|
||||
.size_report = .mode_2048,
|
||||
}),
|
||||
|
||||
.mouse_event_x10 => {
|
||||
@ -1259,4 +1259,14 @@ pub const StreamHandler = struct {
|
||||
|
||||
self.surfaceMessageWriter(message);
|
||||
}
|
||||
|
||||
/// Send a report to the pty.
|
||||
pub fn sendReport(self: *StreamHandler, style: terminal.stream.ReportStyle) 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 }),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
Reference in New Issue
Block a user