825 lines
27 KiB
Zig
825 lines
27 KiB
Zig
const std = @import("std");
|
|
const atomic = std.atomic;
|
|
const base64Encoder = std.base64.standard.Encoder;
|
|
const zigimg = @import("zigimg");
|
|
|
|
const Cell = @import("Cell.zig");
|
|
const Image = @import("Image.zig");
|
|
const InternalScreen = @import("InternalScreen.zig");
|
|
const Key = @import("Key.zig");
|
|
const Mouse = @import("Mouse.zig");
|
|
const Screen = @import("Screen.zig");
|
|
const Tty = @import("Tty.zig");
|
|
const Unicode = @import("Unicode.zig");
|
|
const Window = @import("Window.zig");
|
|
|
|
const Hyperlink = Cell.Hyperlink;
|
|
const KittyFlags = Key.KittyFlags;
|
|
const Shape = Mouse.Shape;
|
|
const Style = Cell.Style;
|
|
const Winsize = Tty.Winsize;
|
|
|
|
const ctlseqs = @import("ctlseqs.zig");
|
|
const gwidth = @import("gwidth.zig");
|
|
|
|
const Vaxis = @This();
|
|
|
|
const log = std.log.scoped(.vaxis);
|
|
|
|
pub const Capabilities = struct {
|
|
kitty_keyboard: bool = false,
|
|
kitty_graphics: bool = false,
|
|
rgb: bool = false,
|
|
unicode: gwidth.Method = .wcwidth,
|
|
};
|
|
|
|
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,
|
|
|
|
/// the screen we write to
|
|
screen: Screen,
|
|
/// The last screen we drew. We keep this so we can efficiently update on
|
|
/// the next render
|
|
screen_last: InternalScreen = undefined,
|
|
|
|
caps: Capabilities = .{},
|
|
|
|
opts: Options = .{},
|
|
|
|
/// if we should redraw the entire screen on the next render
|
|
refresh: bool = false,
|
|
|
|
/// blocks the main thread until a DA1 query has been received, or the
|
|
/// futex times out
|
|
query_futex: atomic.Value(u32) = atomic.Value(u32).init(0),
|
|
|
|
// images
|
|
next_img_id: u32 = 1,
|
|
|
|
unicode: Unicode,
|
|
|
|
// statistics
|
|
renders: usize = 0,
|
|
render_dur: i128 = 0,
|
|
render_timer: std.time.Timer,
|
|
|
|
sgr: enum {
|
|
standard,
|
|
legacy,
|
|
} = .standard,
|
|
|
|
/// Initialize Vaxis with runtime options
|
|
pub fn init(alloc: std.mem.Allocator, opts: Options) !Vaxis {
|
|
return .{
|
|
.opts = opts,
|
|
.tty = null,
|
|
.screen = .{},
|
|
.screen_last = .{},
|
|
.render_timer = try std.time.Timer.start(),
|
|
.unicode = try Unicode.init(alloc),
|
|
};
|
|
}
|
|
|
|
/// Resets the terminal to it's original state. If an allocator is
|
|
/// passed, this will free resources associated with Vaxis. This is left as an
|
|
/// optional so applications can choose to not free resources when the
|
|
/// application will be exiting anyways
|
|
pub fn deinit(self: *Vaxis, alloc: ?std.mem.Allocator) void {
|
|
if (self.tty) |*tty| {
|
|
tty.deinit();
|
|
}
|
|
if (alloc) |a| {
|
|
self.screen.deinit(a);
|
|
self.screen_last.deinit(a);
|
|
}
|
|
if (self.renders > 0) {
|
|
const tpr = @divTrunc(self.render_dur, self.renders);
|
|
log.debug("total renders = {d}", .{self.renders});
|
|
log.debug("microseconds per render = {d}", .{tpr});
|
|
}
|
|
self.unicode.deinit();
|
|
}
|
|
|
|
/// resize allocates a slice of cells equal to the number of cells
|
|
/// required to display the screen (ie width x height). Any previous screen is
|
|
/// freed when resizing. The cursor will be sent to it's home position and a
|
|
/// hardware clear-below-cursor will be sent
|
|
pub fn resize(self: *Vaxis, alloc: std.mem.Allocator, winsize: Winsize) !void {
|
|
log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows });
|
|
self.screen.deinit(alloc);
|
|
self.screen = try Screen.init(alloc, winsize, &self.unicode);
|
|
self.screen.width_method = self.caps.unicode;
|
|
// try self.screen.int(alloc, winsize.cols, winsize.rows);
|
|
// we only init our current screen. This has the effect of redrawing
|
|
// every cell
|
|
self.screen_last.deinit(alloc);
|
|
self.screen_last = try InternalScreen.init(alloc, winsize.cols, winsize.rows);
|
|
var tty = self.tty orelse return;
|
|
if (tty.state.alt_screen)
|
|
_ = try tty.write(ctlseqs.home)
|
|
else {
|
|
_ = try tty.buffered_writer.write("\r");
|
|
var i: usize = 0;
|
|
while (i < tty.state.cursor.row) : (i += 1) {
|
|
_ = try tty.buffered_writer.write(ctlseqs.ri);
|
|
}
|
|
}
|
|
_ = try tty.write(ctlseqs.erase_below_cursor);
|
|
try tty.flush();
|
|
}
|
|
|
|
/// returns a Window comprising of the entire terminal screen
|
|
pub fn window(self: *Vaxis) Window {
|
|
return .{
|
|
.x_off = 0,
|
|
.y_off = 0,
|
|
.width = self.screen.width,
|
|
.height = self.screen.height,
|
|
.screen = &self.screen,
|
|
};
|
|
}
|
|
|
|
/// enter the alternate screen. The alternate screen will automatically
|
|
/// be exited if calling deinit while in the alt screen
|
|
pub fn enterAltScreen(self: *Vaxis) !void {
|
|
if (self.tty) |*tty| {
|
|
if (tty.state.alt_screen) return;
|
|
_ = try tty.write(ctlseqs.smcup);
|
|
try tty.flush();
|
|
tty.state.alt_screen = true;
|
|
}
|
|
}
|
|
|
|
/// exit the alternate screen
|
|
pub fn exitAltScreen(self: *Vaxis) !void {
|
|
if (self.tty) |*tty| {
|
|
if (!tty.state.alt_screen) return;
|
|
_ = try tty.write(ctlseqs.rmcup);
|
|
try tty.flush();
|
|
tty.state.alt_screen = false;
|
|
}
|
|
}
|
|
|
|
/// write queries to the terminal to determine capabilities. Individual
|
|
/// capabilities will be delivered to the client and possibly intercepted by
|
|
/// Vaxis to enable features
|
|
pub fn queryTerminal(self: *Vaxis) !void {
|
|
try self.queryTerminalSend();
|
|
|
|
// 1 second timeout
|
|
std.Thread.Futex.timedWait(&self.query_futex, 0, 1 * std.time.ns_per_s) catch {};
|
|
|
|
try self.enableDetectedFeatures();
|
|
}
|
|
|
|
/// 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(self: *Vaxis) !void {
|
|
var tty = self.tty orelse return;
|
|
|
|
// TODO: re-enable this
|
|
// const colorterm = std.posix.getenv("COLORTERM") orelse "";
|
|
// if (std.mem.eql(u8, colorterm, "truecolor") or
|
|
// std.mem.eql(u8, colorterm, "24bit"))
|
|
// {
|
|
// if (@hasField(Event, "cap_rgb")) {
|
|
// self.postEvent(.cap_rgb);
|
|
// }
|
|
// }
|
|
|
|
// TODO: decide if we actually want to query for focus and sync. It
|
|
// doesn't hurt to blindly use them
|
|
// _ = try tty.write(ctlseqs.decrqm_focus);
|
|
// _ = try tty.write(ctlseqs.decrqm_sync);
|
|
_ = try tty.write(ctlseqs.decrqm_unicode);
|
|
_ = try tty.write(ctlseqs.decrqm_color_theme);
|
|
// TODO: XTVERSION has a DCS response. uncomment when we can parse
|
|
// that
|
|
// _ = try tty.write(ctlseqs.xtversion);
|
|
_ = try tty.write(ctlseqs.csi_u_query);
|
|
_ = try tty.write(ctlseqs.kitty_graphics_query);
|
|
// TODO: sixel geometry query interferes with F4 keys.
|
|
// _ = try tty.write(ctlseqs.sixel_geometry_query);
|
|
|
|
// TODO: XTGETTCAP queries ("RGB", "Smulx")
|
|
|
|
_ = try tty.write(ctlseqs.primary_device_attrs);
|
|
try tty.flush();
|
|
}
|
|
|
|
/// Enable features detected by responses to queryTerminal. This function
|
|
/// is only for use with a custom main loop. Call Vaxis.queryTerminal() if
|
|
/// you are using Loop.run()
|
|
pub fn enableDetectedFeatures(self: *Vaxis) !void {
|
|
var tty = self.tty orelse return;
|
|
|
|
// Apply any environment variables
|
|
if (std.posix.getenv("ASCIINEMA_REC")) |_|
|
|
self.sgr = .legacy;
|
|
if (std.posix.getenv("TERMUX_VERSION")) |_|
|
|
self.sgr = .legacy;
|
|
if (std.posix.getenv("VHS_RECORD")) |_| {
|
|
self.caps.unicode = .wcwidth;
|
|
self.caps.kitty_keyboard = false;
|
|
}
|
|
if (std.posix.getenv("VAXIS_FORCE_LEGACY_SGR")) |_|
|
|
self.sgr = .legacy;
|
|
if (std.posix.getenv("VAXIS_FORCE_WCWIDTH")) |_|
|
|
self.caps.unicode = .wcwidth;
|
|
if (std.posix.getenv("VAXIS_FORCE_UNICODE")) |_|
|
|
self.caps.unicode = .unicode;
|
|
|
|
// enable detected features
|
|
if (self.caps.kitty_keyboard) {
|
|
try self.enableKittyKeyboard(self.opts.kitty_keyboard_flags);
|
|
}
|
|
if (self.caps.unicode == .unicode) {
|
|
_ = try tty.write(ctlseqs.unicode_set);
|
|
}
|
|
}
|
|
|
|
// the next render call will refresh the entire screen
|
|
pub fn queueRefresh(self: *Vaxis) void {
|
|
self.refresh = true;
|
|
}
|
|
|
|
/// draws the screen to the terminal
|
|
pub fn render(self: *Vaxis) !void {
|
|
var tty = self.tty orelse return;
|
|
self.renders += 1;
|
|
self.render_timer.reset();
|
|
defer {
|
|
self.render_dur += self.render_timer.read() / std.time.ns_per_us;
|
|
}
|
|
|
|
defer self.refresh = false;
|
|
defer tty.flush() catch {};
|
|
|
|
// Set up sync before we write anything
|
|
// TODO: optimize sync so we only sync _when we have changes_. This
|
|
// requires a smarter buffered writer, we'll probably have to write
|
|
// our own
|
|
_ = try tty.write(ctlseqs.sync_set);
|
|
defer _ = tty.write(ctlseqs.sync_reset) catch {};
|
|
|
|
// Send the cursor to 0,0
|
|
// TODO: this needs to move after we optimize writes. We only do
|
|
// this if we have an update to make. We also need to hide cursor
|
|
// and then reshow it if needed
|
|
_ = try tty.write(ctlseqs.hide_cursor);
|
|
if (tty.state.alt_screen)
|
|
_ = try tty.write(ctlseqs.home)
|
|
else {
|
|
_ = try tty.write("\r");
|
|
var i: usize = 0;
|
|
while (i < tty.state.cursor.row) : (i += 1) {
|
|
_ = try tty.write(ctlseqs.ri);
|
|
}
|
|
}
|
|
_ = try tty.write(ctlseqs.sgr_reset);
|
|
|
|
// initialize some variables
|
|
var reposition: bool = false;
|
|
var row: usize = 0;
|
|
var col: usize = 0;
|
|
var cursor: Style = .{};
|
|
var link: Hyperlink = .{};
|
|
var cursor_pos: struct {
|
|
row: usize = 0,
|
|
col: usize = 0,
|
|
} = .{};
|
|
|
|
// Clear all images
|
|
if (self.caps.kitty_graphics)
|
|
_ = try tty.write(ctlseqs.kitty_graphics_clear);
|
|
|
|
var i: usize = 0;
|
|
while (i < self.screen.buf.len) {
|
|
const cell = self.screen.buf[i];
|
|
const w = blk: {
|
|
if (cell.char.width != 0) break :blk cell.char.width;
|
|
|
|
const method: gwidth.Method = self.caps.unicode;
|
|
const width = gwidth.gwidth(cell.char.grapheme, method, &self.unicode.width_data) catch 1;
|
|
break :blk @max(1, width);
|
|
};
|
|
defer {
|
|
// advance by the width of this char mod 1
|
|
std.debug.assert(w > 0);
|
|
var j = i + 1;
|
|
while (j < i + w) : (j += 1) {
|
|
if (j >= self.screen_last.buf.len) break;
|
|
self.screen_last.buf[j].skipped = true;
|
|
}
|
|
col += w;
|
|
i += w;
|
|
}
|
|
if (col >= self.screen.width) {
|
|
row += 1;
|
|
col = 0;
|
|
reposition = true;
|
|
}
|
|
// If cell is the same as our last frame, we don't need to do
|
|
// anything
|
|
const last = self.screen_last.buf[i];
|
|
if (!self.refresh and last.eql(cell) and !last.skipped and cell.image == null) {
|
|
reposition = true;
|
|
// Close any osc8 sequence we might be in before
|
|
// repositioning
|
|
if (link.uri.len > 0) {
|
|
_ = try tty.write(ctlseqs.osc8_clear);
|
|
}
|
|
continue;
|
|
}
|
|
self.screen_last.buf[i].skipped = false;
|
|
defer {
|
|
cursor = cell.style;
|
|
link = cell.link;
|
|
}
|
|
// Set this cell in the last frame
|
|
self.screen_last.writeCell(col, row, cell);
|
|
|
|
// reposition the cursor, if needed
|
|
if (reposition) {
|
|
reposition = false;
|
|
if (tty.state.alt_screen)
|
|
try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cup, .{ row + 1, col + 1 })
|
|
else {
|
|
if (cursor_pos.row == row) {
|
|
const n = col - cursor_pos.col;
|
|
try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cuf, .{n});
|
|
} else {
|
|
const n = row - cursor_pos.row;
|
|
var _i: usize = 0;
|
|
_ = try tty.buffered_writer.write("\r");
|
|
while (_i < n) : (_i += 1) {
|
|
_ = try tty.buffered_writer.write("\n");
|
|
}
|
|
if (col > 0)
|
|
try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cuf, .{col});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (cell.image) |img| {
|
|
try tty.buffered_writer.writer().print(
|
|
ctlseqs.kitty_graphics_preamble,
|
|
.{img.img_id},
|
|
);
|
|
if (img.options.pixel_offset) |offset| {
|
|
try tty.buffered_writer.writer().print(
|
|
",X={d},Y={d}",
|
|
.{ offset.x, offset.y },
|
|
);
|
|
}
|
|
if (img.options.clip_region) |clip| {
|
|
if (clip.x) |x|
|
|
try tty.buffered_writer.writer().print(
|
|
",x={d}",
|
|
.{x},
|
|
);
|
|
if (clip.y) |y|
|
|
try tty.buffered_writer.writer().print(
|
|
",y={d}",
|
|
.{y},
|
|
);
|
|
if (clip.width) |width|
|
|
try tty.buffered_writer.writer().print(
|
|
",w={d}",
|
|
.{width},
|
|
);
|
|
if (clip.height) |height|
|
|
try tty.buffered_writer.writer().print(
|
|
",h={d}",
|
|
.{height},
|
|
);
|
|
}
|
|
if (img.options.size) |size| {
|
|
if (size.rows) |rows|
|
|
try tty.buffered_writer.writer().print(
|
|
",r={d}",
|
|
.{rows},
|
|
);
|
|
if (size.cols) |cols|
|
|
try tty.buffered_writer.writer().print(
|
|
",c={d}",
|
|
.{cols},
|
|
);
|
|
}
|
|
if (img.options.z_index) |z| {
|
|
try tty.buffered_writer.writer().print(
|
|
",z={d}",
|
|
.{z},
|
|
);
|
|
}
|
|
try tty.buffered_writer.writer().writeAll(ctlseqs.kitty_graphics_closing);
|
|
}
|
|
|
|
// something is different, so let's loop through everything and
|
|
// find out what
|
|
|
|
// foreground
|
|
if (!Cell.Color.eql(cursor.fg, cell.style.fg)) {
|
|
const writer = tty.buffered_writer.writer();
|
|
switch (cell.style.fg) {
|
|
.default => _ = try tty.write(ctlseqs.fg_reset),
|
|
.index => |idx| {
|
|
switch (idx) {
|
|
0...7 => try std.fmt.format(writer, ctlseqs.fg_base, .{idx}),
|
|
8...15 => try std.fmt.format(writer, ctlseqs.fg_bright, .{idx - 8}),
|
|
else => {
|
|
switch (self.sgr) {
|
|
.standard => try std.fmt.format(writer, ctlseqs.fg_indexed, .{idx}),
|
|
.legacy => try std.fmt.format(writer, ctlseqs.fg_indexed_legacy, .{idx}),
|
|
}
|
|
},
|
|
}
|
|
},
|
|
.rgb => |rgb| {
|
|
switch (self.sgr) {
|
|
.standard => try std.fmt.format(writer, ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }),
|
|
.legacy => try std.fmt.format(writer, ctlseqs.fg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }),
|
|
}
|
|
},
|
|
}
|
|
}
|
|
// background
|
|
if (!Cell.Color.eql(cursor.bg, cell.style.bg)) {
|
|
const writer = tty.buffered_writer.writer();
|
|
switch (cell.style.bg) {
|
|
.default => _ = try tty.write(ctlseqs.bg_reset),
|
|
.index => |idx| {
|
|
switch (idx) {
|
|
0...7 => try std.fmt.format(writer, ctlseqs.bg_base, .{idx}),
|
|
8...15 => try std.fmt.format(writer, ctlseqs.bg_bright, .{idx - 8}),
|
|
else => {
|
|
switch (self.sgr) {
|
|
.standard => try std.fmt.format(writer, ctlseqs.bg_indexed, .{idx}),
|
|
.legacy => try std.fmt.format(writer, ctlseqs.bg_indexed_legacy, .{idx}),
|
|
}
|
|
},
|
|
}
|
|
},
|
|
.rgb => |rgb| {
|
|
switch (self.sgr) {
|
|
.standard => try std.fmt.format(writer, ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }),
|
|
.legacy => try std.fmt.format(writer, ctlseqs.bg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }),
|
|
}
|
|
},
|
|
}
|
|
}
|
|
// underline color
|
|
if (!Cell.Color.eql(cursor.ul, cell.style.ul)) {
|
|
const writer = tty.buffered_writer.writer();
|
|
switch (cell.style.bg) {
|
|
.default => _ = try tty.write(ctlseqs.ul_reset),
|
|
.index => |idx| {
|
|
switch (self.sgr) {
|
|
.standard => try std.fmt.format(writer, ctlseqs.ul_indexed, .{idx}),
|
|
.legacy => try std.fmt.format(writer, ctlseqs.ul_indexed_legacy, .{idx}),
|
|
}
|
|
},
|
|
.rgb => |rgb| {
|
|
switch (self.sgr) {
|
|
.standard => try std.fmt.format(writer, ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }),
|
|
.legacy => try std.fmt.format(writer, ctlseqs.ul_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }),
|
|
}
|
|
},
|
|
}
|
|
}
|
|
// underline style
|
|
if (cursor.ul_style != cell.style.ul_style) {
|
|
const seq = switch (cell.style.ul_style) {
|
|
.off => ctlseqs.ul_off,
|
|
.single => ctlseqs.ul_single,
|
|
.double => ctlseqs.ul_double,
|
|
.curly => ctlseqs.ul_curly,
|
|
.dotted => ctlseqs.ul_dotted,
|
|
.dashed => ctlseqs.ul_dashed,
|
|
};
|
|
_ = try tty.write(seq);
|
|
}
|
|
// bold
|
|
if (cursor.bold != cell.style.bold) {
|
|
const seq = switch (cell.style.bold) {
|
|
true => ctlseqs.bold_set,
|
|
false => ctlseqs.bold_dim_reset,
|
|
};
|
|
_ = try tty.write(seq);
|
|
if (cell.style.dim) {
|
|
_ = try tty.write(ctlseqs.dim_set);
|
|
}
|
|
}
|
|
// dim
|
|
if (cursor.dim != cell.style.dim) {
|
|
const seq = switch (cell.style.dim) {
|
|
true => ctlseqs.dim_set,
|
|
false => ctlseqs.bold_dim_reset,
|
|
};
|
|
_ = try tty.write(seq);
|
|
if (cell.style.bold) {
|
|
_ = try tty.write(ctlseqs.bold_set);
|
|
}
|
|
}
|
|
// dim
|
|
if (cursor.italic != cell.style.italic) {
|
|
const seq = switch (cell.style.italic) {
|
|
true => ctlseqs.italic_set,
|
|
false => ctlseqs.italic_reset,
|
|
};
|
|
_ = try tty.write(seq);
|
|
}
|
|
// dim
|
|
if (cursor.blink != cell.style.blink) {
|
|
const seq = switch (cell.style.blink) {
|
|
true => ctlseqs.blink_set,
|
|
false => ctlseqs.blink_reset,
|
|
};
|
|
_ = try tty.write(seq);
|
|
}
|
|
// reverse
|
|
if (cursor.reverse != cell.style.reverse) {
|
|
const seq = switch (cell.style.reverse) {
|
|
true => ctlseqs.reverse_set,
|
|
false => ctlseqs.reverse_reset,
|
|
};
|
|
_ = try tty.write(seq);
|
|
}
|
|
// invisible
|
|
if (cursor.invisible != cell.style.invisible) {
|
|
const seq = switch (cell.style.invisible) {
|
|
true => ctlseqs.invisible_set,
|
|
false => ctlseqs.invisible_reset,
|
|
};
|
|
_ = try tty.write(seq);
|
|
}
|
|
// strikethrough
|
|
if (cursor.strikethrough != cell.style.strikethrough) {
|
|
const seq = switch (cell.style.strikethrough) {
|
|
true => ctlseqs.strikethrough_set,
|
|
false => ctlseqs.strikethrough_reset,
|
|
};
|
|
_ = try tty.write(seq);
|
|
}
|
|
|
|
// url
|
|
if (!std.mem.eql(u8, link.uri, cell.link.uri)) {
|
|
var ps = cell.link.params;
|
|
if (cell.link.uri.len == 0) {
|
|
// Empty out the params no matter what if we don't have
|
|
// a url
|
|
ps = "";
|
|
}
|
|
const writer = tty.buffered_writer.writer();
|
|
try std.fmt.format(writer, ctlseqs.osc8, .{ ps, cell.link.uri });
|
|
}
|
|
_ = try tty.write(cell.char.grapheme);
|
|
cursor_pos.col = col + w;
|
|
cursor_pos.row = row;
|
|
}
|
|
if (self.screen.cursor_vis) {
|
|
if (tty.state.alt_screen) {
|
|
try std.fmt.format(
|
|
tty.buffered_writer.writer(),
|
|
ctlseqs.cup,
|
|
.{
|
|
self.screen.cursor_row + 1,
|
|
self.screen.cursor_col + 1,
|
|
},
|
|
);
|
|
} else {
|
|
// TODO: position cursor relative to current location
|
|
_ = try tty.write("\r");
|
|
var r: usize = 0;
|
|
if (self.screen.cursor_row >= cursor_pos.row) {
|
|
while (r < (self.screen.cursor_row - cursor_pos.row)) : (r += 1) {
|
|
_ = try tty.write("\n");
|
|
}
|
|
} else {
|
|
while (r < (cursor_pos.row - self.screen.cursor_row)) : (r += 1) {
|
|
_ = try tty.write(ctlseqs.ri);
|
|
}
|
|
}
|
|
if (self.screen.cursor_col > 0) {
|
|
try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cuf, .{self.screen.cursor_col});
|
|
}
|
|
}
|
|
self.tty.?.state.cursor.row = self.screen.cursor_row;
|
|
self.tty.?.state.cursor.col = self.screen.cursor_col;
|
|
_ = try tty.write(ctlseqs.show_cursor);
|
|
} else {
|
|
self.tty.?.state.cursor.row = cursor_pos.row;
|
|
self.tty.?.state.cursor.col = cursor_pos.col;
|
|
}
|
|
if (self.screen.mouse_shape != self.screen_last.mouse_shape) {
|
|
try std.fmt.format(
|
|
tty.buffered_writer.writer(),
|
|
ctlseqs.osc22_mouse_shape,
|
|
.{@tagName(self.screen.mouse_shape)},
|
|
);
|
|
self.screen_last.mouse_shape = self.screen.mouse_shape;
|
|
}
|
|
if (self.screen.cursor_shape != self.screen_last.cursor_shape) {
|
|
try std.fmt.format(
|
|
tty.buffered_writer.writer(),
|
|
ctlseqs.cursor_shape,
|
|
.{@intFromEnum(self.screen.cursor_shape)},
|
|
);
|
|
self.screen_last.cursor_shape = self.screen.cursor_shape;
|
|
}
|
|
}
|
|
|
|
fn enableKittyKeyboard(self: *Vaxis, flags: Key.KittyFlags) !void {
|
|
if (self.tty) |*tty| {
|
|
const flag_int: u5 = @bitCast(flags);
|
|
try std.fmt.format(
|
|
tty.buffered_writer.writer(),
|
|
ctlseqs.csi_u_push,
|
|
.{
|
|
flag_int,
|
|
},
|
|
);
|
|
try tty.flush();
|
|
tty.state.kitty_keyboard = true;
|
|
}
|
|
}
|
|
|
|
/// send a system notification
|
|
pub fn notify(self: *Vaxis, title: ?[]const u8, body: []const u8) !void {
|
|
if (self.tty == null) return;
|
|
if (title) |t| {
|
|
try std.fmt.format(
|
|
self.tty.?.buffered_writer.writer(),
|
|
ctlseqs.osc777_notify,
|
|
.{ t, body },
|
|
);
|
|
} else {
|
|
try std.fmt.format(
|
|
self.tty.?.buffered_writer.writer(),
|
|
ctlseqs.osc9_notify,
|
|
.{body},
|
|
);
|
|
}
|
|
try self.tty.?.flush();
|
|
}
|
|
|
|
/// sets the window title
|
|
pub fn setTitle(self: *Vaxis, title: []const u8) !void {
|
|
if (self.tty == null) return;
|
|
try std.fmt.format(
|
|
self.tty.?.buffered_writer.writer(),
|
|
ctlseqs.osc2_set_title,
|
|
.{title},
|
|
);
|
|
try self.tty.?.flush();
|
|
}
|
|
|
|
// turn bracketed paste on or off. An event will be sent at the
|
|
// beginning and end of a detected paste. All keystrokes between these
|
|
// events were pasted
|
|
pub fn setBracketedPaste(self: *Vaxis, enable: bool) !void {
|
|
if (self.tty) |*tty| {
|
|
const seq = if (enable)
|
|
ctlseqs.bp_set
|
|
else
|
|
ctlseqs.bp_reset;
|
|
_ = try tty.write(seq);
|
|
try tty.flush();
|
|
tty.state.bracketed_paste = enable;
|
|
}
|
|
}
|
|
|
|
/// set the mouse shape
|
|
pub fn setMouseShape(self: *Vaxis, shape: Shape) void {
|
|
self.screen.mouse_shape = shape;
|
|
}
|
|
|
|
/// Change the mouse reporting mode
|
|
pub fn setMouseMode(self: *Vaxis, mode: Mouse.ReportingMode) !void {
|
|
if (self.tty) |*tty| {
|
|
switch (mode) {
|
|
.off => {
|
|
_ = try tty.write(ctlseqs.mouse_reset);
|
|
},
|
|
.cells => {
|
|
tty.state.mouse = true;
|
|
_ = try tty.write(ctlseqs.mouse_set);
|
|
},
|
|
.pixels => {
|
|
tty.state.mouse = true;
|
|
_ = try tty.write(ctlseqs.mouse_set_pixels);
|
|
},
|
|
}
|
|
try tty.flush();
|
|
}
|
|
}
|
|
|
|
pub fn loadImage(
|
|
self: *Vaxis,
|
|
alloc: std.mem.Allocator,
|
|
src: Image.Source,
|
|
) !Image {
|
|
if (!self.caps.kitty_graphics) return error.NoGraphicsCapability;
|
|
var tty = self.tty orelse return error.NoTTY;
|
|
defer self.next_img_id += 1;
|
|
|
|
const writer = tty.buffered_writer.writer();
|
|
|
|
var img = switch (src) {
|
|
.path => |path| try zigimg.Image.fromFilePath(alloc, path),
|
|
.mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes),
|
|
};
|
|
defer img.deinit();
|
|
const png_buf = try alloc.alloc(u8, img.imageByteSize());
|
|
defer alloc.free(png_buf);
|
|
const png = try img.writeToMemory(png_buf, .{ .png = .{} });
|
|
const b64_buf = try alloc.alloc(u8, base64Encoder.calcSize(png.len));
|
|
const encoded = base64Encoder.encode(b64_buf, png);
|
|
defer alloc.free(b64_buf);
|
|
|
|
const id = self.next_img_id;
|
|
|
|
if (encoded.len < 4096) {
|
|
try std.fmt.format(
|
|
writer,
|
|
"\x1b_Gf=100,i={d};{s}\x1b\\",
|
|
.{
|
|
id,
|
|
encoded,
|
|
},
|
|
);
|
|
} else {
|
|
var n: usize = 4096;
|
|
|
|
try std.fmt.format(
|
|
writer,
|
|
"\x1b_Gf=100,i={d},m=1;{s}\x1b\\",
|
|
.{ id, encoded[0..n] },
|
|
);
|
|
while (n < encoded.len) : (n += 4096) {
|
|
const end: usize = @min(n + 4096, encoded.len);
|
|
const m: u2 = if (end == encoded.len) 0 else 1;
|
|
try std.fmt.format(
|
|
writer,
|
|
"\x1b_Gm={d};{s}\x1b\\",
|
|
.{
|
|
m,
|
|
encoded[n..end],
|
|
},
|
|
);
|
|
}
|
|
}
|
|
try tty.buffered_writer.flush();
|
|
return .{
|
|
.id = id,
|
|
.width = img.width,
|
|
.height = img.height,
|
|
};
|
|
}
|
|
|
|
/// deletes an image from the terminal's memory
|
|
pub fn freeImage(self: Vaxis, id: u32) void {
|
|
var tty = self.tty orelse return;
|
|
const writer = tty.buffered_writer.writer();
|
|
std.fmt.format(writer, "\x1b_Ga=d,d=I,i={d};\x1b\\", .{id}) catch |err| {
|
|
log.err("couldn't delete image {d}: {}", .{ id, err });
|
|
return;
|
|
};
|
|
tty.buffered_writer.flush() catch |err| {
|
|
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();
|
|
}
|