terminal/tmux: add more control mode parsing keys
parent
6502922bb6
commit
5bc78d59fb
|
|
@ -26,7 +26,7 @@ pub const Handler = struct {
|
||||||
assert(self.state == .inactive);
|
assert(self.state == .inactive);
|
||||||
|
|
||||||
// Initialize our state to ignore in case of error
|
// Initialize our state to ignore in case of error
|
||||||
self.state = .{ .ignore = {} };
|
self.state = .ignore;
|
||||||
|
|
||||||
// Try to parse the hook.
|
// Try to parse the hook.
|
||||||
const hk_ = self.tryHook(alloc, dcs) catch |err| {
|
const hk_ = self.tryHook(alloc, dcs) catch |err| {
|
||||||
|
|
@ -70,7 +70,7 @@ pub const Handler = struct {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
.command = .{ .tmux = .{ .enter = {} } },
|
.command = .{ .tmux = .enter },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -116,7 +116,7 @@ pub const Handler = struct {
|
||||||
// On error we just discard our state and ignore the rest
|
// On error we just discard our state and ignore the rest
|
||||||
log.info("error putting byte into DCS handler err={}", .{err});
|
log.info("error putting byte into DCS handler err={}", .{err});
|
||||||
self.discard();
|
self.discard();
|
||||||
self.state = .{ .ignore = {} };
|
self.state = .ignore;
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -158,7 +158,7 @@ pub const Handler = struct {
|
||||||
// Note: we do NOT call deinit here on purpose because some commands
|
// Note: we do NOT call deinit here on purpose because some commands
|
||||||
// transfer memory ownership. If state needs cleanup, the switch
|
// transfer memory ownership. If state needs cleanup, the switch
|
||||||
// prong below should handle it.
|
// prong below should handle it.
|
||||||
defer self.state = .{ .inactive = {} };
|
defer self.state = .inactive;
|
||||||
|
|
||||||
return switch (self.state) {
|
return switch (self.state) {
|
||||||
.inactive,
|
.inactive,
|
||||||
|
|
@ -167,7 +167,7 @@ pub const Handler = struct {
|
||||||
|
|
||||||
.tmux => if (comptime build_options.tmux_control_mode) tmux: {
|
.tmux => if (comptime build_options.tmux_control_mode) tmux: {
|
||||||
self.state.deinit();
|
self.state.deinit();
|
||||||
break :tmux .{ .tmux = .{ .exit = {} } };
|
break :tmux .{ .tmux = .exit };
|
||||||
} else unreachable,
|
} else unreachable,
|
||||||
|
|
||||||
.xtgettcap => |*list| xtgettcap: {
|
.xtgettcap => |*list| xtgettcap: {
|
||||||
|
|
@ -200,7 +200,7 @@ pub const Handler = struct {
|
||||||
|
|
||||||
fn discard(self: *Handler) void {
|
fn discard(self: *Handler) void {
|
||||||
self.state.deinit();
|
self.state.deinit();
|
||||||
self.state = .{ .inactive = {} };
|
self.state = .inactive;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -255,21 +255,15 @@ pub const Command = union(enum) {
|
||||||
decstbm,
|
decstbm,
|
||||||
decslrm,
|
decslrm,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Tmux control mode
|
|
||||||
pub const Tmux = union(enum) {
|
|
||||||
enter: void,
|
|
||||||
exit: void,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const State = union(enum) {
|
const State = union(enum) {
|
||||||
/// We're not in a DCS state at the moment.
|
/// We're not in a DCS state at the moment.
|
||||||
inactive: void,
|
inactive,
|
||||||
|
|
||||||
/// We're hooked, but its an unknown DCS command or one that went
|
/// We're hooked, but its an unknown DCS command or one that went
|
||||||
/// invalid due to some bad input, so we're ignoring the rest.
|
/// invalid due to some bad input, so we're ignoring the rest.
|
||||||
ignore: void,
|
ignore,
|
||||||
|
|
||||||
/// XTGETTCAP
|
/// XTGETTCAP
|
||||||
xtgettcap: std.Io.Writer.Allocating,
|
xtgettcap: std.Io.Writer.Allocating,
|
||||||
|
|
|
||||||
|
|
@ -271,6 +271,90 @@ pub const Client = struct {
|
||||||
// Important: do not clear buffer here since name points to it
|
// Important: do not clear buffer here since name points to it
|
||||||
self.state = .idle;
|
self.state = .idle;
|
||||||
return .{ .window_renamed = .{ .id = id, .name = name } };
|
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 {
|
} else {
|
||||||
// Unknown notification, log it and return to idle state.
|
// Unknown notification, log it and return to idle state.
|
||||||
log.warn("unknown tmux control mode notification={s}", .{cmd});
|
log.warn("unknown tmux control mode notification={s}", .{cmd});
|
||||||
|
|
@ -291,34 +375,75 @@ pub const Client = struct {
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Possible notification types from tmux control mode. These are documented
|
/// Possible notification types from tmux control mode. These are documented
|
||||||
/// in tmux(1).
|
/// in tmux(1). A lot of the simple documentation was copied from that man
|
||||||
|
/// page here.
|
||||||
pub const Notification = union(enum) {
|
pub const Notification = union(enum) {
|
||||||
enter: void,
|
/// Entering tmux control mode. This isn't an actual event sent by
|
||||||
exit: void,
|
/// 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_end: []const u8,
|
||||||
block_err: []const u8,
|
block_err: []const u8,
|
||||||
|
|
||||||
|
/// Raw output from a pane.
|
||||||
output: struct {
|
output: struct {
|
||||||
pane_id: usize,
|
pane_id: usize,
|
||||||
data: []const u8, // unescaped
|
data: []const u8, // unescaped
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// The client is now attached to the session with ID session-id, which is
|
||||||
|
/// named name.
|
||||||
session_changed: struct {
|
session_changed: struct {
|
||||||
id: usize,
|
id: usize,
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
},
|
},
|
||||||
|
|
||||||
sessions_changed: void,
|
/// A session was created or destroyed.
|
||||||
|
sessions_changed,
|
||||||
|
|
||||||
|
/// The window with ID window-id was linked to the current session.
|
||||||
window_add: struct {
|
window_add: struct {
|
||||||
id: usize,
|
id: usize,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// The window with ID window-id was renamed to name.
|
||||||
window_renamed: struct {
|
window_renamed: struct {
|
||||||
id: usize,
|
id: usize,
|
||||||
name: []const u8,
|
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" {
|
test "tmux begin/end empty" {
|
||||||
|
|
@ -433,3 +558,42 @@ test "tmux window-renamed" {
|
||||||
try testing.expectEqual(42, n.window_renamed.id);
|
try testing.expectEqual(42, n.window_renamed.id);
|
||||||
try testing.expectEqualStrings("bar", n.window_renamed.name);
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue