tmux: control mode core loop (no GUI connections yet) (#9860)
Related to #1935 This adds a new structure `terminal.tmux.Viewer` which continues building on all the prior tmux control mode work to add a full bidirectional reconciliation loop to discover and sync terminal states from tmux to Ghostty and vice versa. **This is the core, cross-platform business logic that will power the GUIs, later.** Our prior work were protocol building blocks, and this PR is an actual functional piece of work. You can now start Ghostty, run `tmux -CC attach`, and we _will_ be creating full blown terminals internal that capture the content and mirror the state exactly (barring inevitable bugs in something this complex). But, we don't yet show them visually. :) And we don't yet send inputs to it (it's a viewer only, for now). **This sucked.** The control mode protocol is difficult, to put it mildly, for a variety of reasons. Correctness of this is going to be hard. Therefore, I focused really hard on this design to make it **fully unit test friendly.** We're able to simulate full tmux sessions and runt through our state machine and assert various states. I think this will be critical to correctness as we eventually collect real world data. > [!WARNING] > > This does actually have user-impacting changes! When you run `tmux -CC attach` we will now run our full control mode client. This could result in bugs or crashes or other problems. This only activates if you have a real tmux session, though, so it should be avoidable by most users. Since we don't actually take our state and send it to the GUI or anything, this should be pretty safe. **AI disclosure:** I used AI for a lot of the protocol reverse engineering and documentation to figure out how it all works. I designed the architecture myself and implemented most of it manually.pull/9864/head
commit
3b31cef965
|
|
@ -6,6 +6,7 @@ pub const output = @import("tmux/output.zig");
|
|||
pub const ControlParser = control.Parser;
|
||||
pub const ControlNotification = control.Notification;
|
||||
pub const Layout = layout.Layout;
|
||||
pub const Viewer = @import("tmux/viewer.zig").Viewer;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
|
|
|
|||
|
|
@ -531,6 +531,30 @@ pub const Notification = union(enum) {
|
|||
session_id: usize,
|
||||
name: []const u8,
|
||||
},
|
||||
|
||||
pub fn format(self: Notification, writer: *std.Io.Writer) !void {
|
||||
const T = Notification;
|
||||
const info = @typeInfo(T).@"union";
|
||||
|
||||
try writer.writeAll(@typeName(T));
|
||||
if (info.tag_type) |TagType| {
|
||||
try writer.writeAll("{ .");
|
||||
try writer.writeAll(@tagName(@as(TagType, self)));
|
||||
try writer.writeAll(" = ");
|
||||
|
||||
inline for (info.fields) |u_field| {
|
||||
if (self == @field(TagType, u_field.name)) {
|
||||
const value = @field(self, u_field.name);
|
||||
switch (u_field.type) {
|
||||
[]const u8 => try writer.print("\"{s}\"", .{std.mem.trim(u8, value, " \t\r\n")}),
|
||||
else => try writer.print("{any}", .{value}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try writer.writeAll(" }");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test "tmux begin/end empty" {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,36 @@ pub fn parseFormatStruct(
|
|||
return result;
|
||||
}
|
||||
|
||||
pub fn comptimeFormat(
|
||||
comptime vars: []const Variable,
|
||||
comptime delimiter: u8,
|
||||
) []const u8 {
|
||||
comptime {
|
||||
@setEvalBranchQuota(50000);
|
||||
var counter: std.Io.Writer.Discarding = .init(&.{});
|
||||
try format(&counter.writer, vars, delimiter);
|
||||
|
||||
var buf: [counter.count]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try format(&writer, vars, delimiter);
|
||||
const final = buf;
|
||||
return final[0..writer.end];
|
||||
}
|
||||
}
|
||||
|
||||
/// Format a set of variables into the proper format string for tmux
|
||||
/// that we can handle with `parseFormatStruct`.
|
||||
pub fn format(
|
||||
writer: *std.Io.Writer,
|
||||
vars: []const Variable,
|
||||
delimiter: u8,
|
||||
) std.Io.Writer.Error!void {
|
||||
for (vars, 0..) |variable, i| {
|
||||
if (i != 0) try writer.writeByte(delimiter);
|
||||
try writer.print("#{{{t}}}", .{variable});
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a struct type that contains fields for each of the given
|
||||
/// format variables. This can be used with `parseFormatStruct` to
|
||||
/// parse an output string into a format struct.
|
||||
|
|
@ -65,16 +95,109 @@ pub fn FormatStruct(comptime vars: []const Variable) type {
|
|||
/// a subset of them here that are relevant to the use case of implementing
|
||||
/// control mode for terminal emulators.
|
||||
pub const Variable = enum {
|
||||
/// 1 if pane is in alternate screen.
|
||||
alternate_on,
|
||||
/// Saved cursor X in alternate screen.
|
||||
alternate_saved_x,
|
||||
/// Saved cursor Y in alternate screen.
|
||||
alternate_saved_y,
|
||||
/// 1 if bracketed paste mode is enabled.
|
||||
bracketed_paste,
|
||||
/// 1 if the cursor is blinking.
|
||||
cursor_blinking,
|
||||
/// Cursor colour in pane. Possible formats:
|
||||
/// - Named colors: `black`, `red`, `green`, `yellow`, `blue`, `magenta`,
|
||||
/// `cyan`, `white`, `default`, `terminal`, or bright variants.
|
||||
/// - 256 colors: `colour<N>` where N is 0-255 (e.g., `colour100`).
|
||||
/// - RGB hex: `#RRGGBB` (e.g., `#ff0000`).
|
||||
/// - Empty string if unset.
|
||||
cursor_colour,
|
||||
/// Pane cursor flag.
|
||||
cursor_flag,
|
||||
/// Cursor shape in pane. Possible values: `block`, `underline`, `bar`,
|
||||
/// or `default`.
|
||||
cursor_shape,
|
||||
/// Cursor X position in pane.
|
||||
cursor_x,
|
||||
/// Cursor Y position in pane.
|
||||
cursor_y,
|
||||
/// 1 if focus reporting is enabled.
|
||||
focus_flag,
|
||||
/// Pane insert flag.
|
||||
insert_flag,
|
||||
/// Pane keypad cursor flag.
|
||||
keypad_cursor_flag,
|
||||
/// Pane keypad flag.
|
||||
keypad_flag,
|
||||
/// Pane mouse all flag.
|
||||
mouse_all_flag,
|
||||
/// Pane mouse any flag.
|
||||
mouse_any_flag,
|
||||
/// Pane mouse button flag.
|
||||
mouse_button_flag,
|
||||
/// Pane mouse SGR flag.
|
||||
mouse_sgr_flag,
|
||||
/// Pane mouse standard flag.
|
||||
mouse_standard_flag,
|
||||
/// Pane mouse UTF-8 flag.
|
||||
mouse_utf8_flag,
|
||||
/// Pane origin flag.
|
||||
origin_flag,
|
||||
/// Unique pane ID prefixed with `%` (e.g., `%0`, `%42`).
|
||||
pane_id,
|
||||
/// Pane tab positions as a comma-separated list of 0-indexed column
|
||||
/// numbers (e.g., `8,16,24,32`). Empty string if no tabs are set.
|
||||
pane_tabs,
|
||||
/// Bottom of scroll region in pane.
|
||||
scroll_region_lower,
|
||||
/// Top of scroll region in pane.
|
||||
scroll_region_upper,
|
||||
/// Unique session ID prefixed with `$` (e.g., `$0`, `$42`).
|
||||
session_id,
|
||||
/// Server version (e.g., `3.5a`).
|
||||
version,
|
||||
/// Unique window ID prefixed with `@` (e.g., `@0`, `@42`).
|
||||
window_id,
|
||||
/// Width of window.
|
||||
window_width,
|
||||
/// Height of window.
|
||||
window_height,
|
||||
/// Window layout description, ignoring zoomed window panes. Format is
|
||||
/// `<checksum>,<layout>` where checksum is a 4-digit hex CRC16 and layout
|
||||
/// encodes pane dimensions as `WxH,X,Y[,ID]` with `{...}` for horizontal
|
||||
/// splits and `[...]` for vertical splits.
|
||||
window_layout,
|
||||
/// Pane wrap flag.
|
||||
wrap_flag,
|
||||
|
||||
/// Parse the given string value into the appropriate resulting
|
||||
/// type for this variable.
|
||||
pub fn parse(comptime self: Variable, value: []const u8) !Type(self) {
|
||||
return switch (self) {
|
||||
.alternate_on,
|
||||
.bracketed_paste,
|
||||
.cursor_blinking,
|
||||
.cursor_flag,
|
||||
.focus_flag,
|
||||
.insert_flag,
|
||||
.keypad_cursor_flag,
|
||||
.keypad_flag,
|
||||
.mouse_all_flag,
|
||||
.mouse_any_flag,
|
||||
.mouse_button_flag,
|
||||
.mouse_sgr_flag,
|
||||
.mouse_standard_flag,
|
||||
.mouse_utf8_flag,
|
||||
.origin_flag,
|
||||
.wrap_flag,
|
||||
=> std.mem.eql(u8, value, "1"),
|
||||
.alternate_saved_x,
|
||||
.alternate_saved_y,
|
||||
.cursor_x,
|
||||
.cursor_y,
|
||||
.scroll_region_lower,
|
||||
.scroll_region_upper,
|
||||
=> try std.fmt.parseInt(usize, value, 10),
|
||||
.session_id => if (value.len >= 2 and value[0] == '$')
|
||||
try std.fmt.parseInt(usize, value[1..], 10)
|
||||
else
|
||||
|
|
@ -83,24 +206,107 @@ pub const Variable = enum {
|
|||
try std.fmt.parseInt(usize, value[1..], 10)
|
||||
else
|
||||
return error.FormatError,
|
||||
.pane_id => if (value.len >= 2 and value[0] == '%')
|
||||
try std.fmt.parseInt(usize, value[1..], 10)
|
||||
else
|
||||
return error.FormatError,
|
||||
.window_width => try std.fmt.parseInt(usize, value, 10),
|
||||
.window_height => try std.fmt.parseInt(usize, value, 10),
|
||||
.window_layout => value,
|
||||
.cursor_colour,
|
||||
.cursor_shape,
|
||||
.pane_tabs,
|
||||
.version,
|
||||
.window_layout,
|
||||
=> value,
|
||||
};
|
||||
}
|
||||
|
||||
/// The type of the parsed value for this variable type.
|
||||
pub fn Type(comptime self: Variable) type {
|
||||
return switch (self) {
|
||||
.session_id => usize,
|
||||
.window_id => usize,
|
||||
.window_width => usize,
|
||||
.window_height => usize,
|
||||
.window_layout => []const u8,
|
||||
.alternate_on,
|
||||
.bracketed_paste,
|
||||
.cursor_blinking,
|
||||
.cursor_flag,
|
||||
.focus_flag,
|
||||
.insert_flag,
|
||||
.keypad_cursor_flag,
|
||||
.keypad_flag,
|
||||
.mouse_all_flag,
|
||||
.mouse_any_flag,
|
||||
.mouse_button_flag,
|
||||
.mouse_sgr_flag,
|
||||
.mouse_standard_flag,
|
||||
.mouse_utf8_flag,
|
||||
.origin_flag,
|
||||
.wrap_flag,
|
||||
=> bool,
|
||||
.alternate_saved_x,
|
||||
.alternate_saved_y,
|
||||
.cursor_x,
|
||||
.cursor_y,
|
||||
.scroll_region_lower,
|
||||
.scroll_region_upper,
|
||||
.session_id,
|
||||
.window_id,
|
||||
.pane_id,
|
||||
.window_width,
|
||||
.window_height,
|
||||
=> usize,
|
||||
.cursor_colour,
|
||||
.cursor_shape,
|
||||
.pane_tabs,
|
||||
.version,
|
||||
.window_layout,
|
||||
=> []const u8,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
test "parse alternate_on" {
|
||||
try testing.expectEqual(true, try Variable.parse(.alternate_on, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.alternate_on, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.alternate_on, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.alternate_on, "true"));
|
||||
try testing.expectEqual(false, try Variable.parse(.alternate_on, "yes"));
|
||||
}
|
||||
|
||||
test "parse alternate_saved_x" {
|
||||
try testing.expectEqual(0, try Variable.parse(.alternate_saved_x, "0"));
|
||||
try testing.expectEqual(42, try Variable.parse(.alternate_saved_x, "42"));
|
||||
try testing.expectError(error.InvalidCharacter, Variable.parse(.alternate_saved_x, "abc"));
|
||||
}
|
||||
|
||||
test "parse alternate_saved_y" {
|
||||
try testing.expectEqual(0, try Variable.parse(.alternate_saved_y, "0"));
|
||||
try testing.expectEqual(42, try Variable.parse(.alternate_saved_y, "42"));
|
||||
try testing.expectError(error.InvalidCharacter, Variable.parse(.alternate_saved_y, "abc"));
|
||||
}
|
||||
|
||||
test "parse cursor_x" {
|
||||
try testing.expectEqual(0, try Variable.parse(.cursor_x, "0"));
|
||||
try testing.expectEqual(79, try Variable.parse(.cursor_x, "79"));
|
||||
try testing.expectError(error.InvalidCharacter, Variable.parse(.cursor_x, "abc"));
|
||||
}
|
||||
|
||||
test "parse cursor_y" {
|
||||
try testing.expectEqual(0, try Variable.parse(.cursor_y, "0"));
|
||||
try testing.expectEqual(23, try Variable.parse(.cursor_y, "23"));
|
||||
try testing.expectError(error.InvalidCharacter, Variable.parse(.cursor_y, "abc"));
|
||||
}
|
||||
|
||||
test "parse scroll_region_upper" {
|
||||
try testing.expectEqual(0, try Variable.parse(.scroll_region_upper, "0"));
|
||||
try testing.expectEqual(5, try Variable.parse(.scroll_region_upper, "5"));
|
||||
try testing.expectError(error.InvalidCharacter, Variable.parse(.scroll_region_upper, "abc"));
|
||||
}
|
||||
|
||||
test "parse scroll_region_lower" {
|
||||
try testing.expectEqual(0, try Variable.parse(.scroll_region_lower, "0"));
|
||||
try testing.expectEqual(23, try Variable.parse(.scroll_region_lower, "23"));
|
||||
try testing.expectError(error.InvalidCharacter, Variable.parse(.scroll_region_lower, "abc"));
|
||||
}
|
||||
|
||||
test "parse session id" {
|
||||
try testing.expectEqual(42, try Variable.parse(.session_id, "$42"));
|
||||
try testing.expectEqual(0, try Variable.parse(.session_id, "$0"));
|
||||
|
|
@ -146,6 +352,147 @@ test "parse window layout" {
|
|||
try testing.expectEqualStrings("a]b,c{d}e(f)", try Variable.parse(.window_layout, "a]b,c{d}e(f)"));
|
||||
}
|
||||
|
||||
test "parse cursor_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.cursor_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.cursor_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.cursor_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.cursor_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse insert_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.insert_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.insert_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.insert_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.insert_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse keypad_cursor_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.keypad_cursor_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.keypad_cursor_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.keypad_cursor_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.keypad_cursor_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse keypad_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.keypad_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.keypad_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.keypad_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.keypad_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse mouse_any_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.mouse_any_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_any_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_any_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_any_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse mouse_button_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.mouse_button_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_button_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_button_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_button_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse mouse_sgr_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.mouse_sgr_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_sgr_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_sgr_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_sgr_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse mouse_standard_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.mouse_standard_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_standard_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_standard_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_standard_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse mouse_utf8_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.mouse_utf8_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_utf8_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_utf8_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_utf8_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse wrap_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.wrap_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.wrap_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.wrap_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.wrap_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse bracketed_paste" {
|
||||
try testing.expectEqual(true, try Variable.parse(.bracketed_paste, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.bracketed_paste, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.bracketed_paste, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.bracketed_paste, "true"));
|
||||
}
|
||||
|
||||
test "parse cursor_blinking" {
|
||||
try testing.expectEqual(true, try Variable.parse(.cursor_blinking, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.cursor_blinking, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.cursor_blinking, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.cursor_blinking, "true"));
|
||||
}
|
||||
|
||||
test "parse focus_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.focus_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.focus_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.focus_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.focus_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse mouse_all_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.mouse_all_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_all_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_all_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.mouse_all_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse origin_flag" {
|
||||
try testing.expectEqual(true, try Variable.parse(.origin_flag, "1"));
|
||||
try testing.expectEqual(false, try Variable.parse(.origin_flag, "0"));
|
||||
try testing.expectEqual(false, try Variable.parse(.origin_flag, ""));
|
||||
try testing.expectEqual(false, try Variable.parse(.origin_flag, "true"));
|
||||
}
|
||||
|
||||
test "parse pane_id" {
|
||||
try testing.expectEqual(42, try Variable.parse(.pane_id, "%42"));
|
||||
try testing.expectEqual(0, try Variable.parse(.pane_id, "%0"));
|
||||
try testing.expectError(error.FormatError, Variable.parse(.pane_id, "0"));
|
||||
try testing.expectError(error.FormatError, Variable.parse(.pane_id, "@0"));
|
||||
try testing.expectError(error.FormatError, Variable.parse(.pane_id, "%"));
|
||||
try testing.expectError(error.FormatError, Variable.parse(.pane_id, ""));
|
||||
try testing.expectError(error.InvalidCharacter, Variable.parse(.pane_id, "%abc"));
|
||||
}
|
||||
|
||||
test "parse cursor_colour" {
|
||||
try testing.expectEqualStrings("red", try Variable.parse(.cursor_colour, "red"));
|
||||
try testing.expectEqualStrings("#ff0000", try Variable.parse(.cursor_colour, "#ff0000"));
|
||||
try testing.expectEqualStrings("", try Variable.parse(.cursor_colour, ""));
|
||||
}
|
||||
|
||||
test "parse cursor_shape" {
|
||||
try testing.expectEqualStrings("block", try Variable.parse(.cursor_shape, "block"));
|
||||
try testing.expectEqualStrings("underline", try Variable.parse(.cursor_shape, "underline"));
|
||||
try testing.expectEqualStrings("bar", try Variable.parse(.cursor_shape, "bar"));
|
||||
try testing.expectEqualStrings("", try Variable.parse(.cursor_shape, ""));
|
||||
}
|
||||
|
||||
test "parse pane_tabs" {
|
||||
try testing.expectEqualStrings("0,8,16,24", try Variable.parse(.pane_tabs, "0,8,16,24"));
|
||||
try testing.expectEqualStrings("", try Variable.parse(.pane_tabs, ""));
|
||||
try testing.expectEqualStrings("0", try Variable.parse(.pane_tabs, "0"));
|
||||
}
|
||||
|
||||
test "parse version" {
|
||||
try testing.expectEqualStrings("3.5a", try Variable.parse(.version, "3.5a"));
|
||||
try testing.expectEqualStrings("3.5", try Variable.parse(.version, "3.5"));
|
||||
try testing.expectEqualStrings("next-3.5", try Variable.parse(.version, "next-3.5"));
|
||||
try testing.expectEqualStrings("", try Variable.parse(.version, ""));
|
||||
}
|
||||
|
||||
test "parseFormatStruct single field" {
|
||||
const T = FormatStruct(&.{.session_id});
|
||||
const result = try parseFormatStruct(T, "$42", ' ');
|
||||
|
|
@ -203,3 +550,41 @@ test "parseFormatStruct with empty layout field" {
|
|||
try testing.expectEqual(1, result.session_id);
|
||||
try testing.expectEqualStrings("", result.window_layout);
|
||||
}
|
||||
|
||||
fn testFormat(
|
||||
comptime vars: []const Variable,
|
||||
comptime delimiter: u8,
|
||||
comptime expected: []const u8,
|
||||
) !void {
|
||||
const comptime_result = comptime comptimeFormat(vars, delimiter);
|
||||
try testing.expectEqualStrings(expected, comptime_result);
|
||||
|
||||
var buf: [256]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try format(&writer, vars, delimiter);
|
||||
try testing.expectEqualStrings(expected, buf[0..writer.end]);
|
||||
}
|
||||
|
||||
test "format single variable" {
|
||||
try testFormat(&.{.session_id}, ' ', "#{session_id}");
|
||||
}
|
||||
|
||||
test "format multiple variables" {
|
||||
try testFormat(&.{ .session_id, .window_id, .window_width, .window_height }, ' ', "#{session_id} #{window_id} #{window_width} #{window_height}");
|
||||
}
|
||||
|
||||
test "format with comma delimiter" {
|
||||
try testFormat(&.{ .window_id, .window_layout }, ',', "#{window_id},#{window_layout}");
|
||||
}
|
||||
|
||||
test "format with tab delimiter" {
|
||||
try testFormat(&.{ .window_width, .window_height }, '\t', "#{window_width}\t#{window_height}");
|
||||
}
|
||||
|
||||
test "format empty variables" {
|
||||
try testFormat(&.{}, ' ', "");
|
||||
}
|
||||
|
||||
test "format all variables" {
|
||||
try testFormat(&.{ .session_id, .window_id, .window_width, .window_height, .window_layout }, ' ', "#{session_id} #{window_id} #{window_width} #{window_height} #{window_layout}");
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -70,6 +70,9 @@ pub const StreamHandler = struct {
|
|||
/// such as XTGETTCAP.
|
||||
dcs: terminal.dcs.Handler = .{},
|
||||
|
||||
/// The tmux control mode viewer state.
|
||||
tmux_viewer: if (tmux_enabled) ?*terminal.tmux.Viewer else void = if (tmux_enabled) null else {},
|
||||
|
||||
/// 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.
|
||||
|
|
@ -81,9 +84,18 @@ pub const StreamHandler = struct {
|
|||
|
||||
pub const Stream = terminal.Stream(StreamHandler);
|
||||
|
||||
/// True if we have tmux control mode built in.
|
||||
pub const tmux_enabled = terminal.options.tmux_control_mode;
|
||||
|
||||
pub fn deinit(self: *StreamHandler) void {
|
||||
self.apc.deinit();
|
||||
self.dcs.deinit();
|
||||
if (comptime tmux_enabled) tmux: {
|
||||
const viewer = self.tmux_viewer orelse break :tmux;
|
||||
viewer.deinit();
|
||||
self.alloc.destroy(viewer);
|
||||
self.tmux_viewer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// This queues a render operation with the renderer thread. The render
|
||||
|
|
@ -368,9 +380,73 @@ pub const StreamHandler = struct {
|
|||
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});
|
||||
.tmux => |tmux| tmux: {
|
||||
// If tmux control mode is disabled at the build level,
|
||||
// then this whole block shouldn't be analyzed.
|
||||
if (comptime !tmux_enabled) break :tmux;
|
||||
log.info("tmux control mode event cmd={f}", .{tmux});
|
||||
|
||||
switch (tmux) {
|
||||
.enter => {
|
||||
// Setup our viewer state
|
||||
assert(self.tmux_viewer == null);
|
||||
const viewer = try self.alloc.create(terminal.tmux.Viewer);
|
||||
errdefer self.alloc.destroy(viewer);
|
||||
viewer.* = try .init(self.alloc);
|
||||
errdefer viewer.deinit();
|
||||
self.tmux_viewer = viewer;
|
||||
break :tmux;
|
||||
},
|
||||
|
||||
.exit => if (self.tmux_viewer) |viewer| {
|
||||
// Free our viewer state
|
||||
viewer.deinit();
|
||||
self.alloc.destroy(viewer);
|
||||
self.tmux_viewer = null;
|
||||
break :tmux;
|
||||
},
|
||||
|
||||
else => {},
|
||||
}
|
||||
|
||||
assert(tmux != .enter);
|
||||
assert(tmux != .exit);
|
||||
|
||||
const viewer = self.tmux_viewer orelse {
|
||||
// This can only really happen if we failed to
|
||||
// initialize the viewer on enter.
|
||||
log.info(
|
||||
"received tmux control mode command without viewer: {f}",
|
||||
.{tmux},
|
||||
);
|
||||
|
||||
break :tmux;
|
||||
};
|
||||
|
||||
for (viewer.next(.{ .tmux = tmux })) |action| {
|
||||
log.info("tmux viewer action={f}", .{action});
|
||||
switch (action) {
|
||||
.exit => {
|
||||
// We ignore this because we will fully exit when
|
||||
// our DCS connection ends. We may want to handle
|
||||
// this in the future to notify our GUI we're
|
||||
// disconnected though.
|
||||
},
|
||||
|
||||
.command => |command| {
|
||||
assert(command.len > 0);
|
||||
assert(command[command.len - 1] == '\n');
|
||||
self.messageWriter(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
command,
|
||||
));
|
||||
},
|
||||
|
||||
.windows => {
|
||||
// TODO
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
.xtgettcap => |*gettcap| {
|
||||
|
|
|
|||
Loading…
Reference in New Issue