mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
terminal/tmux: parse session-changed notification
This commit is contained in:
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
|
const oni = @import("oniguruma");
|
||||||
|
|
||||||
const log = std.log.scoped(.terminal_tmux);
|
const log = std.log.scoped(.terminal_tmux);
|
||||||
|
|
||||||
@ -15,7 +16,7 @@ pub const Client = struct {
|
|||||||
/// Current state of the client.
|
/// Current state of the client.
|
||||||
state: State = .idle,
|
state: State = .idle,
|
||||||
|
|
||||||
/// The buffer used to store in-progress commands, output, etc.
|
/// The buffer used to store in-progress notifications, output, etc.
|
||||||
buffer: std.ArrayList(u8),
|
buffer: std.ArrayList(u8),
|
||||||
|
|
||||||
/// The maximum size in bytes of the buffer. This is used to limit
|
/// The maximum size in bytes of the buffer. This is used to limit
|
||||||
@ -25,16 +26,18 @@ pub const Client = struct {
|
|||||||
max_bytes: usize = 1024 * 1024,
|
max_bytes: usize = 1024 * 1024,
|
||||||
|
|
||||||
const State = enum {
|
const State = enum {
|
||||||
/// Outside of any active command. This should drop any output
|
/// Outside of any active notifications. This should drop any output
|
||||||
/// unless it is '%' on the first byte of a line.
|
/// 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,
|
idle,
|
||||||
|
|
||||||
/// We experienced unexpected input and are in a broken state
|
/// We experienced unexpected input and are in a broken state
|
||||||
/// so we cannot continue processing.
|
/// so we cannot continue processing.
|
||||||
broken,
|
broken,
|
||||||
|
|
||||||
/// Inside an active command (started with '%').
|
/// Inside an active notification (started with '%').
|
||||||
command,
|
notification,
|
||||||
|
|
||||||
/// Inside a begin/end block.
|
/// Inside a begin/end block.
|
||||||
block,
|
block,
|
||||||
@ -55,24 +58,27 @@ pub const Client = struct {
|
|||||||
// Drop because we're in a broken state.
|
// Drop because we're in a broken state.
|
||||||
.broken => return null,
|
.broken => return null,
|
||||||
|
|
||||||
// Waiting for a command so if the byte is not '%' then
|
// Waiting for a notification so if the byte is not '%' then
|
||||||
// we're in a broken state. Return an exit command.
|
// 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 != '%') {
|
.idle => if (byte != '%') {
|
||||||
self.broken();
|
self.broken();
|
||||||
return .{ .exit = {} };
|
return .{ .exit = {} };
|
||||||
} else {
|
} else {
|
||||||
self.state = .command;
|
self.buffer.clearRetainingCapacity();
|
||||||
|
self.state = .notification;
|
||||||
},
|
},
|
||||||
|
|
||||||
// If we're in a command and its not a newline then
|
// If we're in a notification and its not a newline then
|
||||||
// we accumulate. If it is a newline then we have a
|
// we accumulate. If it is a newline then we have a
|
||||||
// complete command we need to parse.
|
// complete notification we need to parse.
|
||||||
.command => if (byte == '\n') {
|
.notification => if (byte == '\n') {
|
||||||
// We have a complete command, parse it.
|
// We have a complete notification, parse it.
|
||||||
return try self.parseNotification();
|
return try self.parseNotification();
|
||||||
},
|
},
|
||||||
|
|
||||||
// If we're ina block then we accumulate until we see a newline
|
// 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.
|
// and then we check to see if that line ended the block.
|
||||||
.block => if (byte == '\n') {
|
.block => if (byte == '\n') {
|
||||||
const idx = if (std.mem.lastIndexOfScalar(
|
const idx = if (std.mem.lastIndexOfScalar(
|
||||||
@ -81,6 +87,7 @@ pub const Client = struct {
|
|||||||
'\n',
|
'\n',
|
||||||
)) |v| v + 1 else 0;
|
)) |v| v + 1 else 0;
|
||||||
const line = self.buffer.items[idx..];
|
const line = self.buffer.items[idx..];
|
||||||
|
|
||||||
if (std.mem.startsWith(u8, line, "%end") or
|
if (std.mem.startsWith(u8, line, "%end") or
|
||||||
std.mem.startsWith(u8, line, "%error"))
|
std.mem.startsWith(u8, line, "%error"))
|
||||||
{
|
{
|
||||||
@ -95,6 +102,8 @@ pub const Client = struct {
|
|||||||
self.buffer.clearRetainingCapacity();
|
self.buffer.clearRetainingCapacity();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Didn't end the block, continue accumulating.
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,11 +113,12 @@ pub const Client = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parseNotification(self: *Client) !?Notification {
|
fn parseNotification(self: *Client) !?Notification {
|
||||||
assert(self.state == .command);
|
assert(self.state == .notification);
|
||||||
|
|
||||||
var it = std.mem.tokenizeScalar(u8, self.buffer.items, ' ');
|
const line = self.buffer.items;
|
||||||
|
var it = std.mem.tokenizeScalar(u8, line, ' ');
|
||||||
|
|
||||||
// The command MUST exist because we guard entering the command
|
// The notification MUST exist because we guard entering the notification
|
||||||
// state on seeing at least a '%'.
|
// state on seeing at least a '%'.
|
||||||
const cmd = it.next().?;
|
const cmd = it.next().?;
|
||||||
if (std.mem.eql(u8, cmd, "%begin")) {
|
if (std.mem.eql(u8, cmd, "%begin")) {
|
||||||
@ -123,12 +133,40 @@ pub const Client = struct {
|
|||||||
self.state = .block;
|
self.state = .block;
|
||||||
self.buffer.clearRetainingCapacity();
|
self.buffer.clearRetainingCapacity();
|
||||||
return null;
|
return null;
|
||||||
|
} 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} err={}", .{ cmd, 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 {
|
} else {
|
||||||
// Unknown command, log it and return to idle state.
|
// Unknown notification, log it and return to idle state.
|
||||||
log.warn("unknown tmux control mode command={s}", .{cmd});
|
log.warn("unknown tmux control mode notification={s}", .{cmd});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Successful exit, revert to idle state.
|
// Unknown command. Clear the buffer and return to idle state.
|
||||||
self.buffer.clearRetainingCapacity();
|
self.buffer.clearRetainingCapacity();
|
||||||
self.state = .idle;
|
self.state = .idle;
|
||||||
|
|
||||||
@ -147,6 +185,11 @@ pub const Client = struct {
|
|||||||
pub const Notification = union(enum) {
|
pub const Notification = union(enum) {
|
||||||
enter: void,
|
enter: void,
|
||||||
exit: void,
|
exit: void,
|
||||||
|
|
||||||
|
session_changed: struct {
|
||||||
|
id: usize,
|
||||||
|
name: []const u8,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
test "tmux begin/end empty" {
|
test "tmux begin/end empty" {
|
||||||
@ -168,3 +211,16 @@ test "tmux begin/error empty" {
|
|||||||
for ("%begin 1578922740 269 1\n") |byte| try testing.expect(try c.put(byte) == null);
|
for ("%begin 1578922740 269 1\n") |byte| try testing.expect(try c.put(byte) == null);
|
||||||
for ("%error 1578922740 269 1\n") |byte| try testing.expect(try c.put(byte) == null);
|
for ("%error 1578922740 269 1\n") |byte| try testing.expect(try c.put(byte) == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user