Skip to content

Commit

Permalink
caps!: implement explicit width extension
Browse files Browse the repository at this point in the history
Implement explicit width hint extension, developed by kitty. When
both explicit width and mode 2027 are available, we default to explicit
width. Custom event loop authors will need to update their loops to add
support for this by setting the new capability value.

For simplicity, we don't actually add a flag in the parser for checking
between a cursor position and an F3 key. Instead, we send the cursor
home, then do an explicit width command, *then* check the cursor
position. If the cursor has moved - meaning the extension is supported -
we will see an F3 key with the shift modifier. The response will be
something like `\x1b[1;2R` which we parse as a shift+F3. But in the
loop, we check the flag if we have sent queries and handle this specific
event differently.

Reference: kovidgoyal/kitty#8226
  • Loading branch information
rockorager committed Feb 3, 2025
1 parent 9ec4232 commit 1150a32
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Unix-likes.
- Color Mode Updates (Mode 2031)
- [In-Band Resize Reports](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83) (Mode 2048)
- 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)

## Usage

Expand Down
14 changes: 14 additions & 0 deletions src/Loop.zig
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Even
},
.cap_da1 => {
std.Thread.Futex.wake(&vx.query_futex, 10);
vx.queries_done.store(true, .unordered);
},
.mouse => |mouse| {
if (@hasField(Event, "mouse")) {
Expand All @@ -220,6 +221,18 @@ pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Even
else => {
switch (event) {
.key_press => |key| {
// Check for a cursor position response for our explicity width query. This will
// always be an F3 key with shift = true, and we must be looking for queries
if (key.codepoint == vaxis.Key.f3 and
key.mods.shift and
!vx.queries_done.load(.unordered))
{
log.info("explicit width capability detected", .{});
vx.caps.explicit_width = true;
vx.caps.unicode = .unicode;
vx.screen.width_method = .unicode;
return;
}
if (@hasField(Event, "key_press")) {
// HACK: yuck. there has to be a better way
var mut_key = key;
Expand Down Expand Up @@ -311,6 +324,7 @@ pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Even
},
.cap_da1 => {
std.Thread.Futex.wake(&vx.query_futex, 10);
vx.queries_done.store(true, .unordered);
},
.winsize => |winsize| {
vx.state.in_band_resize = true;
Expand Down
30 changes: 27 additions & 3 deletions src/Vaxis.zig
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub const Capabilities = struct {
unicode: gwidth.Method = .wcwidth,
sgr_pixels: bool = false,
color_scheme_updates: bool = false,
explicit_width: bool = false,
};

pub const Options = struct {
Expand Down Expand Up @@ -63,6 +64,11 @@ refresh: bool = false,
/// futex times out
query_futex: atomic.Value(u32) = atomic.Value(u32).init(0),

/// If Queries were sent, we set this to false. We reset to true when all queries are complete. This
/// is used because we do explicit cursor position reports in the queries, which interfere with F3
/// key encoding. This can be used as a flag to determine how we should evaluate this sequence
queries_done: atomic.Value(bool) = atomic.Value(bool).init(true),

// images
next_img_id: u32 = 1,

Expand Down Expand Up @@ -236,13 +242,15 @@ pub fn queryTerminal(self: *Vaxis, tty: AnyWriter, timeout_ns: u64) !void {
try self.queryTerminalSend(tty);
// 1 second timeout
std.Thread.Futex.timedWait(&self.query_futex, 0, timeout_ns) catch {};
self.queries_done.store(true, .unordered);
try self.enableDetectedFeatures(tty);
}

/// write queries to the terminal to determine capabilities. This function
/// is only for use with a custom main loop. Call Vaxis.queryTerminal() if
/// you are using Loop.run()
pub fn queryTerminalSend(_: Vaxis, tty: AnyWriter) !void {
pub fn queryTerminalSend(vx: *Vaxis, tty: AnyWriter) !void {
vx.queries_done.store(false, .unordered);

// TODO: re-enable this
// const colorterm = std.posix.getenv("COLORTERM") orelse "";
Expand All @@ -263,6 +271,15 @@ pub fn queryTerminalSend(_: Vaxis, tty: AnyWriter) !void {
ctlseqs.decrqm_unicode ++
ctlseqs.decrqm_color_scheme ++
ctlseqs.in_band_resize_set ++

// Explicit width query. We send the cursor home, then do an explicit width command, then
// query the position. If the parsed value is an F3 with shift, we support explicit width.
// The returned response will be something like \x1b[1;2R...which when parsed as a Key is a
// shift + F3 (the row is ignored). We only care if the column has moved from 1->2, which is
// why we see a Shift modifier
ctlseqs.home ++
ctlseqs.explicit_width_query ++
ctlseqs.cursor_position_request ++
ctlseqs.xtversion ++
ctlseqs.csi_u_query ++
ctlseqs.kitty_graphics_query ++
Expand Down Expand Up @@ -302,7 +319,8 @@ pub fn enableDetectedFeatures(self: *Vaxis, tty: AnyWriter) !void {
if (self.caps.kitty_keyboard) {
try self.enableKittyKeyboard(tty, self.opts.kitty_keyboard_flags);
}
if (self.caps.unicode == .unicode) {
// Only enable mode 2027 if we don't have explicit width
if (self.caps.unicode == .unicode and !self.caps.explicit_width) {
try tty.writeAll(ctlseqs.unicode_set);
}
},
Expand Down Expand Up @@ -611,7 +629,13 @@ pub fn render(self: *Vaxis, tty: AnyWriter) !void {
}
try tty.print(ctlseqs.osc8, .{ ps, cell.link.uri });
}
try tty.writeAll(cell.char.grapheme);

// If we have explicit width and our width is greater than 1, let's use it
if (self.caps.explicit_width and w > 1) {
try tty.print(ctlseqs.explicit_width, .{ w, cell.char.grapheme });
} else {
try tty.writeAll(cell.char.grapheme);
}
cursor_pos.col = col + w;
cursor_pos.row = row;
}
Expand Down
3 changes: 3 additions & 0 deletions src/ctlseqs.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ pub const decrqm_color_scheme = "\x1b[?2031$p";
pub const csi_u_query = "\x1b[?u";
pub const kitty_graphics_query = "\x1b_Gi=1,a=q\x1b\\";
pub const sixel_geometry_query = "\x1b[?2;1;0S";
pub const cursor_position_request = "\x1b[6n";
pub const explicit_width_query = "\x1b]66;w=1; \x1b\\";

// mouse. We try for button motion and any motion. terminals will enable the
// last one we tried (any motion). This was added because zellij doesn't
Expand All @@ -31,6 +33,7 @@ pub const sync_reset = "\x1b[?2026l";
// unicode
pub const unicode_set = "\x1b[?2027h";
pub const unicode_reset = "\x1b[?2027l";
pub const explicit_width = "\x1b]66;w={d};{s}\x1b\\";

// bracketed paste
pub const bp_set = "\x1b[?2004h";
Expand Down

0 comments on commit 1150a32

Please sign in to comment.