vaxis: add osc52 copy/paste support
This commit is contained in:
parent
a1263b1baa
commit
d48826c0b1
14 changed files with 180 additions and 36 deletions
|
@ -19,7 +19,7 @@ pub fn main() !void {
|
|||
|
||||
var loop: vaxis.Loop(Event) = .{ .vaxis = &vx };
|
||||
|
||||
try loop.run();
|
||||
try loop.run(alloc);
|
||||
defer loop.stop();
|
||||
|
||||
try vx.queryTerminal();
|
||||
|
|
|
@ -23,7 +23,7 @@ pub fn main() !void {
|
|||
|
||||
var loop: vaxis.Loop(Event) = .{ .vaxis = &vx };
|
||||
|
||||
try loop.run();
|
||||
try loop.run(alloc);
|
||||
defer loop.stop();
|
||||
|
||||
try vx.enterAltScreen();
|
||||
|
|
|
@ -22,7 +22,7 @@ pub fn main() !void {
|
|||
|
||||
// Start the read loop. This puts the terminal in raw mode and begins
|
||||
// reading user input
|
||||
try loop.run();
|
||||
try loop.run(alloc);
|
||||
defer loop.stop();
|
||||
|
||||
// Optionally enter the alternate screen
|
||||
|
|
|
@ -29,7 +29,7 @@ pub fn main() !void {
|
|||
|
||||
var loop: vaxis.Loop(Event) = .{ .vaxis = &vx };
|
||||
|
||||
try loop.run();
|
||||
try loop.run(alloc);
|
||||
defer loop.stop();
|
||||
|
||||
// Optionally enter the alternate screen
|
||||
|
|
|
@ -16,7 +16,7 @@ pub fn main() !void {
|
|||
|
||||
var loop: vaxis.Loop(Event) = .{ .vaxis = &vx };
|
||||
|
||||
try loop.run();
|
||||
try loop.run(alloc);
|
||||
defer loop.stop();
|
||||
try vx.enterAltScreen();
|
||||
try vx.queryTerminal();
|
||||
|
|
|
@ -19,7 +19,7 @@ pub fn main() !void {
|
|||
|
||||
var loop: vaxis.Loop(Event) = .{ .vaxis = &vx };
|
||||
|
||||
try loop.run();
|
||||
try loop.run(alloc);
|
||||
defer loop.stop();
|
||||
|
||||
try vx.queryTerminal();
|
||||
|
|
|
@ -32,7 +32,7 @@ pub fn main() !void {
|
|||
winsize: vaxis.Winsize,
|
||||
}) = .{ .vaxis = &vx };
|
||||
|
||||
try loop.run();
|
||||
try loop.run(alloc);
|
||||
defer loop.stop();
|
||||
try vx.enterAltScreen();
|
||||
try vx.queryTerminal();
|
||||
|
|
|
@ -42,7 +42,7 @@ pub fn main() !void {
|
|||
|
||||
// Start the read loop. This puts the terminal in raw mode and begins
|
||||
// reading user input
|
||||
try loop.run();
|
||||
try loop.run(alloc);
|
||||
defer loop.stop();
|
||||
|
||||
// Optionally enter the alternate screen
|
||||
|
@ -85,7 +85,7 @@ pub fn main() !void {
|
|||
loop.stop();
|
||||
var child = std.process.Child.init(&.{"nvim"}, alloc);
|
||||
_ = try child.spawnAndWait();
|
||||
try loop.run();
|
||||
try loop.run(alloc);
|
||||
try vx.enterAltScreen();
|
||||
vx.queueRefresh();
|
||||
} else if (key.matches(vaxis.Key.enter, .{})) {
|
||||
|
|
|
@ -25,6 +25,7 @@ pub fn Loop(comptime T: type) type {
|
|||
T,
|
||||
self,
|
||||
&self.vaxis.unicode.grapheme_data,
|
||||
self.vaxis.opts.system_clipboard_allocator,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
149
src/Parser.zig
149
src/Parser.zig
|
@ -61,7 +61,7 @@ buf: [128]u8 = undefined,
|
|||
|
||||
grapheme_data: *const grapheme.GraphemeData,
|
||||
|
||||
pub fn parse(self: *Parser, input: []const u8) !Result {
|
||||
pub fn parse(self: *Parser, input: []const u8, paste_allocator: ?std.mem.Allocator) !Result {
|
||||
const n = input.len;
|
||||
|
||||
var seq: Sequence = .{};
|
||||
|
@ -555,6 +555,88 @@ pub fn parse(self: *Parser, input: []const u8) !Result {
|
|||
else => {},
|
||||
}
|
||||
},
|
||||
.osc => {
|
||||
switch (b) {
|
||||
0x07, 0x1B => {
|
||||
state = .ground;
|
||||
if (b == 0x1b) {
|
||||
// advance one more for the backslash
|
||||
i += 1;
|
||||
}
|
||||
log.warn("unhandled osc: OSC {s}", .{input[start + 1 .. i + 1]});
|
||||
return .{
|
||||
.event = null,
|
||||
.n = i + 1,
|
||||
};
|
||||
},
|
||||
0x30...0x39 => {
|
||||
seq.param_buf[seq.param_buf_idx] = b;
|
||||
seq.param_buf_idx += 1;
|
||||
},
|
||||
';' => {
|
||||
if (seq.param_buf_idx == 0) {
|
||||
seq.param_idx += 1;
|
||||
}
|
||||
if (seq.param_idx == 0) {
|
||||
const p = try std.fmt.parseUnsigned(u16, seq.param_buf[0..seq.param_buf_idx], 10);
|
||||
seq.param_buf_idx = 0;
|
||||
seq.param_idx += 1;
|
||||
switch (p) {
|
||||
52 => {
|
||||
var payload: ?std.ArrayList(u8) = if (paste_allocator) |allocator|
|
||||
std.ArrayList(u8).init(allocator)
|
||||
else
|
||||
null;
|
||||
defer if (payload) |_| payload.?.deinit();
|
||||
|
||||
while (i < n) : (i += 1) {
|
||||
const b_ = input[i];
|
||||
switch (b_) {
|
||||
';' => {
|
||||
if (seq.param_buf_idx == 0) {
|
||||
// empty param. default it to 0 and set the
|
||||
// empty state
|
||||
seq.params[seq.param_idx] = 0;
|
||||
seq.empty_state.set(seq.param_idx);
|
||||
seq.param_idx += 1;
|
||||
} else {
|
||||
seq.params[seq.param_idx] = @intCast(b_);
|
||||
seq.param_buf_idx = 0;
|
||||
seq.param_idx += 1;
|
||||
}
|
||||
},
|
||||
0x07, 0x1B => {
|
||||
state = .ground;
|
||||
if (b == 0x1b) {
|
||||
// advance one more for the backslash
|
||||
i += 1;
|
||||
}
|
||||
if (payload) |_| {
|
||||
log.debug("decoding paste: {s}", .{payload.?.items});
|
||||
const decoder = std.base64.standard.Decoder;
|
||||
const text = try paste_allocator.?.alloc(u8, try decoder.calcSizeForSlice(payload.?.items));
|
||||
try decoder.decode(text, payload.?.items);
|
||||
log.debug("decoded paste: {s}", .{text});
|
||||
return .{
|
||||
.event = .{ .paste = text },
|
||||
.n = i + 2,
|
||||
};
|
||||
} else return .{
|
||||
.event = null,
|
||||
.n = i + 2,
|
||||
};
|
||||
},
|
||||
else => if (seq.param_idx == 3 and payload != null) try payload.?.append(b_),
|
||||
}
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
@ -572,7 +654,7 @@ test "parse: single xterm keypress" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "a";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_key: Key = .{
|
||||
.codepoint = 'a',
|
||||
.text = "a",
|
||||
|
@ -589,7 +671,7 @@ test "parse: single xterm keypress backspace" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "\x08";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_key: Key = .{
|
||||
.codepoint = Key.backspace,
|
||||
};
|
||||
|
@ -605,7 +687,7 @@ test "parse: single xterm keypress with more buffer" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "ab";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_key: Key = .{
|
||||
.codepoint = 'a',
|
||||
.text = "a",
|
||||
|
@ -623,7 +705,7 @@ test "parse: xterm escape keypress" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "\x1b";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_key: Key = .{ .codepoint = Key.escape };
|
||||
const expected_event: Event = .{ .key_press = expected_key };
|
||||
|
||||
|
@ -637,7 +719,7 @@ test "parse: xterm ctrl+a" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "\x01";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .ctrl = true } };
|
||||
const expected_event: Event = .{ .key_press = expected_key };
|
||||
|
||||
|
@ -651,7 +733,7 @@ test "parse: xterm alt+a" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "\x1ba";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .alt = true } };
|
||||
const expected_event: Event = .{ .key_press = expected_key };
|
||||
|
||||
|
@ -665,7 +747,7 @@ test "parse: xterm invalid ss3" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "\x1bOZ";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
|
||||
try testing.expectEqual(3, result.n);
|
||||
try testing.expectEqual(null, result.event);
|
||||
|
@ -679,7 +761,7 @@ test "parse: xterm key up" {
|
|||
// normal version
|
||||
const input = "\x1bOA";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_key: Key = .{ .codepoint = Key.up };
|
||||
const expected_event: Event = .{ .key_press = expected_key };
|
||||
|
||||
|
@ -691,7 +773,7 @@ test "parse: xterm key up" {
|
|||
// application keys version
|
||||
const input = "\x1b[2~";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_key: Key = .{ .codepoint = Key.insert };
|
||||
const expected_event: Event = .{ .key_press = expected_key };
|
||||
|
||||
|
@ -706,7 +788,7 @@ test "parse: xterm shift+up" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "\x1b[1;2A";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } };
|
||||
const expected_event: Event = .{ .key_press = expected_key };
|
||||
|
||||
|
@ -720,7 +802,7 @@ test "parse: xterm insert" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "\x1b[1;2A";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } };
|
||||
const expected_event: Event = .{ .key_press = expected_key };
|
||||
|
||||
|
@ -734,7 +816,7 @@ test "parse: paste_start" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "\x1b[200~";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_event: Event = .paste_start;
|
||||
|
||||
try testing.expectEqual(6, result.n);
|
||||
|
@ -747,20 +829,39 @@ test "parse: paste_end" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "\x1b[201~";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_event: Event = .paste_end;
|
||||
|
||||
try testing.expectEqual(6, result.n);
|
||||
try testing.expectEqual(expected_event, result.event);
|
||||
}
|
||||
|
||||
test "parse: osc52 paste" {
|
||||
const alloc = testing.allocator_instance.allocator();
|
||||
const grapheme_data = try grapheme.GraphemeData.init(alloc);
|
||||
defer grapheme_data.deinit();
|
||||
const input = "\x1b]52;c;b3NjNTIgcGFzdGU=\x1b\\";
|
||||
const expected_text = "osc52 paste";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input, alloc);
|
||||
|
||||
try testing.expectEqual(25, result.n);
|
||||
switch (result.event.?) {
|
||||
.paste => |text| {
|
||||
defer alloc.free(text);
|
||||
try testing.expectEqualStrings(expected_text, text);
|
||||
},
|
||||
else => try testing.expect(false),
|
||||
}
|
||||
}
|
||||
|
||||
test "parse: focus_in" {
|
||||
const alloc = testing.allocator_instance.allocator();
|
||||
const grapheme_data = try grapheme.GraphemeData.init(alloc);
|
||||
defer grapheme_data.deinit();
|
||||
const input = "\x1b[I";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_event: Event = .focus_in;
|
||||
|
||||
try testing.expectEqual(3, result.n);
|
||||
|
@ -773,7 +874,7 @@ test "parse: focus_out" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "\x1b[O";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_event: Event = .focus_out;
|
||||
|
||||
try testing.expectEqual(3, result.n);
|
||||
|
@ -786,7 +887,7 @@ test "parse: kitty: shift+a without text reporting" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "\x1b[97:65;2u";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_key: Key = .{
|
||||
.codepoint = 'a',
|
||||
.shifted_codepoint = 'A',
|
||||
|
@ -804,7 +905,7 @@ test "parse: kitty: alt+shift+a without text reporting" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "\x1b[97:65;4u";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_key: Key = .{
|
||||
.codepoint = 'a',
|
||||
.shifted_codepoint = 'A',
|
||||
|
@ -822,7 +923,7 @@ test "parse: kitty: a without text reporting" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "\x1b[97u";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_key: Key = .{
|
||||
.codepoint = 'a',
|
||||
};
|
||||
|
@ -838,7 +939,7 @@ test "parse: kitty: release event" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "\x1b[97;1:3u";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_key: Key = .{
|
||||
.codepoint = 'a',
|
||||
};
|
||||
|
@ -854,7 +955,7 @@ test "parse: single codepoint" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "🙂";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_key: Key = .{
|
||||
.codepoint = 0x1F642,
|
||||
.text = input,
|
||||
|
@ -871,7 +972,7 @@ test "parse: single codepoint with more in buffer" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "🙂a";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_key: Key = .{
|
||||
.codepoint = 0x1F642,
|
||||
.text = "🙂",
|
||||
|
@ -888,7 +989,7 @@ test "parse: multiple codepoint grapheme" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "👩🚀";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_key: Key = .{
|
||||
.codepoint = Key.multicodepoint,
|
||||
.text = input,
|
||||
|
@ -905,7 +1006,7 @@ test "parse: multiple codepoint grapheme with more after" {
|
|||
defer grapheme_data.deinit();
|
||||
const input = "👩🚀abc";
|
||||
var parser: Parser = .{ .grapheme_data = &grapheme_data };
|
||||
const result = try parser.parse(input);
|
||||
const result = try parser.parse(input, alloc);
|
||||
const expected_key: Key = .{
|
||||
.codepoint = Key.multicodepoint,
|
||||
.text = "👩🚀",
|
||||
|
|
11
src/Tty.zig
11
src/Tty.zig
|
@ -89,6 +89,7 @@ pub fn run(
|
|||
comptime Event: type,
|
||||
loop: *Loop(Event),
|
||||
grapheme_data: *const grapheme.GraphemeData,
|
||||
paste_allocator: ?std.mem.Allocator,
|
||||
) !void {
|
||||
// get our initial winsize
|
||||
const winsize = try getWinsize(self.fd);
|
||||
|
@ -146,7 +147,7 @@ pub fn run(
|
|||
const n = try posix.read(self.fd, &buf);
|
||||
var start: usize = 0;
|
||||
while (start < n) {
|
||||
const result = try parser.parse(buf[start..n]);
|
||||
const result = try parser.parse(buf[start..n], paste_allocator);
|
||||
start += result.n;
|
||||
// TODO: if we get 0 byte read, copy the remaining bytes to the
|
||||
// beginning of the buffer and read mmore? this should only happen
|
||||
|
@ -201,6 +202,14 @@ pub fn run(
|
|||
loop.postEvent(.paste_end);
|
||||
}
|
||||
},
|
||||
.paste => |text| {
|
||||
if (@hasField(Event, "paste")) {
|
||||
loop.postEvent(.{ .paste = text });
|
||||
} else {
|
||||
if (paste_allocator) |_|
|
||||
paste_allocator.?.free(text);
|
||||
}
|
||||
},
|
||||
.cap_kitty_keyboard => {
|
||||
log.info("kitty keyboard capability detected", .{});
|
||||
loop.vaxis.caps.kitty_keyboard = true;
|
||||
|
|
|
@ -35,6 +35,10 @@ pub const Capabilities = struct {
|
|||
|
||||
pub const Options = struct {
|
||||
kitty_keyboard_flags: KittyFlags = .{},
|
||||
/// When supplied, this allocator will be used for system clipboard
|
||||
/// requests. If not supplied, it won't be possible to request the system
|
||||
/// clipboard
|
||||
system_clipboard_allocator: ?std.mem.Allocator = null,
|
||||
};
|
||||
|
||||
tty: ?Tty,
|
||||
|
@ -793,3 +797,29 @@ pub fn freeImage(self: Vaxis, id: u32) void {
|
|||
log.err("couldn't flush writer: {}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn copyToSystemClipboard(self: Vaxis, text: []const u8, encode_allocator: std.mem.Allocator) !void {
|
||||
var tty = self.tty orelse return;
|
||||
const encoder = std.base64.standard.Encoder;
|
||||
const size = encoder.calcSize(text.len);
|
||||
const buf = try encode_allocator.alloc(u8, size);
|
||||
const b64 = encoder.encode(buf, text);
|
||||
defer encode_allocator.free(buf);
|
||||
try std.fmt.format(
|
||||
tty.buffered_writer.writer(),
|
||||
ctlseqs.osc52_clipboard_copy,
|
||||
.{b64},
|
||||
);
|
||||
try tty.flush();
|
||||
}
|
||||
|
||||
pub fn requestSystemClipboard(self: Vaxis) !void {
|
||||
if (self.opts.system_clipboard_allocator == null) return error.NoClipboardAllocator;
|
||||
var tty = self.tty orelse return;
|
||||
try std.fmt.format(
|
||||
tty.buffered_writer.writer(),
|
||||
ctlseqs.osc52_clipboard_request,
|
||||
.{},
|
||||
);
|
||||
try tty.flush();
|
||||
}
|
||||
|
|
|
@ -105,6 +105,8 @@ pub const osc8_clear = "\x1b]8;;\x1b\\";
|
|||
pub const osc9_notify = "\x1b]9;{s}\x1b\\";
|
||||
pub const osc777_notify = "\x1b]777;notify;{s};{s}\x1b\\";
|
||||
pub const osc22_mouse_shape = "\x1b]22;{s}\x1b\\";
|
||||
pub const osc52_clipboard_copy = "\x1b]52;c;{s}\x1b\\";
|
||||
pub const osc52_clipboard_request = "\x1b]52;c;?\x1b\\";
|
||||
|
||||
// Kitty graphics
|
||||
pub const kitty_graphics_clear = "\x1b_Ga=d\x1b\\";
|
||||
|
|
|
@ -8,8 +8,9 @@ pub const Event = union(enum) {
|
|||
mouse: Mouse,
|
||||
focus_in,
|
||||
focus_out,
|
||||
paste_start,
|
||||
paste_end,
|
||||
paste_start, // bracketed paste start
|
||||
paste_end, // bracketed paste end
|
||||
paste: []const u8, // osc 52 paste, caller must free
|
||||
|
||||
// these are delivered as discovered terminal capabilities
|
||||
cap_kitty_keyboard,
|
||||
|
|
Loading…
Reference in a new issue