mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
terminal: add write_pty effect and implement DECRQM
Add a generic write_pty effect callback to the stream terminal handler, allowing callers to receive pty response data. Use it to implement request_mode and request_mode_unknown (DECRQM), which encode the mode state as a DECRPM response and write it back through the callback. Previously these were silently ignored. The write_pty data is stack-allocated and only valid for the duration of the call.
This commit is contained in:
@@ -35,11 +35,17 @@ pub const Handler = struct {
|
||||
/// Called when the bell is rung (BEL).
|
||||
bell: ?*const fn (*Handler) void,
|
||||
|
||||
/// Called when the terminal needs to write data back to the pty,
|
||||
/// e.g. in response to a DECRQM query. The data is only valid
|
||||
/// during the lifetime of the call.
|
||||
write_pty: ?*const fn (*Handler, [:0]const u8) void,
|
||||
|
||||
/// No effects means that the stream effectively becomes readonly
|
||||
/// that only affects pure terminal state and ignores all side
|
||||
/// effects beyond that.
|
||||
pub const readonly: Effects = .{
|
||||
.bell = null,
|
||||
.write_pty = null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -184,6 +190,8 @@ pub const Handler = struct {
|
||||
|
||||
// Effect-based handlers
|
||||
.bell => self.bell(),
|
||||
.request_mode => self.requestMode(value.mode),
|
||||
.request_mode_unknown => self.requestModeUnknown(value.mode, value.ansi),
|
||||
|
||||
// No supported DCS commands have any terminal-modifying effects,
|
||||
// but they may in the future. For now we just ignore it.
|
||||
@@ -201,8 +209,6 @@ pub const Handler = struct {
|
||||
|
||||
// Have no terminal-modifying effect
|
||||
.enquiry,
|
||||
.request_mode,
|
||||
.request_mode_unknown,
|
||||
.size_report,
|
||||
.xtversion,
|
||||
.device_attributes,
|
||||
@@ -224,6 +230,36 @@ pub const Handler = struct {
|
||||
func(self);
|
||||
}
|
||||
|
||||
inline fn writePty(self: *Handler, data: [:0]const u8) void {
|
||||
const func = self.effects.write_pty orelse return;
|
||||
func(self, data);
|
||||
}
|
||||
|
||||
fn requestMode(self: *Handler, mode: modes.Mode) void {
|
||||
const report = self.terminal.modes.getReport(.fromMode(mode));
|
||||
self.sendModeReport(report);
|
||||
}
|
||||
|
||||
fn requestModeUnknown(self: *Handler, mode_raw: u16, ansi: bool) void {
|
||||
const report = self.terminal.modes.getReport(.{
|
||||
.value = @truncate(mode_raw),
|
||||
.ansi = ansi,
|
||||
});
|
||||
self.sendModeReport(report);
|
||||
}
|
||||
|
||||
fn sendModeReport(self: *Handler, report: modes.Report) void {
|
||||
var buf: [modes.Report.max_size + 1]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
report.encode(&writer) catch |err| {
|
||||
log.warn("error encoding mode report err={}", .{err});
|
||||
return;
|
||||
};
|
||||
const len = writer.buffered().len;
|
||||
buf[len] = 0;
|
||||
self.writePty(buf[0..len :0]);
|
||||
}
|
||||
|
||||
inline fn horizontalTab(self: *Handler, count: u16) void {
|
||||
for (0..count) |_| {
|
||||
const x = self.terminal.screens.active.cursor.x;
|
||||
@@ -1064,6 +1100,54 @@ test "bell effect callback" {
|
||||
}
|
||||
}
|
||||
|
||||
test "request mode DECRQM with write_pty callback" {
|
||||
var t: Terminal = try .init(testing.allocator, .{ .cols = 80, .rows = 24 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
// Without callback, DECRQM should not crash
|
||||
{
|
||||
var s: Stream = .initAlloc(testing.allocator, .init(&t));
|
||||
defer s.deinit();
|
||||
|
||||
// DECRQM for mode 7 (wraparound) — should be silently ignored
|
||||
s.nextSlice("\x1B[?7$p");
|
||||
}
|
||||
|
||||
t.fullReset();
|
||||
|
||||
// With callback, DECRQM should produce a response
|
||||
{
|
||||
const S = struct {
|
||||
var last_response: ?[:0]const u8 = null;
|
||||
fn writePty(_: *Handler, data: [:0]const u8) void {
|
||||
if (last_response) |old| testing.allocator.free(old);
|
||||
last_response = testing.allocator.dupeZ(u8, data) catch @panic("OOM");
|
||||
}
|
||||
};
|
||||
S.last_response = null;
|
||||
defer if (S.last_response) |old| testing.allocator.free(old);
|
||||
|
||||
var handler: Handler = .init(&t);
|
||||
handler.effects.write_pty = &S.writePty;
|
||||
|
||||
var s: Stream = .initAlloc(testing.allocator, handler);
|
||||
defer s.deinit();
|
||||
|
||||
// Wraparound mode (7) is set by default
|
||||
s.nextSlice("\x1B[?7$p");
|
||||
try testing.expectEqualStrings("\x1B[?7;1$y", S.last_response.?);
|
||||
|
||||
// Disable wraparound and query again
|
||||
s.nextSlice("\x1B[?7l");
|
||||
s.nextSlice("\x1B[?7$p");
|
||||
try testing.expectEqualStrings("\x1B[?7;2$y", S.last_response.?);
|
||||
|
||||
// Query an unknown mode
|
||||
s.nextSlice("\x1B[?9999$p");
|
||||
try testing.expectEqualStrings("\x1B[?9999;0$y", S.last_response.?);
|
||||
}
|
||||
}
|
||||
|
||||
test "stream: CSI W with intermediate but no params" {
|
||||
// Regression test from AFL++ crash. CSI ? W without
|
||||
// parameters caused an out-of-bounds access on input.params[0].
|
||||
|
||||
Reference in New Issue
Block a user