Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,29 @@ Unix-likes.
- Images ([kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/))
- [Explicit Width](https://github.com/kovidgoyal/kitty/blob/master/docs/text-sizing-protocol.rst) (width modifiers only)

### tmux + kitty graphics

When running inside `tmux`, kitty graphics needs passthrough enabled:

```tmux
set -g allow-passthrough on
```

`libvaxis` can auto-detect `tmux` and wrap kitty graphics commands in tmux
passthrough mode. It also uses kitty Unicode placeholders for image rendering
in tmux.

The behavior is configured with `Vaxis.Options.kitty_graphics_tmux_mode`:

- `.auto` (default): enable tmux wrapping when `$TMUX` is present
- `.off`: disable tmux wrapping
- `.on`: force tmux wrapping

In placeholder mode, `pixel_offset` placement is not supported and falls back
to direct placement.
If placeholder row/column indexes exceed the combining-mark table, `libvaxis`
uses a deterministic fallback combining mark.

## Usage

[Documentation](https://rockorager.github.io/libvaxis/#vaxis.Vaxis)
Expand Down Expand Up @@ -295,7 +318,9 @@ pub fn main() !void {
defer tty.deinit();

// Initialize Vaxis
var vx = try vaxis.init(alloc, .{});
var vx = try vaxis.init(alloc, .{
.kitty_graphics_tmux_mode = .auto,
});
// deinit takes an optional allocator. If your program is exiting, you can
// choose to pass a null allocator to save some exit time.
defer vx.deinit(alloc, tty.writer());
Expand Down
23 changes: 23 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@ lets Vaxis know what features it can use. For example, the Kitty Keyboard
protocol, in-band-resize reports, and Unicode width measurements are just a few
examples.

## tmux + kitty graphics

If your application uses kitty graphics inside `tmux`, enable passthrough in
tmux configuration:

```tmux
set -g allow-passthrough on
```

`Vaxis.Options` includes `kitty_graphics_tmux_mode`:

- `.auto` (default): detect tmux via `$TMUX`
- `.off`: disable tmux wrapping
- `.on`: force tmux wrapping

Example:

```zig
var vx = try vaxis.init(alloc, .{
.kitty_graphics_tmux_mode = .auto,
});
```

### `libxev`

Below is an example [`libxev`](https://github.com/mitchellh/libxev) event loop.
Expand Down
4 changes: 4 additions & 0 deletions src/Image.zig
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ pub const TransmitMedium = enum {
pub const Placement = struct {
img_id: u32,
options: Image.DrawOptions,
src_width_px: u16 = 0,
src_height_px: u16 = 0,
};

pub const CellSize = struct {
Expand Down Expand Up @@ -164,6 +166,8 @@ pub fn draw(self: Image, win: Window, opts: DrawOptions) !void {
const p = Placement{
.img_id = self.id,
.options = p_opts,
.src_width_px = self.width,
.src_height_px = self.height,
};
win.writeCell(0, 0, .{ .image = p });
}
Expand Down
81 changes: 64 additions & 17 deletions src/Parser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pub fn parse(self: *Parser, input: []const u8, paste_allocator: ?std.mem.Allocat
if (input[0] == 0x1b and input.len > 1) {
switch (input[1]) {
0x4F => return parseSs3(input),
0x50 => return skipUntilST(input), // DCS
0x50 => return parseDcs(input), // DCS
0x58 => return skipUntilST(input), // SOS
0x5B => return parseCsi(input, &self.buf), // CSI
0x5D => return parseOsc(input, paste_allocator),
Expand Down Expand Up @@ -188,11 +188,9 @@ inline fn parseApc(input: []const u8) Result {
.n = 0,
};
}
const end = std.mem.indexOfScalarPos(u8, input, 2, 0x1b) orelse return .{
.event = null,
.n = 0,
};
const sequence = input[0 .. end + 1 + 1];
const st = skipUntilST(input);
if (st.n == 0) return st;
const sequence = input[0..st.n];

switch (input[2]) {
'G' => return .{
Expand All @@ -206,6 +204,31 @@ inline fn parseApc(input: []const u8) Result {
}
}

inline fn parseDcs(input: []const u8) Result {
const st = skipUntilST(input);
if (st.n == 0) return st;

const sequence = input[0..st.n];
// tmux passthrough wraps escaped sequences as:
// ESC Ptmux; ESC ESC <inner-sequence> ESC ESC \ ESC \
if (sequence.len >= 11 and std.mem.startsWith(u8, sequence, "\x1bPtmux;")) {
// We only need to detect wrapped kitty graphics capability replies here.
// Inner APC can appear as ESC ESC _ G (escaped) or ESC _ G.
if (std.mem.indexOf(u8, sequence, "\x1b\x1b_G") != null or
std.mem.indexOf(u8, sequence, "\x1b_G") != null)
{
return .{
.event = .cap_kitty_graphics,
.n = sequence.len,
};
}
}
return .{
.event = null,
.n = sequence.len,
};
}

/// Skips sequences until we see an ST (String Terminator, ESC \)
inline fn skipUntilST(input: []const u8) Result {
if (input.len < 3) {
Expand All @@ -214,20 +237,24 @@ inline fn skipUntilST(input: []const u8) Result {
.n = 0,
};
}
const end = std.mem.indexOfScalarPos(u8, input, 2, 0x1b) orelse return .{
.event = null,
.n = 0,
};
if (input.len < end + 1 + 1) {
return .{
.event = null,
.n = 0,
};
var i: usize = 2;
while (i + 1 < input.len) : (i += 1) {
if (input[i] != 0x1b) continue;
// tmux passthrough escapes embedded ESC bytes by doubling them.
if (input[i + 1] == 0x1b) {
i += 1;
continue;
}
if (input[i + 1] == '\\') {
return .{
.event = null,
.n = i + 2,
};
}
}
const sequence = input[0 .. end + 1 + 1];
return .{
.event = null,
.n = sequence.len,
.n = 0,
};
}

Expand Down Expand Up @@ -906,6 +933,26 @@ test "parse: osc52 paste" {
}
}

test "parse: dcs with escaped esc bytes" {
const alloc = testing.allocator_instance.allocator();
const input = "\x1bPabc\x1b\x1bdef\x1b\\x";
var parser: Parser = .{};
const result = try parser.parse(input, alloc);

try testing.expectEqual(input.len - 1, result.n);
try testing.expectEqual(@as(?Event, null), result.event);
}

test "parse: tmux wrapped kitty graphics response" {
const alloc = testing.allocator_instance.allocator();
const input = "\x1bPtmux;\x1b\x1b_Gi=1;OK\x1b\x1b\\\x1b\\";
var parser: Parser = .{};
const result = try parser.parse(input, alloc);

try testing.expectEqual(input.len, result.n);
try testing.expectEqual(@as(?Event, .cap_kitty_graphics), result.event);
}

test "parse: focus_in" {
const alloc = testing.allocator_instance.allocator();
const input = "\x1b[I";
Expand Down
Loading