terminal: move tmux into folder
parent
5bc78d59fb
commit
6e016ea81e
|
|
@ -213,7 +213,7 @@ pub const Command = union(enum) {
|
||||||
|
|
||||||
/// Tmux control mode
|
/// Tmux control mode
|
||||||
tmux: if (build_options.tmux_control_mode)
|
tmux: if (build_options.tmux_control_mode)
|
||||||
terminal.tmux.Notification
|
terminal.tmux.ControlNotification
|
||||||
else
|
else
|
||||||
void,
|
void,
|
||||||
|
|
||||||
|
|
@ -276,7 +276,7 @@ const State = union(enum) {
|
||||||
|
|
||||||
/// Tmux control mode: https://github.com/tmux/tmux/wiki/Control-Mode
|
/// Tmux control mode: https://github.com/tmux/tmux/wiki/Control-Mode
|
||||||
tmux: if (build_options.tmux_control_mode)
|
tmux: if (build_options.tmux_control_mode)
|
||||||
terminal.tmux.Client
|
terminal.tmux.ControlParser
|
||||||
else
|
else
|
||||||
void,
|
void,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,599 +1,9 @@
|
||||||
//! This file contains the implementation for tmux control mode. See
|
//! Types and functions related to tmux protocols.
|
||||||
//! tmux(1) for more information on control mode. Some basics are documented
|
|
||||||
//! here but this is not meant to be a comprehensive source of protocol
|
|
||||||
//! documentation.
|
|
||||||
|
|
||||||
const std = @import("std");
|
const control = @import("tmux/control.zig");
|
||||||
const assert = @import("../quirks.zig").inlineAssert;
|
pub const ControlParser = control.Parser;
|
||||||
const oni = @import("oniguruma");
|
pub const ControlNotification = control.Notification;
|
||||||
|
|
||||||
const log = std.log.scoped(.terminal_tmux);
|
test {
|
||||||
|
@import("std").testing.refAllDecls(@This());
|
||||||
/// A tmux control mode client. It is expected that the caller establishes
|
|
||||||
/// the connection in some way (i.e. detects the opening DCS sequence). This
|
|
||||||
/// just works on a byte stream.
|
|
||||||
pub const Client = struct {
|
|
||||||
/// Current state of the client.
|
|
||||||
state: State = .idle,
|
|
||||||
|
|
||||||
/// The buffer used to store in-progress notifications, output, etc.
|
|
||||||
buffer: std.Io.Writer.Allocating,
|
|
||||||
|
|
||||||
/// The maximum size in bytes of the buffer. This is used to limit
|
|
||||||
/// memory usage. If the buffer exceeds this size, the client will
|
|
||||||
/// enter a broken state (the control mode session will be forcibly
|
|
||||||
/// exited and future data dropped).
|
|
||||||
max_bytes: usize = 1024 * 1024,
|
|
||||||
|
|
||||||
const State = enum {
|
|
||||||
/// Outside of any active notifications. This should drop any output
|
|
||||||
/// unless it is '%' on the first byte of a line. The buffer will be
|
|
||||||
/// cleared when it sees '%', this is so that the previous notification
|
|
||||||
/// data is valid until we receive/process new data.
|
|
||||||
idle,
|
|
||||||
|
|
||||||
/// We experienced unexpected input and are in a broken state
|
|
||||||
/// so we cannot continue processing. When this state is set,
|
|
||||||
/// the buffer has been deinited and must not be accessed.
|
|
||||||
broken,
|
|
||||||
|
|
||||||
/// Inside an active notification (started with '%').
|
|
||||||
notification,
|
|
||||||
|
|
||||||
/// Inside a begin/end block.
|
|
||||||
block,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn deinit(self: *Client) void {
|
|
||||||
// If we're in a broken state, we already deinited
|
|
||||||
// the buffer, so we don't need to do anything.
|
|
||||||
if (self.state == .broken) return;
|
|
||||||
|
|
||||||
self.buffer.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle a byte of input.
|
|
||||||
pub fn put(self: *Client, byte: u8) !?Notification {
|
|
||||||
// If we're in a broken state, just do nothing.
|
|
||||||
//
|
|
||||||
// We have to do this check here before we check the buffer, because if
|
|
||||||
// we're in a broken state then we'd have already deinited the buffer.
|
|
||||||
if (self.state == .broken) return null;
|
|
||||||
|
|
||||||
if (self.buffer.written().len >= self.max_bytes) {
|
|
||||||
self.broken();
|
|
||||||
return error.OutOfMemory;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (self.state) {
|
|
||||||
// Drop because we're in a broken state.
|
|
||||||
.broken => return null,
|
|
||||||
|
|
||||||
// Waiting for a notification so if the byte is not '%' then
|
|
||||||
// we're in a broken state. Control mode output should always
|
|
||||||
// be wrapped in '%begin/%end' orelse we expect a notification.
|
|
||||||
// Return an exit notification.
|
|
||||||
.idle => if (byte != '%') {
|
|
||||||
self.broken();
|
|
||||||
return .{ .exit = {} };
|
|
||||||
} else {
|
|
||||||
self.buffer.clearRetainingCapacity();
|
|
||||||
self.state = .notification;
|
|
||||||
},
|
|
||||||
|
|
||||||
// If we're in a notification and its not a newline then
|
|
||||||
// we accumulate. If it is a newline then we have a
|
|
||||||
// complete notification we need to parse.
|
|
||||||
.notification => if (byte == '\n') {
|
|
||||||
// We have a complete notification, parse it.
|
|
||||||
return try self.parseNotification();
|
|
||||||
},
|
|
||||||
|
|
||||||
// If we're in a block then we accumulate until we see a newline
|
|
||||||
// and then we check to see if that line ended the block.
|
|
||||||
.block => if (byte == '\n') {
|
|
||||||
const written = self.buffer.written();
|
|
||||||
const idx = if (std.mem.lastIndexOfScalar(
|
|
||||||
u8,
|
|
||||||
written,
|
|
||||||
'\n',
|
|
||||||
)) |v| v + 1 else 0;
|
|
||||||
const line = written[idx..];
|
|
||||||
|
|
||||||
if (std.mem.startsWith(u8, line, "%end") or
|
|
||||||
std.mem.startsWith(u8, line, "%error"))
|
|
||||||
{
|
|
||||||
const err = std.mem.startsWith(u8, line, "%error");
|
|
||||||
const output = std.mem.trimRight(u8, written[0..idx], "\r\n");
|
|
||||||
|
|
||||||
// If it is an error then log it.
|
|
||||||
if (err) log.warn("tmux control mode error={s}", .{output});
|
|
||||||
|
|
||||||
// Important: do not clear buffer since the notification
|
|
||||||
// contains it.
|
|
||||||
self.state = .idle;
|
|
||||||
return if (err) .{ .block_err = output } else .{ .block_end = output };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Didn't end the block, continue accumulating.
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
try self.buffer.writer.writeByte(byte);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseNotification(self: *Client) !?Notification {
|
|
||||||
assert(self.state == .notification);
|
|
||||||
|
|
||||||
const line = line: {
|
|
||||||
var line = self.buffer.written();
|
|
||||||
if (line[line.len - 1] == '\r') line = line[0 .. line.len - 1];
|
|
||||||
break :line line;
|
|
||||||
};
|
|
||||||
const cmd = cmd: {
|
|
||||||
const idx = std.mem.indexOfScalar(u8, line, ' ') orelse line.len;
|
|
||||||
break :cmd line[0..idx];
|
|
||||||
};
|
|
||||||
|
|
||||||
// The notification MUST exist because we guard entering the notification
|
|
||||||
// state on seeing at least a '%'.
|
|
||||||
if (std.mem.eql(u8, cmd, "%begin")) {
|
|
||||||
// We don't use the rest of the tokens for now because tmux
|
|
||||||
// claims to guarantee that begin/end are always in order and
|
|
||||||
// never intermixed. In the future, we should probably validate
|
|
||||||
// this.
|
|
||||||
// TODO(tmuxcc): do this before merge?
|
|
||||||
|
|
||||||
// Move to block state because we expect a corresponding end/error
|
|
||||||
// and want to accumulate the data.
|
|
||||||
self.state = .block;
|
|
||||||
self.buffer.clearRetainingCapacity();
|
|
||||||
return null;
|
|
||||||
} else if (std.mem.eql(u8, cmd, "%output")) cmd: {
|
|
||||||
var re = try oni.Regex.init(
|
|
||||||
"^%output %([0-9]+) (.+)$",
|
|
||||||
.{ .capture_group = true },
|
|
||||||
oni.Encoding.utf8,
|
|
||||||
oni.Syntax.default,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
defer re.deinit();
|
|
||||||
|
|
||||||
var region = re.search(line, .{}) catch |err| {
|
|
||||||
log.warn("failed to match notification cmd={s} line=\"{s}\" err={}", .{ cmd, line, err });
|
|
||||||
break :cmd;
|
|
||||||
};
|
|
||||||
defer region.deinit();
|
|
||||||
const starts = region.starts();
|
|
||||||
const ends = region.ends();
|
|
||||||
|
|
||||||
const id = std.fmt.parseInt(
|
|
||||||
usize,
|
|
||||||
line[@intCast(starts[1])..@intCast(ends[1])],
|
|
||||||
10,
|
|
||||||
) catch unreachable;
|
|
||||||
const data = line[@intCast(starts[2])..@intCast(ends[2])];
|
|
||||||
|
|
||||||
// Important: do not clear buffer here since name points to it
|
|
||||||
self.state = .idle;
|
|
||||||
return .{ .output = .{ .pane_id = id, .data = data } };
|
|
||||||
} else if (std.mem.eql(u8, cmd, "%session-changed")) cmd: {
|
|
||||||
var re = try oni.Regex.init(
|
|
||||||
"^%session-changed \\$([0-9]+) (.+)$",
|
|
||||||
.{ .capture_group = true },
|
|
||||||
oni.Encoding.utf8,
|
|
||||||
oni.Syntax.default,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
defer re.deinit();
|
|
||||||
|
|
||||||
var region = re.search(line, .{}) catch |err| {
|
|
||||||
log.warn("failed to match notification cmd={s} line=\"{s}\" err={}", .{ cmd, line, err });
|
|
||||||
break :cmd;
|
|
||||||
};
|
|
||||||
defer region.deinit();
|
|
||||||
const starts = region.starts();
|
|
||||||
const ends = region.ends();
|
|
||||||
|
|
||||||
const id = std.fmt.parseInt(
|
|
||||||
usize,
|
|
||||||
line[@intCast(starts[1])..@intCast(ends[1])],
|
|
||||||
10,
|
|
||||||
) catch unreachable;
|
|
||||||
const name = line[@intCast(starts[2])..@intCast(ends[2])];
|
|
||||||
|
|
||||||
// Important: do not clear buffer here since name points to it
|
|
||||||
self.state = .idle;
|
|
||||||
return .{ .session_changed = .{ .id = id, .name = name } };
|
|
||||||
} else if (std.mem.eql(u8, cmd, "%sessions-changed")) cmd: {
|
|
||||||
if (!std.mem.eql(u8, line, "%sessions-changed")) {
|
|
||||||
log.warn("failed to match notification cmd={s} line=\"{s}\"", .{ cmd, line });
|
|
||||||
break :cmd;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.buffer.clearRetainingCapacity();
|
|
||||||
self.state = .idle;
|
|
||||||
return .{ .sessions_changed = {} };
|
|
||||||
} else if (std.mem.eql(u8, cmd, "%window-add")) cmd: {
|
|
||||||
var re = try oni.Regex.init(
|
|
||||||
"^%window-add @([0-9]+)$",
|
|
||||||
.{ .capture_group = true },
|
|
||||||
oni.Encoding.utf8,
|
|
||||||
oni.Syntax.default,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
defer re.deinit();
|
|
||||||
|
|
||||||
var region = re.search(line, .{}) catch |err| {
|
|
||||||
log.warn("failed to match notification cmd={s} line=\"{s}\" err={}", .{ cmd, line, err });
|
|
||||||
break :cmd;
|
|
||||||
};
|
|
||||||
defer region.deinit();
|
|
||||||
const starts = region.starts();
|
|
||||||
const ends = region.ends();
|
|
||||||
|
|
||||||
const id = std.fmt.parseInt(
|
|
||||||
usize,
|
|
||||||
line[@intCast(starts[1])..@intCast(ends[1])],
|
|
||||||
10,
|
|
||||||
) catch unreachable;
|
|
||||||
|
|
||||||
self.buffer.clearRetainingCapacity();
|
|
||||||
self.state = .idle;
|
|
||||||
return .{ .window_add = .{ .id = id } };
|
|
||||||
} else if (std.mem.eql(u8, cmd, "%window-renamed")) cmd: {
|
|
||||||
var re = try oni.Regex.init(
|
|
||||||
"^%window-renamed @([0-9]+) (.+)$",
|
|
||||||
.{ .capture_group = true },
|
|
||||||
oni.Encoding.utf8,
|
|
||||||
oni.Syntax.default,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
defer re.deinit();
|
|
||||||
|
|
||||||
var region = re.search(line, .{}) catch |err| {
|
|
||||||
log.warn("failed to match notification cmd={s} line=\"{s}\" err={}", .{ cmd, line, err });
|
|
||||||
break :cmd;
|
|
||||||
};
|
|
||||||
defer region.deinit();
|
|
||||||
const starts = region.starts();
|
|
||||||
const ends = region.ends();
|
|
||||||
|
|
||||||
const id = std.fmt.parseInt(
|
|
||||||
usize,
|
|
||||||
line[@intCast(starts[1])..@intCast(ends[1])],
|
|
||||||
10,
|
|
||||||
) catch unreachable;
|
|
||||||
const name = line[@intCast(starts[2])..@intCast(ends[2])];
|
|
||||||
|
|
||||||
// Important: do not clear buffer here since name points to it
|
|
||||||
self.state = .idle;
|
|
||||||
return .{ .window_renamed = .{ .id = id, .name = name } };
|
|
||||||
} else if (std.mem.eql(u8, cmd, "%window-pane-changed")) cmd: {
|
|
||||||
var re = try oni.Regex.init(
|
|
||||||
"^%window-pane-changed @([0-9]+) %([0-9]+)$",
|
|
||||||
.{ .capture_group = true },
|
|
||||||
oni.Encoding.utf8,
|
|
||||||
oni.Syntax.default,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
defer re.deinit();
|
|
||||||
|
|
||||||
var region = re.search(line, .{}) catch |err| {
|
|
||||||
log.warn("failed to match notification cmd={s} line=\"{s}\" err={}", .{ cmd, line, err });
|
|
||||||
break :cmd;
|
|
||||||
};
|
|
||||||
defer region.deinit();
|
|
||||||
const starts = region.starts();
|
|
||||||
const ends = region.ends();
|
|
||||||
|
|
||||||
const window_id = std.fmt.parseInt(
|
|
||||||
usize,
|
|
||||||
line[@intCast(starts[1])..@intCast(ends[1])],
|
|
||||||
10,
|
|
||||||
) catch unreachable;
|
|
||||||
const pane_id = std.fmt.parseInt(
|
|
||||||
usize,
|
|
||||||
line[@intCast(starts[2])..@intCast(ends[2])],
|
|
||||||
10,
|
|
||||||
) catch unreachable;
|
|
||||||
|
|
||||||
self.buffer.clearRetainingCapacity();
|
|
||||||
self.state = .idle;
|
|
||||||
return .{ .window_pane_changed = .{ .window_id = window_id, .pane_id = pane_id } };
|
|
||||||
} else if (std.mem.eql(u8, cmd, "%client-detached")) cmd: {
|
|
||||||
var re = try oni.Regex.init(
|
|
||||||
"^%client-detached (.+)$",
|
|
||||||
.{ .capture_group = true },
|
|
||||||
oni.Encoding.utf8,
|
|
||||||
oni.Syntax.default,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
defer re.deinit();
|
|
||||||
|
|
||||||
var region = re.search(line, .{}) catch |err| {
|
|
||||||
log.warn("failed to match notification cmd={s} line=\"{s}\" err={}", .{ cmd, line, err });
|
|
||||||
break :cmd;
|
|
||||||
};
|
|
||||||
defer region.deinit();
|
|
||||||
const starts = region.starts();
|
|
||||||
const ends = region.ends();
|
|
||||||
|
|
||||||
const client = line[@intCast(starts[1])..@intCast(ends[1])];
|
|
||||||
|
|
||||||
// Important: do not clear buffer here since client points to it
|
|
||||||
self.state = .idle;
|
|
||||||
return .{ .client_detached = .{ .client = client } };
|
|
||||||
} else if (std.mem.eql(u8, cmd, "%client-session-changed")) cmd: {
|
|
||||||
var re = try oni.Regex.init(
|
|
||||||
"^%client-session-changed (.+) \\$([0-9]+) (.+)$",
|
|
||||||
.{ .capture_group = true },
|
|
||||||
oni.Encoding.utf8,
|
|
||||||
oni.Syntax.default,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
defer re.deinit();
|
|
||||||
|
|
||||||
var region = re.search(line, .{}) catch |err| {
|
|
||||||
log.warn("failed to match notification cmd={s} line=\"{s}\" err={}", .{ cmd, line, err });
|
|
||||||
break :cmd;
|
|
||||||
};
|
|
||||||
defer region.deinit();
|
|
||||||
const starts = region.starts();
|
|
||||||
const ends = region.ends();
|
|
||||||
|
|
||||||
const client = line[@intCast(starts[1])..@intCast(ends[1])];
|
|
||||||
const session_id = std.fmt.parseInt(
|
|
||||||
usize,
|
|
||||||
line[@intCast(starts[2])..@intCast(ends[2])],
|
|
||||||
10,
|
|
||||||
) catch unreachable;
|
|
||||||
const name = line[@intCast(starts[3])..@intCast(ends[3])];
|
|
||||||
|
|
||||||
// Important: do not clear buffer here since client/name point to it
|
|
||||||
self.state = .idle;
|
|
||||||
return .{ .client_session_changed = .{ .client = client, .session_id = session_id, .name = name } };
|
|
||||||
} else {
|
|
||||||
// Unknown notification, log it and return to idle state.
|
|
||||||
log.warn("unknown tmux control mode notification={s}", .{cmd});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unknown command. Clear the buffer and return to idle state.
|
|
||||||
self.buffer.clearRetainingCapacity();
|
|
||||||
self.state = .idle;
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark the tmux state as broken.
|
|
||||||
fn broken(self: *Client) void {
|
|
||||||
self.state = .broken;
|
|
||||||
self.buffer.deinit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Possible notification types from tmux control mode. These are documented
|
|
||||||
/// in tmux(1). A lot of the simple documentation was copied from that man
|
|
||||||
/// page here.
|
|
||||||
pub const Notification = union(enum) {
|
|
||||||
/// Entering tmux control mode. This isn't an actual event sent by
|
|
||||||
/// tmux but is one sent by us to indicate that we have detected that
|
|
||||||
/// tmux control mode is starting.
|
|
||||||
enter,
|
|
||||||
|
|
||||||
/// Exit.
|
|
||||||
///
|
|
||||||
/// NOTE: The tmux protocol contains a "reason" string (human friendly)
|
|
||||||
/// associated with this. We currently drop it because we don't need it
|
|
||||||
/// but this may be something we want to add later. If we do add it,
|
|
||||||
/// we have to consider buffer limits and how we handle those (dropping
|
|
||||||
/// vs truncating, etc.).
|
|
||||||
exit,
|
|
||||||
|
|
||||||
/// Dispatched at the end of a begin/end block with the raw data.
|
|
||||||
/// The control mode parser can't parse the data because it is unaware
|
|
||||||
/// of the command that was sent to trigger this output.
|
|
||||||
block_end: []const u8,
|
|
||||||
block_err: []const u8,
|
|
||||||
|
|
||||||
/// Raw output from a pane.
|
|
||||||
output: struct {
|
|
||||||
pane_id: usize,
|
|
||||||
data: []const u8, // unescaped
|
|
||||||
},
|
|
||||||
|
|
||||||
/// The client is now attached to the session with ID session-id, which is
|
|
||||||
/// named name.
|
|
||||||
session_changed: struct {
|
|
||||||
id: usize,
|
|
||||||
name: []const u8,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// A session was created or destroyed.
|
|
||||||
sessions_changed,
|
|
||||||
|
|
||||||
/// The window with ID window-id was linked to the current session.
|
|
||||||
window_add: struct {
|
|
||||||
id: usize,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// The window with ID window-id was renamed to name.
|
|
||||||
window_renamed: struct {
|
|
||||||
id: usize,
|
|
||||||
name: []const u8,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// The active pane in the window with ID window-id changed to the pane
|
|
||||||
/// with ID pane-id.
|
|
||||||
window_pane_changed: struct {
|
|
||||||
window_id: usize,
|
|
||||||
pane_id: usize,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// The client has detached.
|
|
||||||
client_detached: struct {
|
|
||||||
client: []const u8,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// The client is now attached to the session with ID session-id, which is
|
|
||||||
/// named name.
|
|
||||||
client_session_changed: struct {
|
|
||||||
client: []const u8,
|
|
||||||
session_id: usize,
|
|
||||||
name: []const u8,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
test "tmux begin/end empty" {
|
|
||||||
const testing = std.testing;
|
|
||||||
const alloc = testing.allocator;
|
|
||||||
|
|
||||||
var c: Client = .{ .buffer = .init(alloc) };
|
|
||||||
defer c.deinit();
|
|
||||||
for ("%begin 1578922740 269 1\n") |byte| try testing.expect(try c.put(byte) == null);
|
|
||||||
for ("%end 1578922740 269 1") |byte| try testing.expect(try c.put(byte) == null);
|
|
||||||
const n = (try c.put('\n')).?;
|
|
||||||
try testing.expect(n == .block_end);
|
|
||||||
try testing.expectEqualStrings("", n.block_end);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "tmux begin/error empty" {
|
|
||||||
const testing = std.testing;
|
|
||||||
const alloc = testing.allocator;
|
|
||||||
|
|
||||||
var c: Client = .{ .buffer = .init(alloc) };
|
|
||||||
defer c.deinit();
|
|
||||||
for ("%begin 1578922740 269 1\n") |byte| try testing.expect(try c.put(byte) == null);
|
|
||||||
for ("%error 1578922740 269 1") |byte| try testing.expect(try c.put(byte) == null);
|
|
||||||
const n = (try c.put('\n')).?;
|
|
||||||
try testing.expect(n == .block_err);
|
|
||||||
try testing.expectEqualStrings("", n.block_err);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "tmux begin/end data" {
|
|
||||||
const testing = std.testing;
|
|
||||||
const alloc = testing.allocator;
|
|
||||||
|
|
||||||
var c: Client = .{ .buffer = .init(alloc) };
|
|
||||||
defer c.deinit();
|
|
||||||
for ("%begin 1578922740 269 1\n") |byte| try testing.expect(try c.put(byte) == null);
|
|
||||||
for ("hello\nworld\n") |byte| try testing.expect(try c.put(byte) == null);
|
|
||||||
for ("%end 1578922740 269 1") |byte| try testing.expect(try c.put(byte) == null);
|
|
||||||
const n = (try c.put('\n')).?;
|
|
||||||
try testing.expect(n == .block_end);
|
|
||||||
try testing.expectEqualStrings("hello\nworld", n.block_end);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "tmux output" {
|
|
||||||
const testing = std.testing;
|
|
||||||
const alloc = testing.allocator;
|
|
||||||
|
|
||||||
var c: Client = .{ .buffer = .init(alloc) };
|
|
||||||
defer c.deinit();
|
|
||||||
for ("%output %42 foo bar baz") |byte| try testing.expect(try c.put(byte) == null);
|
|
||||||
const n = (try c.put('\n')).?;
|
|
||||||
try testing.expect(n == .output);
|
|
||||||
try testing.expectEqual(42, n.output.pane_id);
|
|
||||||
try testing.expectEqualStrings("foo bar baz", n.output.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "tmux session-changed" {
|
|
||||||
const testing = std.testing;
|
|
||||||
const alloc = testing.allocator;
|
|
||||||
|
|
||||||
var c: Client = .{ .buffer = .init(alloc) };
|
|
||||||
defer c.deinit();
|
|
||||||
for ("%session-changed $42 foo") |byte| try testing.expect(try c.put(byte) == null);
|
|
||||||
const n = (try c.put('\n')).?;
|
|
||||||
try testing.expect(n == .session_changed);
|
|
||||||
try testing.expectEqual(42, n.session_changed.id);
|
|
||||||
try testing.expectEqualStrings("foo", n.session_changed.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "tmux sessions-changed" {
|
|
||||||
const testing = std.testing;
|
|
||||||
const alloc = testing.allocator;
|
|
||||||
|
|
||||||
var c: Client = .{ .buffer = .init(alloc) };
|
|
||||||
defer c.deinit();
|
|
||||||
for ("%sessions-changed") |byte| try testing.expect(try c.put(byte) == null);
|
|
||||||
const n = (try c.put('\n')).?;
|
|
||||||
try testing.expect(n == .sessions_changed);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "tmux sessions-changed carriage return" {
|
|
||||||
const testing = std.testing;
|
|
||||||
const alloc = testing.allocator;
|
|
||||||
|
|
||||||
var c: Client = .{ .buffer = .init(alloc) };
|
|
||||||
defer c.deinit();
|
|
||||||
for ("%sessions-changed\r") |byte| try testing.expect(try c.put(byte) == null);
|
|
||||||
const n = (try c.put('\n')).?;
|
|
||||||
try testing.expect(n == .sessions_changed);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "tmux window-add" {
|
|
||||||
const testing = std.testing;
|
|
||||||
const alloc = testing.allocator;
|
|
||||||
|
|
||||||
var c: Client = .{ .buffer = .init(alloc) };
|
|
||||||
defer c.deinit();
|
|
||||||
for ("%window-add @14") |byte| try testing.expect(try c.put(byte) == null);
|
|
||||||
const n = (try c.put('\n')).?;
|
|
||||||
try testing.expect(n == .window_add);
|
|
||||||
try testing.expectEqual(14, n.window_add.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "tmux window-renamed" {
|
|
||||||
const testing = std.testing;
|
|
||||||
const alloc = testing.allocator;
|
|
||||||
|
|
||||||
var c: Client = .{ .buffer = .init(alloc) };
|
|
||||||
defer c.deinit();
|
|
||||||
for ("%window-renamed @42 bar") |byte| try testing.expect(try c.put(byte) == null);
|
|
||||||
const n = (try c.put('\n')).?;
|
|
||||||
try testing.expect(n == .window_renamed);
|
|
||||||
try testing.expectEqual(42, n.window_renamed.id);
|
|
||||||
try testing.expectEqualStrings("bar", n.window_renamed.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "tmux window-pane-changed" {
|
|
||||||
const testing = std.testing;
|
|
||||||
const alloc = testing.allocator;
|
|
||||||
|
|
||||||
var c: Client = .{ .buffer = .init(alloc) };
|
|
||||||
defer c.deinit();
|
|
||||||
for ("%window-pane-changed @42 %2") |byte| try testing.expect(try c.put(byte) == null);
|
|
||||||
const n = (try c.put('\n')).?;
|
|
||||||
try testing.expect(n == .window_pane_changed);
|
|
||||||
try testing.expectEqual(42, n.window_pane_changed.window_id);
|
|
||||||
try testing.expectEqual(2, n.window_pane_changed.pane_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "tmux client-detached" {
|
|
||||||
const testing = std.testing;
|
|
||||||
const alloc = testing.allocator;
|
|
||||||
|
|
||||||
var c: Client = .{ .buffer = .init(alloc) };
|
|
||||||
defer c.deinit();
|
|
||||||
for ("%client-detached /dev/pts/1") |byte| try testing.expect(try c.put(byte) == null);
|
|
||||||
const n = (try c.put('\n')).?;
|
|
||||||
try testing.expect(n == .client_detached);
|
|
||||||
try testing.expectEqualStrings("/dev/pts/1", n.client_detached.client);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "tmux client-session-changed" {
|
|
||||||
const testing = std.testing;
|
|
||||||
const alloc = testing.allocator;
|
|
||||||
|
|
||||||
var c: Client = .{ .buffer = .init(alloc) };
|
|
||||||
defer c.deinit();
|
|
||||||
for ("%client-session-changed /dev/pts/1 $2 mysession") |byte| try testing.expect(try c.put(byte) == null);
|
|
||||||
const n = (try c.put('\n')).?;
|
|
||||||
try testing.expect(n == .client_session_changed);
|
|
||||||
try testing.expectEqualStrings("/dev/pts/1", n.client_session_changed.client);
|
|
||||||
try testing.expectEqual(2, n.client_session_changed.session_id);
|
|
||||||
try testing.expectEqualStrings("mysession", n.client_session_changed.name);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,602 @@
|
||||||
|
//! This file contains the implementation for tmux control mode. See
|
||||||
|
//! tmux(1) for more information on control mode. Some basics are documented
|
||||||
|
//! here but this is not meant to be a comprehensive source of protocol
|
||||||
|
//! documentation.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = @import("../../quirks.zig").inlineAssert;
|
||||||
|
const oni = @import("oniguruma");
|
||||||
|
|
||||||
|
const log = std.log.scoped(.terminal_tmux);
|
||||||
|
|
||||||
|
/// A tmux control mode parser. This takes in output from tmux control
|
||||||
|
/// mode and parses it into a structured notifications.
|
||||||
|
///
|
||||||
|
/// It is up to the caller to establish the connection to the tmux
|
||||||
|
/// control mode session in some way (e.g. via exec, a network socket,
|
||||||
|
/// whatever). This is fully agnostic to how the data is received and sent.
|
||||||
|
pub const Parser = struct {
|
||||||
|
/// Current state of the client.
|
||||||
|
state: State = .idle,
|
||||||
|
|
||||||
|
/// The buffer used to store in-progress notifications, output, etc.
|
||||||
|
buffer: std.Io.Writer.Allocating,
|
||||||
|
|
||||||
|
/// The maximum size in bytes of the buffer. This is used to limit
|
||||||
|
/// memory usage. If the buffer exceeds this size, the client will
|
||||||
|
/// enter a broken state (the control mode session will be forcibly
|
||||||
|
/// exited and future data dropped).
|
||||||
|
max_bytes: usize = 1024 * 1024,
|
||||||
|
|
||||||
|
const State = enum {
|
||||||
|
/// Outside of any active notifications. This should drop any output
|
||||||
|
/// unless it is '%' on the first byte of a line. The buffer will be
|
||||||
|
/// cleared when it sees '%', this is so that the previous notification
|
||||||
|
/// data is valid until we receive/process new data.
|
||||||
|
idle,
|
||||||
|
|
||||||
|
/// We experienced unexpected input and are in a broken state
|
||||||
|
/// so we cannot continue processing. When this state is set,
|
||||||
|
/// the buffer has been deinited and must not be accessed.
|
||||||
|
broken,
|
||||||
|
|
||||||
|
/// Inside an active notification (started with '%').
|
||||||
|
notification,
|
||||||
|
|
||||||
|
/// Inside a begin/end block.
|
||||||
|
block,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn deinit(self: *Parser) void {
|
||||||
|
// If we're in a broken state, we already deinited
|
||||||
|
// the buffer, so we don't need to do anything.
|
||||||
|
if (self.state == .broken) return;
|
||||||
|
|
||||||
|
self.buffer.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle a byte of input.
|
||||||
|
pub fn put(self: *Parser, byte: u8) !?Notification {
|
||||||
|
// If we're in a broken state, just do nothing.
|
||||||
|
//
|
||||||
|
// We have to do this check here before we check the buffer, because if
|
||||||
|
// we're in a broken state then we'd have already deinited the buffer.
|
||||||
|
if (self.state == .broken) return null;
|
||||||
|
|
||||||
|
if (self.buffer.written().len >= self.max_bytes) {
|
||||||
|
self.broken();
|
||||||
|
return error.OutOfMemory;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (self.state) {
|
||||||
|
// Drop because we're in a broken state.
|
||||||
|
.broken => return null,
|
||||||
|
|
||||||
|
// Waiting for a notification so if the byte is not '%' then
|
||||||
|
// we're in a broken state. Control mode output should always
|
||||||
|
// be wrapped in '%begin/%end' orelse we expect a notification.
|
||||||
|
// Return an exit notification.
|
||||||
|
.idle => if (byte != '%') {
|
||||||
|
self.broken();
|
||||||
|
return .{ .exit = {} };
|
||||||
|
} else {
|
||||||
|
self.buffer.clearRetainingCapacity();
|
||||||
|
self.state = .notification;
|
||||||
|
},
|
||||||
|
|
||||||
|
// If we're in a notification and its not a newline then
|
||||||
|
// we accumulate. If it is a newline then we have a
|
||||||
|
// complete notification we need to parse.
|
||||||
|
.notification => if (byte == '\n') {
|
||||||
|
// We have a complete notification, parse it.
|
||||||
|
return try self.parseNotification();
|
||||||
|
},
|
||||||
|
|
||||||
|
// If we're in a block then we accumulate until we see a newline
|
||||||
|
// and then we check to see if that line ended the block.
|
||||||
|
.block => if (byte == '\n') {
|
||||||
|
const written = self.buffer.written();
|
||||||
|
const idx = if (std.mem.lastIndexOfScalar(
|
||||||
|
u8,
|
||||||
|
written,
|
||||||
|
'\n',
|
||||||
|
)) |v| v + 1 else 0;
|
||||||
|
const line = written[idx..];
|
||||||
|
|
||||||
|
if (std.mem.startsWith(u8, line, "%end") or
|
||||||
|
std.mem.startsWith(u8, line, "%error"))
|
||||||
|
{
|
||||||
|
const err = std.mem.startsWith(u8, line, "%error");
|
||||||
|
const output = std.mem.trimRight(u8, written[0..idx], "\r\n");
|
||||||
|
|
||||||
|
// If it is an error then log it.
|
||||||
|
if (err) log.warn("tmux control mode error={s}", .{output});
|
||||||
|
|
||||||
|
// Important: do not clear buffer since the notification
|
||||||
|
// contains it.
|
||||||
|
self.state = .idle;
|
||||||
|
return if (err) .{ .block_err = output } else .{ .block_end = output };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Didn't end the block, continue accumulating.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.buffer.writer.writeByte(byte);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseNotification(self: *Parser) !?Notification {
|
||||||
|
assert(self.state == .notification);
|
||||||
|
|
||||||
|
const line = line: {
|
||||||
|
var line = self.buffer.written();
|
||||||
|
if (line[line.len - 1] == '\r') line = line[0 .. line.len - 1];
|
||||||
|
break :line line;
|
||||||
|
};
|
||||||
|
const cmd = cmd: {
|
||||||
|
const idx = std.mem.indexOfScalar(u8, line, ' ') orelse line.len;
|
||||||
|
break :cmd line[0..idx];
|
||||||
|
};
|
||||||
|
|
||||||
|
// The notification MUST exist because we guard entering the notification
|
||||||
|
// state on seeing at least a '%'.
|
||||||
|
if (std.mem.eql(u8, cmd, "%begin")) {
|
||||||
|
// We don't use the rest of the tokens for now because tmux
|
||||||
|
// claims to guarantee that begin/end are always in order and
|
||||||
|
// never intermixed. In the future, we should probably validate
|
||||||
|
// this.
|
||||||
|
// TODO(tmuxcc): do this before merge?
|
||||||
|
|
||||||
|
// Move to block state because we expect a corresponding end/error
|
||||||
|
// and want to accumulate the data.
|
||||||
|
self.state = .block;
|
||||||
|
self.buffer.clearRetainingCapacity();
|
||||||
|
return null;
|
||||||
|
} else if (std.mem.eql(u8, cmd, "%output")) cmd: {
|
||||||
|
var re = try oni.Regex.init(
|
||||||
|
"^%output %([0-9]+) (.+)$",
|
||||||
|
.{ .capture_group = true },
|
||||||
|
oni.Encoding.utf8,
|
||||||
|
oni.Syntax.default,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
defer re.deinit();
|
||||||
|
|
||||||
|
var region = re.search(line, .{}) catch |err| {
|
||||||
|
log.warn("failed to match notification cmd={s} line=\"{s}\" err={}", .{ cmd, line, err });
|
||||||
|
break :cmd;
|
||||||
|
};
|
||||||
|
defer region.deinit();
|
||||||
|
const starts = region.starts();
|
||||||
|
const ends = region.ends();
|
||||||
|
|
||||||
|
const id = std.fmt.parseInt(
|
||||||
|
usize,
|
||||||
|
line[@intCast(starts[1])..@intCast(ends[1])],
|
||||||
|
10,
|
||||||
|
) catch unreachable;
|
||||||
|
const data = line[@intCast(starts[2])..@intCast(ends[2])];
|
||||||
|
|
||||||
|
// Important: do not clear buffer here since name points to it
|
||||||
|
self.state = .idle;
|
||||||
|
return .{ .output = .{ .pane_id = id, .data = data } };
|
||||||
|
} else if (std.mem.eql(u8, cmd, "%session-changed")) cmd: {
|
||||||
|
var re = try oni.Regex.init(
|
||||||
|
"^%session-changed \\$([0-9]+) (.+)$",
|
||||||
|
.{ .capture_group = true },
|
||||||
|
oni.Encoding.utf8,
|
||||||
|
oni.Syntax.default,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
defer re.deinit();
|
||||||
|
|
||||||
|
var region = re.search(line, .{}) catch |err| {
|
||||||
|
log.warn("failed to match notification cmd={s} line=\"{s}\" err={}", .{ cmd, line, err });
|
||||||
|
break :cmd;
|
||||||
|
};
|
||||||
|
defer region.deinit();
|
||||||
|
const starts = region.starts();
|
||||||
|
const ends = region.ends();
|
||||||
|
|
||||||
|
const id = std.fmt.parseInt(
|
||||||
|
usize,
|
||||||
|
line[@intCast(starts[1])..@intCast(ends[1])],
|
||||||
|
10,
|
||||||
|
) catch unreachable;
|
||||||
|
const name = line[@intCast(starts[2])..@intCast(ends[2])];
|
||||||
|
|
||||||
|
// Important: do not clear buffer here since name points to it
|
||||||
|
self.state = .idle;
|
||||||
|
return .{ .session_changed = .{ .id = id, .name = name } };
|
||||||
|
} else if (std.mem.eql(u8, cmd, "%sessions-changed")) cmd: {
|
||||||
|
if (!std.mem.eql(u8, line, "%sessions-changed")) {
|
||||||
|
log.warn("failed to match notification cmd={s} line=\"{s}\"", .{ cmd, line });
|
||||||
|
break :cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.buffer.clearRetainingCapacity();
|
||||||
|
self.state = .idle;
|
||||||
|
return .{ .sessions_changed = {} };
|
||||||
|
} else if (std.mem.eql(u8, cmd, "%window-add")) cmd: {
|
||||||
|
var re = try oni.Regex.init(
|
||||||
|
"^%window-add @([0-9]+)$",
|
||||||
|
.{ .capture_group = true },
|
||||||
|
oni.Encoding.utf8,
|
||||||
|
oni.Syntax.default,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
defer re.deinit();
|
||||||
|
|
||||||
|
var region = re.search(line, .{}) catch |err| {
|
||||||
|
log.warn("failed to match notification cmd={s} line=\"{s}\" err={}", .{ cmd, line, err });
|
||||||
|
break :cmd;
|
||||||
|
};
|
||||||
|
defer region.deinit();
|
||||||
|
const starts = region.starts();
|
||||||
|
const ends = region.ends();
|
||||||
|
|
||||||
|
const id = std.fmt.parseInt(
|
||||||
|
usize,
|
||||||
|
line[@intCast(starts[1])..@intCast(ends[1])],
|
||||||
|
10,
|
||||||
|
) catch unreachable;
|
||||||
|
|
||||||
|
self.buffer.clearRetainingCapacity();
|
||||||
|
self.state = .idle;
|
||||||
|
return .{ .window_add = .{ .id = id } };
|
||||||
|
} else if (std.mem.eql(u8, cmd, "%window-renamed")) cmd: {
|
||||||
|
var re = try oni.Regex.init(
|
||||||
|
"^%window-renamed @([0-9]+) (.+)$",
|
||||||
|
.{ .capture_group = true },
|
||||||
|
oni.Encoding.utf8,
|
||||||
|
oni.Syntax.default,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
defer re.deinit();
|
||||||
|
|
||||||
|
var region = re.search(line, .{}) catch |err| {
|
||||||
|
log.warn("failed to match notification cmd={s} line=\"{s}\" err={}", .{ cmd, line, err });
|
||||||
|
break :cmd;
|
||||||
|
};
|
||||||
|
defer region.deinit();
|
||||||
|
const starts = region.starts();
|
||||||
|
const ends = region.ends();
|
||||||
|
|
||||||
|
const id = std.fmt.parseInt(
|
||||||
|
usize,
|
||||||
|
line[@intCast(starts[1])..@intCast(ends[1])],
|
||||||
|
10,
|
||||||
|
) catch unreachable;
|
||||||
|
const name = line[@intCast(starts[2])..@intCast(ends[2])];
|
||||||
|
|
||||||
|
// Important: do not clear buffer here since name points to it
|
||||||
|
self.state = .idle;
|
||||||
|
return .{ .window_renamed = .{ .id = id, .name = name } };
|
||||||
|
} else if (std.mem.eql(u8, cmd, "%window-pane-changed")) cmd: {
|
||||||
|
var re = try oni.Regex.init(
|
||||||
|
"^%window-pane-changed @([0-9]+) %([0-9]+)$",
|
||||||
|
.{ .capture_group = true },
|
||||||
|
oni.Encoding.utf8,
|
||||||
|
oni.Syntax.default,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
defer re.deinit();
|
||||||
|
|
||||||
|
var region = re.search(line, .{}) catch |err| {
|
||||||
|
log.warn("failed to match notification cmd={s} line=\"{s}\" err={}", .{ cmd, line, err });
|
||||||
|
break :cmd;
|
||||||
|
};
|
||||||
|
defer region.deinit();
|
||||||
|
const starts = region.starts();
|
||||||
|
const ends = region.ends();
|
||||||
|
|
||||||
|
const window_id = std.fmt.parseInt(
|
||||||
|
usize,
|
||||||
|
line[@intCast(starts[1])..@intCast(ends[1])],
|
||||||
|
10,
|
||||||
|
) catch unreachable;
|
||||||
|
const pane_id = std.fmt.parseInt(
|
||||||
|
usize,
|
||||||
|
line[@intCast(starts[2])..@intCast(ends[2])],
|
||||||
|
10,
|
||||||
|
) catch unreachable;
|
||||||
|
|
||||||
|
self.buffer.clearRetainingCapacity();
|
||||||
|
self.state = .idle;
|
||||||
|
return .{ .window_pane_changed = .{ .window_id = window_id, .pane_id = pane_id } };
|
||||||
|
} else if (std.mem.eql(u8, cmd, "%client-detached")) cmd: {
|
||||||
|
var re = try oni.Regex.init(
|
||||||
|
"^%client-detached (.+)$",
|
||||||
|
.{ .capture_group = true },
|
||||||
|
oni.Encoding.utf8,
|
||||||
|
oni.Syntax.default,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
defer re.deinit();
|
||||||
|
|
||||||
|
var region = re.search(line, .{}) catch |err| {
|
||||||
|
log.warn("failed to match notification cmd={s} line=\"{s}\" err={}", .{ cmd, line, err });
|
||||||
|
break :cmd;
|
||||||
|
};
|
||||||
|
defer region.deinit();
|
||||||
|
const starts = region.starts();
|
||||||
|
const ends = region.ends();
|
||||||
|
|
||||||
|
const client = line[@intCast(starts[1])..@intCast(ends[1])];
|
||||||
|
|
||||||
|
// Important: do not clear buffer here since client points to it
|
||||||
|
self.state = .idle;
|
||||||
|
return .{ .client_detached = .{ .client = client } };
|
||||||
|
} else if (std.mem.eql(u8, cmd, "%client-session-changed")) cmd: {
|
||||||
|
var re = try oni.Regex.init(
|
||||||
|
"^%client-session-changed (.+) \\$([0-9]+) (.+)$",
|
||||||
|
.{ .capture_group = true },
|
||||||
|
oni.Encoding.utf8,
|
||||||
|
oni.Syntax.default,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
defer re.deinit();
|
||||||
|
|
||||||
|
var region = re.search(line, .{}) catch |err| {
|
||||||
|
log.warn("failed to match notification cmd={s} line=\"{s}\" err={}", .{ cmd, line, err });
|
||||||
|
break :cmd;
|
||||||
|
};
|
||||||
|
defer region.deinit();
|
||||||
|
const starts = region.starts();
|
||||||
|
const ends = region.ends();
|
||||||
|
|
||||||
|
const client = line[@intCast(starts[1])..@intCast(ends[1])];
|
||||||
|
const session_id = std.fmt.parseInt(
|
||||||
|
usize,
|
||||||
|
line[@intCast(starts[2])..@intCast(ends[2])],
|
||||||
|
10,
|
||||||
|
) catch unreachable;
|
||||||
|
const name = line[@intCast(starts[3])..@intCast(ends[3])];
|
||||||
|
|
||||||
|
// Important: do not clear buffer here since client/name point to it
|
||||||
|
self.state = .idle;
|
||||||
|
return .{ .client_session_changed = .{ .client = client, .session_id = session_id, .name = name } };
|
||||||
|
} else {
|
||||||
|
// Unknown notification, log it and return to idle state.
|
||||||
|
log.warn("unknown tmux control mode notification={s}", .{cmd});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown command. Clear the buffer and return to idle state.
|
||||||
|
self.buffer.clearRetainingCapacity();
|
||||||
|
self.state = .idle;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the tmux state as broken.
|
||||||
|
fn broken(self: *Parser) void {
|
||||||
|
self.state = .broken;
|
||||||
|
self.buffer.deinit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Possible notification types from tmux control mode. These are documented
|
||||||
|
/// in tmux(1). A lot of the simple documentation was copied from that man
|
||||||
|
/// page here.
|
||||||
|
pub const Notification = union(enum) {
|
||||||
|
/// Entering tmux control mode. This isn't an actual event sent by
|
||||||
|
/// tmux but is one sent by us to indicate that we have detected that
|
||||||
|
/// tmux control mode is starting.
|
||||||
|
enter,
|
||||||
|
|
||||||
|
/// Exit.
|
||||||
|
///
|
||||||
|
/// NOTE: The tmux protocol contains a "reason" string (human friendly)
|
||||||
|
/// associated with this. We currently drop it because we don't need it
|
||||||
|
/// but this may be something we want to add later. If we do add it,
|
||||||
|
/// we have to consider buffer limits and how we handle those (dropping
|
||||||
|
/// vs truncating, etc.).
|
||||||
|
exit,
|
||||||
|
|
||||||
|
/// Dispatched at the end of a begin/end block with the raw data.
|
||||||
|
/// The control mode parser can't parse the data because it is unaware
|
||||||
|
/// of the command that was sent to trigger this output.
|
||||||
|
block_end: []const u8,
|
||||||
|
block_err: []const u8,
|
||||||
|
|
||||||
|
/// Raw output from a pane.
|
||||||
|
output: struct {
|
||||||
|
pane_id: usize,
|
||||||
|
data: []const u8, // unescaped
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The client is now attached to the session with ID session-id, which is
|
||||||
|
/// named name.
|
||||||
|
session_changed: struct {
|
||||||
|
id: usize,
|
||||||
|
name: []const u8,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A session was created or destroyed.
|
||||||
|
sessions_changed,
|
||||||
|
|
||||||
|
/// The window with ID window-id was linked to the current session.
|
||||||
|
window_add: struct {
|
||||||
|
id: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The window with ID window-id was renamed to name.
|
||||||
|
window_renamed: struct {
|
||||||
|
id: usize,
|
||||||
|
name: []const u8,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The active pane in the window with ID window-id changed to the pane
|
||||||
|
/// with ID pane-id.
|
||||||
|
window_pane_changed: struct {
|
||||||
|
window_id: usize,
|
||||||
|
pane_id: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The client has detached.
|
||||||
|
client_detached: struct {
|
||||||
|
client: []const u8,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The client is now attached to the session with ID session-id, which is
|
||||||
|
/// named name.
|
||||||
|
client_session_changed: struct {
|
||||||
|
client: []const u8,
|
||||||
|
session_id: usize,
|
||||||
|
name: []const u8,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
test "tmux begin/end empty" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var c: Parser = .{ .buffer = .init(alloc) };
|
||||||
|
defer c.deinit();
|
||||||
|
for ("%begin 1578922740 269 1\n") |byte| try testing.expect(try c.put(byte) == null);
|
||||||
|
for ("%end 1578922740 269 1") |byte| try testing.expect(try c.put(byte) == null);
|
||||||
|
const n = (try c.put('\n')).?;
|
||||||
|
try testing.expect(n == .block_end);
|
||||||
|
try testing.expectEqualStrings("", n.block_end);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tmux begin/error empty" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var c: Parser = .{ .buffer = .init(alloc) };
|
||||||
|
defer c.deinit();
|
||||||
|
for ("%begin 1578922740 269 1\n") |byte| try testing.expect(try c.put(byte) == null);
|
||||||
|
for ("%error 1578922740 269 1") |byte| try testing.expect(try c.put(byte) == null);
|
||||||
|
const n = (try c.put('\n')).?;
|
||||||
|
try testing.expect(n == .block_err);
|
||||||
|
try testing.expectEqualStrings("", n.block_err);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tmux begin/end data" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var c: Parser = .{ .buffer = .init(alloc) };
|
||||||
|
defer c.deinit();
|
||||||
|
for ("%begin 1578922740 269 1\n") |byte| try testing.expect(try c.put(byte) == null);
|
||||||
|
for ("hello\nworld\n") |byte| try testing.expect(try c.put(byte) == null);
|
||||||
|
for ("%end 1578922740 269 1") |byte| try testing.expect(try c.put(byte) == null);
|
||||||
|
const n = (try c.put('\n')).?;
|
||||||
|
try testing.expect(n == .block_end);
|
||||||
|
try testing.expectEqualStrings("hello\nworld", n.block_end);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tmux output" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var c: Parser = .{ .buffer = .init(alloc) };
|
||||||
|
defer c.deinit();
|
||||||
|
for ("%output %42 foo bar baz") |byte| try testing.expect(try c.put(byte) == null);
|
||||||
|
const n = (try c.put('\n')).?;
|
||||||
|
try testing.expect(n == .output);
|
||||||
|
try testing.expectEqual(42, n.output.pane_id);
|
||||||
|
try testing.expectEqualStrings("foo bar baz", n.output.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tmux session-changed" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var c: Parser = .{ .buffer = .init(alloc) };
|
||||||
|
defer c.deinit();
|
||||||
|
for ("%session-changed $42 foo") |byte| try testing.expect(try c.put(byte) == null);
|
||||||
|
const n = (try c.put('\n')).?;
|
||||||
|
try testing.expect(n == .session_changed);
|
||||||
|
try testing.expectEqual(42, n.session_changed.id);
|
||||||
|
try testing.expectEqualStrings("foo", n.session_changed.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tmux sessions-changed" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var c: Parser = .{ .buffer = .init(alloc) };
|
||||||
|
defer c.deinit();
|
||||||
|
for ("%sessions-changed") |byte| try testing.expect(try c.put(byte) == null);
|
||||||
|
const n = (try c.put('\n')).?;
|
||||||
|
try testing.expect(n == .sessions_changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tmux sessions-changed carriage return" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var c: Parser = .{ .buffer = .init(alloc) };
|
||||||
|
defer c.deinit();
|
||||||
|
for ("%sessions-changed\r") |byte| try testing.expect(try c.put(byte) == null);
|
||||||
|
const n = (try c.put('\n')).?;
|
||||||
|
try testing.expect(n == .sessions_changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tmux window-add" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var c: Parser = .{ .buffer = .init(alloc) };
|
||||||
|
defer c.deinit();
|
||||||
|
for ("%window-add @14") |byte| try testing.expect(try c.put(byte) == null);
|
||||||
|
const n = (try c.put('\n')).?;
|
||||||
|
try testing.expect(n == .window_add);
|
||||||
|
try testing.expectEqual(14, n.window_add.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tmux window-renamed" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var c: Parser = .{ .buffer = .init(alloc) };
|
||||||
|
defer c.deinit();
|
||||||
|
for ("%window-renamed @42 bar") |byte| try testing.expect(try c.put(byte) == null);
|
||||||
|
const n = (try c.put('\n')).?;
|
||||||
|
try testing.expect(n == .window_renamed);
|
||||||
|
try testing.expectEqual(42, n.window_renamed.id);
|
||||||
|
try testing.expectEqualStrings("bar", n.window_renamed.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tmux window-pane-changed" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var c: Parser = .{ .buffer = .init(alloc) };
|
||||||
|
defer c.deinit();
|
||||||
|
for ("%window-pane-changed @42 %2") |byte| try testing.expect(try c.put(byte) == null);
|
||||||
|
const n = (try c.put('\n')).?;
|
||||||
|
try testing.expect(n == .window_pane_changed);
|
||||||
|
try testing.expectEqual(42, n.window_pane_changed.window_id);
|
||||||
|
try testing.expectEqual(2, n.window_pane_changed.pane_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tmux client-detached" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var c: Parser = .{ .buffer = .init(alloc) };
|
||||||
|
defer c.deinit();
|
||||||
|
for ("%client-detached /dev/pts/1") |byte| try testing.expect(try c.put(byte) == null);
|
||||||
|
const n = (try c.put('\n')).?;
|
||||||
|
try testing.expect(n == .client_detached);
|
||||||
|
try testing.expectEqualStrings("/dev/pts/1", n.client_detached.client);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tmux client-session-changed" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var c: Parser = .{ .buffer = .init(alloc) };
|
||||||
|
defer c.deinit();
|
||||||
|
for ("%client-session-changed /dev/pts/1 $2 mysession") |byte| try testing.expect(try c.put(byte) == null);
|
||||||
|
const n = (try c.put('\n')).?;
|
||||||
|
try testing.expect(n == .client_session_changed);
|
||||||
|
try testing.expectEqualStrings("/dev/pts/1", n.client_session_changed.client);
|
||||||
|
try testing.expectEqual(2, n.client_session_changed.session_id);
|
||||||
|
try testing.expectEqualStrings("mysession", n.client_session_changed.name);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue