mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 16:26:08 +03:00
Merge pull request #1946 from ghostty-org/tmuxcc
Tmux Control Mode Parser (ONLY the parser)
This commit is contained in:
@ -800,7 +800,31 @@ test "csi: too many params" {
|
||||
}
|
||||
}
|
||||
|
||||
test "dcs" {
|
||||
test "dcs: XTGETTCAP" {
|
||||
var p = init();
|
||||
_ = p.next(0x1B);
|
||||
for ("P+") |c| {
|
||||
const a = p.next(c);
|
||||
try testing.expect(a[0] == null);
|
||||
try testing.expect(a[1] == null);
|
||||
try testing.expect(a[2] == null);
|
||||
}
|
||||
|
||||
{
|
||||
const a = p.next('q');
|
||||
try testing.expect(p.state == .dcs_passthrough);
|
||||
try testing.expect(a[0] == null);
|
||||
try testing.expect(a[1] == null);
|
||||
try testing.expect(a[2].? == .dcs_hook);
|
||||
|
||||
const hook = a[2].?.dcs_hook;
|
||||
try testing.expectEqualSlices(u8, &[_]u8{'+'}, hook.intermediates);
|
||||
try testing.expectEqualSlices(u16, &[_]u16{}, hook.params);
|
||||
try testing.expectEqual('q', hook.final);
|
||||
}
|
||||
}
|
||||
|
||||
test "dcs: params" {
|
||||
var p = init();
|
||||
_ = p.next(0x1B);
|
||||
for ("P1000") |c| {
|
||||
|
@ -21,33 +21,67 @@ pub const Handler = struct {
|
||||
self.discard();
|
||||
}
|
||||
|
||||
pub fn hook(self: *Handler, alloc: Allocator, dcs: DCS) void {
|
||||
pub fn hook(self: *Handler, alloc: Allocator, dcs: DCS) ?Command {
|
||||
assert(self.state == .inactive);
|
||||
self.state = if (tryHook(alloc, dcs)) |state_| state: {
|
||||
if (state_) |state| break :state state else {
|
||||
log.info("unknown DCS hook: {}", .{dcs});
|
||||
break :state .{ .ignore = {} };
|
||||
}
|
||||
} else |err| state: {
|
||||
log.info(
|
||||
"error initializing DCS hook, will ignore hook err={}",
|
||||
.{err},
|
||||
);
|
||||
break :state .{ .ignore = {} };
|
||||
|
||||
// Initialize our state to ignore in case of error
|
||||
self.state = .{ .ignore = {} };
|
||||
|
||||
// Try to parse the hook.
|
||||
const hk_ = self.tryHook(alloc, dcs) catch |err| {
|
||||
log.info("error initializing DCS hook, will ignore hook err={}", .{err});
|
||||
return null;
|
||||
};
|
||||
const hk = hk_ orelse {
|
||||
log.info("unknown DCS hook: {}", .{dcs});
|
||||
return null;
|
||||
};
|
||||
|
||||
self.state = hk.state;
|
||||
return hk.command;
|
||||
}
|
||||
|
||||
fn tryHook(alloc: Allocator, dcs: DCS) !?State {
|
||||
const Hook = struct {
|
||||
state: State,
|
||||
command: ?Command = null,
|
||||
};
|
||||
|
||||
fn tryHook(self: Handler, alloc: Allocator, dcs: DCS) !?Hook {
|
||||
return switch (dcs.intermediates.len) {
|
||||
0 => switch (dcs.final) {
|
||||
// Tmux control mode
|
||||
'p' => tmux: {
|
||||
// Tmux control mode must start with ESC P 1000 p
|
||||
if (dcs.params.len != 1 or dcs.params[0] != 1000) break :tmux null;
|
||||
|
||||
break :tmux .{
|
||||
.state = .{
|
||||
.tmux = .{
|
||||
.max_bytes = self.max_bytes,
|
||||
.buffer = try std.ArrayList(u8).initCapacity(
|
||||
alloc,
|
||||
128, // Arbitrary choice to limit initial reallocs
|
||||
),
|
||||
},
|
||||
},
|
||||
.command = .{ .tmux = .{ .enter = {} } },
|
||||
};
|
||||
},
|
||||
|
||||
else => null,
|
||||
},
|
||||
|
||||
1 => switch (dcs.intermediates[0]) {
|
||||
'+' => switch (dcs.final) {
|
||||
// XTGETTCAP
|
||||
// https://github.com/mitchellh/ghostty/issues/517
|
||||
'q' => .{
|
||||
.xtgettcap = try std.ArrayList(u8).initCapacity(
|
||||
alloc,
|
||||
128, // Arbitrary choice
|
||||
),
|
||||
.state = .{
|
||||
.xtgettcap = try std.ArrayList(u8).initCapacity(
|
||||
alloc,
|
||||
128, // Arbitrary choice
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
else => null,
|
||||
@ -55,9 +89,9 @@ pub const Handler = struct {
|
||||
|
||||
'$' => switch (dcs.final) {
|
||||
// DECRQSS
|
||||
'q' => .{
|
||||
'q' => .{ .state = .{
|
||||
.decrqss = .{},
|
||||
},
|
||||
} },
|
||||
|
||||
else => null,
|
||||
},
|
||||
@ -69,21 +103,28 @@ pub const Handler = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn put(self: *Handler, byte: u8) void {
|
||||
self.tryPut(byte) catch |err| {
|
||||
/// Put a byte into the DCS handler. This will return a command
|
||||
/// if a command needs to be executed.
|
||||
pub fn put(self: *Handler, byte: u8) ?Command {
|
||||
return self.tryPut(byte) catch |err| {
|
||||
// On error we just discard our state and ignore the rest
|
||||
log.info("error putting byte into DCS handler err={}", .{err});
|
||||
self.discard();
|
||||
self.state = .{ .ignore = {} };
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
fn tryPut(self: *Handler, byte: u8) !void {
|
||||
fn tryPut(self: *Handler, byte: u8) !?Command {
|
||||
switch (self.state) {
|
||||
.inactive,
|
||||
.ignore,
|
||||
=> {},
|
||||
|
||||
.tmux => |*tmux| return .{
|
||||
.tmux = (try tmux.put(byte)) orelse return null,
|
||||
},
|
||||
|
||||
.xtgettcap => |*list| {
|
||||
if (list.items.len >= self.max_bytes) {
|
||||
return error.OutOfMemory;
|
||||
@ -101,15 +142,26 @@ pub const Handler = struct {
|
||||
buffer.len += 1;
|
||||
},
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn unhook(self: *Handler) ?Command {
|
||||
// Note: we do NOT call deinit here on purpose because some commands
|
||||
// transfer memory ownership. If state needs cleanup, the switch
|
||||
// prong below should handle it.
|
||||
defer self.state = .{ .inactive = {} };
|
||||
|
||||
return switch (self.state) {
|
||||
.inactive,
|
||||
.ignore,
|
||||
=> null,
|
||||
|
||||
.tmux => tmux: {
|
||||
self.state.deinit();
|
||||
break :tmux .{ .tmux = .{ .exit = {} } };
|
||||
},
|
||||
|
||||
.xtgettcap => |list| .{ .xtgettcap = .{ .data = list } },
|
||||
|
||||
.decrqss => |buffer| .{ .decrqss = switch (buffer.len) {
|
||||
@ -133,16 +185,7 @@ pub const Handler = struct {
|
||||
}
|
||||
|
||||
fn discard(self: *Handler) void {
|
||||
switch (self.state) {
|
||||
.inactive,
|
||||
.ignore,
|
||||
=> {},
|
||||
|
||||
.xtgettcap => |*list| list.deinit(),
|
||||
|
||||
.decrqss => {},
|
||||
}
|
||||
|
||||
self.state.deinit();
|
||||
self.state = .{ .inactive = {} };
|
||||
}
|
||||
};
|
||||
@ -154,12 +197,14 @@ pub const Command = union(enum) {
|
||||
/// DECRQSS
|
||||
decrqss: DECRQSS,
|
||||
|
||||
/// Tmux control mode
|
||||
tmux: terminal.tmux.Notification,
|
||||
|
||||
pub fn deinit(self: Command) void {
|
||||
switch (self) {
|
||||
.xtgettcap => |*v| {
|
||||
v.data.deinit();
|
||||
},
|
||||
.xtgettcap => |*v| v.data.deinit(),
|
||||
.decrqss => {},
|
||||
.tmux => {},
|
||||
}
|
||||
}
|
||||
|
||||
@ -193,6 +238,12 @@ pub const Command = union(enum) {
|
||||
decstbm,
|
||||
decslrm,
|
||||
};
|
||||
|
||||
/// Tmux control mode
|
||||
pub const Tmux = union(enum) {
|
||||
enter: void,
|
||||
exit: void,
|
||||
};
|
||||
};
|
||||
|
||||
const State = union(enum) {
|
||||
@ -211,6 +262,21 @@ const State = union(enum) {
|
||||
data: [2]u8 = undefined,
|
||||
len: u2 = 0,
|
||||
},
|
||||
|
||||
/// Tmux control mode: https://github.com/tmux/tmux/wiki/Control-Mode
|
||||
tmux: terminal.tmux.Client,
|
||||
|
||||
pub fn deinit(self: *State) void {
|
||||
switch (self.*) {
|
||||
.inactive,
|
||||
.ignore,
|
||||
=> {},
|
||||
|
||||
.xtgettcap => |*v| v.deinit(),
|
||||
.decrqss => {},
|
||||
.tmux => |*v| v.deinit(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test "unknown DCS command" {
|
||||
@ -219,7 +285,7 @@ test "unknown DCS command" {
|
||||
|
||||
var h: Handler = .{};
|
||||
defer h.deinit();
|
||||
h.hook(alloc, .{ .final = 'A' });
|
||||
try testing.expect(h.hook(alloc, .{ .final = 'A' }) == null);
|
||||
try testing.expect(h.state == .ignore);
|
||||
try testing.expect(h.unhook() == null);
|
||||
try testing.expect(h.state == .inactive);
|
||||
@ -231,8 +297,8 @@ test "XTGETTCAP command" {
|
||||
|
||||
var h: Handler = .{};
|
||||
defer h.deinit();
|
||||
h.hook(alloc, .{ .intermediates = "+", .final = 'q' });
|
||||
for ("536D756C78") |byte| h.put(byte);
|
||||
try testing.expect(h.hook(alloc, .{ .intermediates = "+", .final = 'q' }) == null);
|
||||
for ("536D756C78") |byte| _ = h.put(byte);
|
||||
var cmd = h.unhook().?;
|
||||
defer cmd.deinit();
|
||||
try testing.expect(cmd == .xtgettcap);
|
||||
@ -246,8 +312,8 @@ test "XTGETTCAP command multiple keys" {
|
||||
|
||||
var h: Handler = .{};
|
||||
defer h.deinit();
|
||||
h.hook(alloc, .{ .intermediates = "+", .final = 'q' });
|
||||
for ("536D756C78;536D756C78") |byte| h.put(byte);
|
||||
try testing.expect(h.hook(alloc, .{ .intermediates = "+", .final = 'q' }) == null);
|
||||
for ("536D756C78;536D756C78") |byte| _ = h.put(byte);
|
||||
var cmd = h.unhook().?;
|
||||
defer cmd.deinit();
|
||||
try testing.expect(cmd == .xtgettcap);
|
||||
@ -262,8 +328,8 @@ test "XTGETTCAP command invalid data" {
|
||||
|
||||
var h: Handler = .{};
|
||||
defer h.deinit();
|
||||
h.hook(alloc, .{ .intermediates = "+", .final = 'q' });
|
||||
for ("who;536D756C78") |byte| h.put(byte);
|
||||
try testing.expect(h.hook(alloc, .{ .intermediates = "+", .final = 'q' }) == null);
|
||||
for ("who;536D756C78") |byte| _ = h.put(byte);
|
||||
var cmd = h.unhook().?;
|
||||
defer cmd.deinit();
|
||||
try testing.expect(cmd == .xtgettcap);
|
||||
@ -278,8 +344,8 @@ test "DECRQSS command" {
|
||||
|
||||
var h: Handler = .{};
|
||||
defer h.deinit();
|
||||
h.hook(alloc, .{ .intermediates = "$", .final = 'q' });
|
||||
h.put('m');
|
||||
try testing.expect(h.hook(alloc, .{ .intermediates = "$", .final = 'q' }) == null);
|
||||
_ = h.put('m');
|
||||
var cmd = h.unhook().?;
|
||||
defer cmd.deinit();
|
||||
try testing.expect(cmd == .decrqss);
|
||||
@ -292,8 +358,8 @@ test "DECRQSS invalid command" {
|
||||
|
||||
var h: Handler = .{};
|
||||
defer h.deinit();
|
||||
h.hook(alloc, .{ .intermediates = "$", .final = 'q' });
|
||||
h.put('z');
|
||||
try testing.expect(h.hook(alloc, .{ .intermediates = "$", .final = 'q' }) == null);
|
||||
_ = h.put('z');
|
||||
var cmd = h.unhook().?;
|
||||
defer cmd.deinit();
|
||||
try testing.expect(cmd == .decrqss);
|
||||
@ -301,9 +367,31 @@ test "DECRQSS invalid command" {
|
||||
|
||||
h.discard();
|
||||
|
||||
h.hook(alloc, .{ .intermediates = "$", .final = 'q' });
|
||||
h.put('"');
|
||||
h.put(' ');
|
||||
h.put('q');
|
||||
try testing.expect(h.hook(alloc, .{ .intermediates = "$", .final = 'q' }) == null);
|
||||
_ = h.put('"');
|
||||
_ = h.put(' ');
|
||||
_ = h.put('q');
|
||||
try testing.expect(h.unhook() == null);
|
||||
}
|
||||
|
||||
test "tmux enter and implicit exit" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var h: Handler = .{};
|
||||
defer h.deinit();
|
||||
|
||||
{
|
||||
var cmd = h.hook(alloc, .{ .params = &.{1000}, .final = 'p' }).?;
|
||||
defer cmd.deinit();
|
||||
try testing.expect(cmd == .tmux);
|
||||
try testing.expect(cmd.tmux == .enter);
|
||||
}
|
||||
|
||||
{
|
||||
var cmd = h.unhook().?;
|
||||
defer cmd.deinit();
|
||||
try testing.expect(cmd == .tmux);
|
||||
try testing.expect(cmd.tmux == .exit);
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ pub const modes = @import("modes.zig");
|
||||
pub const page = @import("page.zig");
|
||||
pub const parse_table = @import("parse_table.zig");
|
||||
pub const size = @import("size.zig");
|
||||
pub const tmux = @import("tmux.zig");
|
||||
pub const x11_color = @import("x11_color.zig");
|
||||
|
||||
pub const Charset = charsets.Charset;
|
||||
|
423
src/terminal/tmux.zig
Normal file
423
src/terminal/tmux.zig
Normal file
@ -0,0 +1,423 @@
|
||||
//! 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 = std.debug.assert;
|
||||
const oni = @import("oniguruma");
|
||||
|
||||
const log = std.log.scoped(.terminal_tmux);
|
||||
|
||||
/// 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.ArrayList(u8),
|
||||
|
||||
/// 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.
|
||||
broken,
|
||||
|
||||
/// Inside an active notification (started with '%').
|
||||
notification,
|
||||
|
||||
/// Inside a begin/end block.
|
||||
block,
|
||||
};
|
||||
|
||||
pub fn deinit(self: *Client) void {
|
||||
self.buffer.deinit();
|
||||
}
|
||||
|
||||
// Handle a byte of input.
|
||||
pub fn put(self: *Client, byte: u8) !?Notification {
|
||||
if (self.buffer.items.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 idx = if (std.mem.lastIndexOfScalar(
|
||||
u8,
|
||||
self.buffer.items,
|
||||
'\n',
|
||||
)) |v| v + 1 else 0;
|
||||
const line = self.buffer.items[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, self.buffer.items[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.append(byte);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn parseNotification(self: *Client) !?Notification {
|
||||
assert(self.state == .notification);
|
||||
|
||||
const line = line: {
|
||||
var line = self.buffer.items;
|
||||
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 {
|
||||
// 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.clearAndFree();
|
||||
}
|
||||
};
|
||||
|
||||
/// Possible notification types from tmux control mode. These are documented
|
||||
/// in tmux(1).
|
||||
pub const Notification = union(enum) {
|
||||
enter: void,
|
||||
exit: void,
|
||||
|
||||
block_end: []const u8,
|
||||
block_err: []const u8,
|
||||
|
||||
output: struct {
|
||||
pane_id: usize,
|
||||
data: []const u8, // unescaped
|
||||
},
|
||||
|
||||
session_changed: struct {
|
||||
id: usize,
|
||||
name: []const u8,
|
||||
},
|
||||
|
||||
sessions_changed: void,
|
||||
|
||||
window_add: struct {
|
||||
id: usize,
|
||||
},
|
||||
|
||||
window_renamed: struct {
|
||||
id: usize,
|
||||
name: []const u8,
|
||||
},
|
||||
};
|
||||
|
||||
test "tmux begin/end empty" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var c: Client = .{ .buffer = std.ArrayList(u8).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 = std.ArrayList(u8).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 = std.ArrayList(u8).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 = std.ArrayList(u8).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 = std.ArrayList(u8).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 = std.ArrayList(u8).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 = std.ArrayList(u8).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 = std.ArrayList(u8).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 = std.ArrayList(u8).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);
|
||||
}
|
@ -1860,19 +1860,31 @@ const StreamHandler = struct {
|
||||
}
|
||||
|
||||
pub fn dcsHook(self: *StreamHandler, dcs: terminal.DCS) !void {
|
||||
self.dcs.hook(self.alloc, dcs);
|
||||
var cmd = self.dcs.hook(self.alloc, dcs) orelse return;
|
||||
defer cmd.deinit();
|
||||
try self.dcsCommand(&cmd);
|
||||
}
|
||||
|
||||
pub fn dcsPut(self: *StreamHandler, byte: u8) !void {
|
||||
self.dcs.put(byte);
|
||||
var cmd = self.dcs.put(byte) orelse return;
|
||||
defer cmd.deinit();
|
||||
try self.dcsCommand(&cmd);
|
||||
}
|
||||
|
||||
pub fn dcsUnhook(self: *StreamHandler) !void {
|
||||
var cmd = self.dcs.unhook() orelse return;
|
||||
defer cmd.deinit();
|
||||
try self.dcsCommand(&cmd);
|
||||
}
|
||||
|
||||
fn dcsCommand(self: *StreamHandler, cmd: *terminal.dcs.Command) !void {
|
||||
// log.warn("DCS command: {}", .{cmd});
|
||||
switch (cmd) {
|
||||
switch (cmd.*) {
|
||||
.tmux => |tmux| {
|
||||
// TODO: process it
|
||||
log.warn("tmux control mode event unimplemented cmd={}", .{tmux});
|
||||
},
|
||||
|
||||
.xtgettcap => |*gettcap| {
|
||||
const map = comptime terminfo.ghostty.xtgettcapMap();
|
||||
while (gettcap.next()) |key| {
|
||||
@ -1880,6 +1892,7 @@ const StreamHandler = struct {
|
||||
self.messageWriter(.{ .write_stable = response });
|
||||
}
|
||||
},
|
||||
|
||||
.decrqss => |decrqss| {
|
||||
var response: [128]u8 = undefined;
|
||||
var stream = std.io.fixedBufferStream(&response);
|
||||
|
Reference in New Issue
Block a user