vaxis: add osc52 copy/paste support

This commit is contained in:
CJ van den Berg 2024-05-20 22:01:02 +02:00 committed by Tim Culverhouse
parent a1263b1baa
commit d48826c0b1
14 changed files with 180 additions and 36 deletions

View file

@ -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();

View file

@ -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();

View file

@ -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

View file

@ -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

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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, .{})) {

View file

@ -25,6 +25,7 @@ pub fn Loop(comptime T: type) type {
T,
self,
&self.vaxis.unicode.grapheme_data,
self.vaxis.opts.system_clipboard_allocator,
});
}

View file

@ -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 = "👩‍🚀",

View file

@ -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;

View file

@ -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();
}

View file

@ -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\\";

View file

@ -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,