this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: implement scaled text

Implement the kitty scaled text extension. We do this by adding a Scale
struct to the core Cell structure. The scale struct defines the scale,
numerator, denominator, and vertical positioning. We also query
independently for scaled text vs explicit width text.

Users of scaled text must be very careful of how they model their cells,
as cells must be written ahead of time with knowledge of the
capabilities of the underlying terminal. Libvaxis does not move contents
of cells based on scale (ie 3 contiguous cells of scale=2 with contents
"abc" will not transform into what you think - the user of the library
must take car to put a, b, and c into appropriate cells. Libvaxis will
do the work to prevent overwriting the cell). Capability can be checked
in the vx.caps.scaled_text value.

+149 -3
+24 -1
examples/main.zig
··· 34 34 var color_idx: u8 = 0; 35 35 const msg = "Hello, world!"; 36 36 37 + var scale: u3 = 1; 38 + 37 39 // The main event loop. Vaxis provides a thread safe, blocking, buffered 38 40 // queue which can serve as the primary event queue for an application 39 41 while (true) { ··· 50 52 }; 51 53 if (key.codepoint == 'c' and key.mods.ctrl) { 52 54 break; 55 + } 56 + if (key.matches('j', .{})) { 57 + if (vx.caps.scaled_text and scale > 1) { 58 + scale -= 1; 59 + } 60 + } 61 + if (key.matches('k', .{})) { 62 + if (vx.caps.scaled_text and scale < 7) { 63 + scale += 1; 64 + } 53 65 } 54 66 }, 55 67 .winsize => |ws| { ··· 86 98 .style = .{ 87 99 .fg = .{ .index = color_idx }, 88 100 }, 101 + .scale = .{ 102 + .scale = scale, 103 + }, 89 104 }; 90 - child.writeCell(@intCast(i), 0, cell); 105 + const second_cell: Cell = .{ 106 + .char = .{ .grapheme = msg[i .. i + 1] }, 107 + .style = .{ 108 + .fg = .{ .index = color_idx }, 109 + }, 110 + }; 111 + child.writeCell(@intCast(i * scale), 0, cell); 112 + child.writeCell(@intCast(i), scale - 1, second_cell); 113 + child.writeCell(@intCast(i), scale, second_cell); 91 114 } 92 115 // Render the screen 93 116 try vx.render(tty.anyWriter());
+20
src/Cell.zig
··· 9 9 /// Set to true if this cell is the last cell printed in a row before wrap. Vaxis will determine if 10 10 /// it should rely on the terminal's autowrap feature which can help with primary screen resizes 11 11 wrapped: bool = false, 12 + scale: Scale = .{}, 12 13 13 14 /// Segment is a contiguous run of text that has a constant style 14 15 pub const Segment = struct { ··· 40 41 uri: []const u8 = "", 41 42 /// ie "id=app-1234" 42 43 params: []const u8 = "", 44 + }; 45 + 46 + pub const Scale = packed struct { 47 + scale: u3 = 1, 48 + // The spec allows up to 15, but we limit to 7 49 + numerator: u4 = 1, 50 + // The spec allows up to 15, but we limit to 7 51 + denominator: u4 = 1, 52 + vertical_alignment: enum(u2) { 53 + top = 0, 54 + bottom = 1, 55 + center = 2, 56 + } = .top, 57 + 58 + pub fn eql(self: Scale, other: Scale) bool { 59 + const a_scale: u13 = @bitCast(self); 60 + const b_scale: u13 = @bitCast(other); 61 + return a_scale == b_scale; 62 + } 43 63 }; 44 64 45 65 pub const Style = struct {
+6
src/InternalScreen.zig
··· 18 18 skipped: bool = false, 19 19 default: bool = true, 20 20 21 + // If we should skip rendering *this* round due to being printed over previously (from a scaled 22 + // cell, for example) 23 + skip: bool = false, 24 + 25 + scale: Cell.Scale = .{}, 26 + 21 27 pub fn eql(self: InternalCell, cell: Cell) bool { 22 28 23 29 // fastpath when both cells are default
+21 -1
src/Loop.zig
··· 177 177 } 178 178 }, 179 179 .key_press => |key| { 180 - // Check for a cursor position response for our explicity width query. This will 180 + // Check for a cursor position response for our explicit width query. This will 181 181 // always be an F3 key with shift = true, and we must be looking for queries 182 182 if (key.codepoint == vaxis.Key.f3 and 183 183 key.mods.shift and ··· 187 187 vx.caps.explicit_width = true; 188 188 vx.caps.unicode = .unicode; 189 189 vx.screen.width_method = .unicode; 190 + return; 191 + } 192 + // Check for a cursor position response for our scaled text query. This will 193 + // always be an F3 key with alt = true, and we must be looking for queries 194 + if (key.codepoint == vaxis.Key.f3 and 195 + key.mods.alt and 196 + !vx.queries_done.load(.unordered)) 197 + { 198 + log.info("scaled text capability detected", .{}); 199 + vx.caps.scaled_text = true; 190 200 return; 191 201 } 192 202 if (@hasField(Event, "key_press")) { ··· 243 253 vx.caps.explicit_width = true; 244 254 vx.caps.unicode = .unicode; 245 255 vx.screen.width_method = .unicode; 256 + return; 257 + } 258 + // Check for a cursor position response for our scaled text query. This will 259 + // always be an F3 key with alt = true, and we must be looking for queries 260 + if (key.codepoint == vaxis.Key.f3 and 261 + key.mods.alt and 262 + !vx.queries_done.load(.unordered)) 263 + { 264 + log.info("scaled text capability detected", .{}); 265 + vx.caps.scaled_text = true; 246 266 return; 247 267 } 248 268 if (@hasField(Event, "key_press")) {
+73 -1
src/Vaxis.zig
··· 37 37 sgr_pixels: bool = false, 38 38 color_scheme_updates: bool = false, 39 39 explicit_width: bool = false, 40 + scaled_text: bool = false, 40 41 }; 41 42 42 43 pub const Options = struct { ··· 280 281 ctlseqs.home ++ 281 282 ctlseqs.explicit_width_query ++ 282 283 ctlseqs.cursor_position_request ++ 284 + // Explicit width query. We send the cursor home, then do an scaled text command, then 285 + // query the position. If the parsed value is an F3 with al, we support scaled text. 286 + // The returned response will be something like \x1b[1;3R...which when parsed as a Key is a 287 + // alt + F3 (the row is ignored). We only care if the column has moved from 1->3, which is 288 + // why we see a Shift modifier 289 + ctlseqs.home ++ 290 + ctlseqs.scaled_text_query ++ 291 + ctlseqs.cursor_position_request ++ 283 292 ctlseqs.xtversion ++ 284 293 ctlseqs.csi_u_query ++ 285 294 ctlseqs.kitty_graphics_query ++ ··· 373 382 if (self.caps.kitty_graphics) 374 383 try tty.writeAll(ctlseqs.kitty_graphics_clear); 375 384 385 + // Reset skip flag on all last_screen cells 386 + for (self.screen_last.buf) |*last_cell| { 387 + last_cell.skip = false; 388 + } 389 + 376 390 var i: usize = 0; 377 391 while (i < self.screen.buf.len) { 378 392 const cell = self.screen.buf[i]; ··· 404 418 // If cell is the same as our last frame, we don't need to do 405 419 // anything 406 420 const last = self.screen_last.buf[i]; 407 - if (!self.refresh and last.eql(cell) and !last.skipped and cell.image == null) { 421 + if ((!self.refresh and 422 + last.eql(cell) and 423 + !last.skipped and 424 + cell.image == null) or 425 + last.skip) 426 + { 408 427 reposition = true; 409 428 // Close any osc8 sequence we might be in before 410 429 // repositioning ··· 421 440 // Set this cell in the last frame 422 441 self.screen_last.writeCell(col, row, cell); 423 442 443 + // If we support scaled text, we set the flags now 444 + if (self.caps.scaled_text and cell.scale.scale > 1) { 445 + // The cell is scaled. Set appropriate skips. We only need to do this if the scale factor is 446 + // > 1 447 + assert(cell.char.width > 0); 448 + const cols = cell.scale.scale * cell.char.width; 449 + const rows = cell.scale.scale; 450 + for (0..rows) |skipped_row| { 451 + for (0..cols) |skipped_col| { 452 + if (skipped_row == 0 and skipped_col == 0) { 453 + continue; 454 + } 455 + const skipped_i = (@as(usize, @intCast(skipped_row + row)) * self.screen_last.width) + (skipped_col + col); 456 + self.screen_last.buf[skipped_i].skip = true; 457 + } 458 + } 459 + } 460 + 424 461 // reposition the cursor, if needed 425 462 if (reposition) { 426 463 reposition = false; ··· 628 665 ps = ""; 629 666 } 630 667 try tty.print(ctlseqs.osc8, .{ ps, cell.link.uri }); 668 + } 669 + 670 + // scale 671 + if (self.caps.scaled_text and !cell.scale.eql(.{})) { 672 + const scale = cell.scale; 673 + // We have a scaled cell. 674 + switch (cell.scale.denominator) { 675 + // Denominator cannot be 0 676 + 0 => unreachable, 677 + 1 => { 678 + // no fractional scaling, just a straight scale factor 679 + try tty.print( 680 + ctlseqs.scaled_text, 681 + .{ scale.scale, w, cell.char.grapheme }, 682 + ); 683 + }, 684 + else => { 685 + // fractional scaling 686 + // no fractional scaling, just a straight scale factor 687 + try tty.print( 688 + ctlseqs.scaled_text_with_fractions, 689 + .{ 690 + scale.scale, 691 + w, 692 + scale.numerator, 693 + scale.denominator, 694 + @intFromEnum(scale.vertical_alignment), 695 + cell.char.grapheme, 696 + }, 697 + ); 698 + }, 699 + } 700 + cursor_pos.col = col + (w * scale.scale); 701 + cursor_pos.row = row; 702 + continue; 631 703 } 632 704 633 705 // If we have explicit width and our width is greater than 1, let's use it
+5
src/ctlseqs.zig
··· 13 13 pub const sixel_geometry_query = "\x1b[?2;1;0S"; 14 14 pub const cursor_position_request = "\x1b[6n"; 15 15 pub const explicit_width_query = "\x1b]66;w=1; \x1b\\"; 16 + pub const scaled_text_query = "\x1b]66;s=2; \x1b\\"; 16 17 17 18 // mouse. We try for button motion and any motion. terminals will enable the 18 19 // last one we tried (any motion). This was added because zellij doesn't ··· 34 35 pub const unicode_set = "\x1b[?2027h"; 35 36 pub const unicode_reset = "\x1b[?2027l"; 36 37 pub const explicit_width = "\x1b]66;w={d};{s}\x1b\\"; 38 + 39 + // text sizing 40 + pub const scaled_text = "\x1b]66;s={d}:w={d};{s}\x1b\\"; 41 + pub const scaled_text_with_fractions = "\x1b]66;s={d}:w={d}:n={d}:d={d}:v={d};{s}\x1b\\"; 37 42 38 43 // bracketed paste 39 44 pub const bp_set = "\x1b[?2004h";