Skip to content

Commit

Permalink
Merge pull request #2057 from jcollie/xtwinops
Browse files Browse the repository at this point in the history
[DRAFT] Implement the XTWINOPS (CSI t) control sequences that "make sense".
  • Loading branch information
mitchellh authored Aug 10, 2024
2 parents aa5aad4 + 61ad6d1 commit edea928
Show file tree
Hide file tree
Showing 12 changed files with 256 additions and 24 deletions.
25 changes: 25 additions & 0 deletions src/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,31 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
try self.rt_surface.setTitle(slice);
},

.report_title => |style| {
const title: ?[:0]const u8 = title: {
if (!@hasDecl(apprt.runtime.Surface, "getTitle")) break :title null;
break :title self.rt_surface.getTitle();
};

const data = switch (style) {
.csi_21_t => try std.fmt.allocPrint(
self.alloc,
"\x1b]l{s}\x1b\\",
.{title orelse ""},
),
};

// We always use an allocating message because we don't know
// the length of the title and this isn't a performance critical
// path.
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);
Expand Down
17 changes: 17 additions & 0 deletions src/apprt/embedded.zig
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,10 @@ pub const Surface = struct {
keymap_state: input.Keymap.State,
inspector: ?*Inspector = null,

/// The current title of the surface. The embedded apprt saves this so
/// that getTitle works without the implementer needing to save it.
title: ?[:0]const u8 = null,

/// Surface initialization options.
pub const Options = extern struct {
/// The platform that this surface is being initialized for and
Expand Down Expand Up @@ -429,6 +433,9 @@ pub const Surface = struct {
// Shut down our inspector
self.freeInspector();

// Free our title
if (self.title) |v| self.app.core_app.alloc.free(v);

// Remove ourselves from the list of known surfaces in the app.
self.app.core_app.deleteSurface(self);

Expand Down Expand Up @@ -537,12 +544,22 @@ pub const Surface = struct {
}

pub fn setTitle(self: *Surface, slice: [:0]const u8) !void {
// Dupe the title so that we can store it. If we get an allocation
// error we just ignore it, since this only breaks a few minor things.
const alloc = self.app.core_app.alloc;
if (self.title) |v| alloc.free(v);
self.title = alloc.dupeZ(u8, slice) catch null;

self.app.opts.set_title(
self.userdata,
slice.ptr,
);
}

pub fn getTitle(self: *Surface) ?[:0]const u8 {
return self.title;
}

pub fn setMouseShape(self: *Surface, shape: terminal.MouseShape) !void {
self.app.opts.set_mouse_shape(
self.userdata,
Expand Down
16 changes: 15 additions & 1 deletion src/apprt/glfw.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions src/apprt/gtk/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions src/apprt/surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -57,6 +60,12 @@ pub const Message = union(enum) {

/// Report the color scheme
report_color_scheme: void,

pub const ReportTitleStyle = enum {
csi_21_t,

// This enum is a placeholder for future title styles.
};
};

/// A surface mailbox.
Expand Down
15 changes: 12 additions & 3 deletions src/terminal/csi.zig
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Modes for the ED CSI command.
/// Modes for the ED CSI command.
pub const EraseDisplay = enum(u8) {
below = 0,
above = 1,
Expand All @@ -10,7 +10,7 @@ pub const EraseDisplay = enum(u8) {
scroll_complete = 22,
};

// Modes for the EL CSI command.
/// Modes for the EL CSI command.
pub const EraseLine = enum(u8) {
right = 0,
left = 1,
Expand All @@ -22,7 +22,7 @@ pub const EraseLine = enum(u8) {
_,
};

// Modes for the TBC (tab clear) command.
/// Modes for the TBC (tab clear) command.
pub const TabClear = enum(u8) {
current = 0,
all = 3,
Expand All @@ -31,3 +31,12 @@ pub const TabClear = enum(u8) {
// user-generated.
_,
};

/// Style formats for terminal size reports.
pub const SizeReportStyle = enum {
// XTWINOPS
csi_14_t,
csi_16_t,
csi_18_t,
csi_21_t,
};
1 change: 1 addition & 0 deletions src/terminal/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub const Pin = PageList.Pin;
pub const Screen = @import("Screen.zig");
pub const ScreenType = Terminal.ScreenType;
pub const Selection = @import("Selection.zig");
pub const SizeReportStyle = csi.SizeReportStyle;
pub const StringMap = @import("StringMap.zig");
pub const Style = style.Style;
pub const Terminal = @import("Terminal.zig");
Expand Down
108 changes: 108 additions & 0 deletions src/terminal/stream.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1109,6 +1109,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, "sendSizeReport")) {
self.handler.sendSizeReport(.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, "sendSizeReport")) {
self.handler.sendSizeReport(.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, "sendSizeReport")) {
self.handler.sendSizeReport(.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, "sendSizeReport")) {
self.handler.sendSizeReport(.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()
Expand Down Expand Up @@ -2047,3 +2116,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: ?csi.SizeReportStyle = null,

pub fn sendSizeReport(self: *@This(), style: csi.SizeReportStyle) void {
self.style = style;
}
};

var s: Stream(H) = .{ .handler = .{} };

try s.nextSlice("\x1b[14t");
try testing.expectEqual(csi.SizeReportStyle.csi_14_t, s.handler.style);

try s.nextSlice("\x1b[16t");
try testing.expectEqual(csi.SizeReportStyle.csi_16_t, s.handler.style);

try s.nextSlice("\x1b[18t");
try testing.expectEqual(csi.SizeReportStyle.csi_18_t, s.handler.style);

try s.nextSlice("\x1b[21t");
try testing.expectEqual(csi.SizeReportStyle.csi_21_t, s.handler.style);
}

test "stream: invalid CSI t" {
const H = struct {
style: ?csi.SizeReportStyle = null,

pub fn sendSizeReport(self: *@This(), style: csi.SizeReportStyle) void {
self.style = style;
}
};

var s: Stream(H) = .{ .handler = .{} };

try s.nextSlice("\x1b[19t");
try testing.expectEqual(null, s.handler.style);
}
56 changes: 41 additions & 15 deletions src/termio/Termio.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/termio/Thread.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Loading

0 comments on commit edea928

Please sign in to comment.