refactor(vaxis): require an io.AnyWriter for writes

All vaxis functions that write to the terminal now require an
io.AnyWriter as a parameter
This commit is contained in:
Tim Culverhouse 2024-05-29 13:13:54 -05:00
parent 59abd7d7d4
commit 5f41978054
13 changed files with 294 additions and 517 deletions

View file

@ -15,38 +15,46 @@ Contributions are welcome.
Vaxis uses zig `0.12.0`. Vaxis uses zig `0.12.0`.
## Feature comparison ## Features
| Feature | Vaxis | libvaxis | notcurses | | Feature | libvaxis |
| ------------------------------ | :---: | :------: | :-------: | | ------------------------------ | :------: |
| RGB | ✅ | ✅ | ✅ | | RGB | ✅ |
| Hyperlinks | ✅ | ✅ | ❌ | | Hyperlinks | ✅ |
| Bracketed Paste | ✅ | ✅ | ❌ | | Bracketed Paste | ✅ |
| Kitty Keyboard | ✅ | ✅ | ✅ | | Kitty Keyboard | ✅ |
| Styled Underlines | ✅ | ✅ | ✅ | | Styled Underlines | ✅ |
| Mouse Shapes (OSC 22) | ✅ | ✅ | ❌ | | Mouse Shapes (OSC 22) | ✅ |
| System Clipboard (OSC 52) | ✅ | ✅ | ❌ | | System Clipboard (OSC 52) | ✅ |
| System Notifications (OSC 9) | ✅ | ✅ | ❌ | | System Notifications (OSC 9) | ✅ |
| System Notifications (OSC 777) | ✅ | ✅ | ❌ | | System Notifications (OSC 777) | ✅ |
| Synchronized Output (DEC 2026) | ✅ | ✅ | ✅ | | Synchronized Output (DEC 2026) | ✅ |
| Unicode Core (DEC 2027) | ✅ | ✅ | ❌ | | Unicode Core (DEC 2027) | ✅ |
| Color Mode Updates (DEC 2031) | ✅ | ✅ | ❌ | | Color Mode Updates (DEC 2031) | ✅ |
| Images (full/space) | ✅ | planned | ✅ | | Images (kitty) | ✅ |
| Images (half block) | ✅ | planned | ✅ |
| Images (quadrant) | ✅ | planned | ✅ |
| Images (sextant) | ❌ | ❌ | ✅ |
| Images (sixel) | ✅ | ❌ | ✅ |
| Images (kitty) | ✅ | ✅ | ✅ |
| Images (iterm2) | ❌ | ❌ | ✅ |
| Video | ❌ | ❌ | ✅ |
| Dank | 🆗 | 🆗 | ✅ |
## Usage ## Usage
[Documentation](https://rockorager.github.io/libvaxis/#vaxis.Vaxis) [Documentation](https://rockorager.github.io/libvaxis/#vaxis.Vaxis)
The below example can be run using `zig build run 2>log`. stderr must be Vaxis requires three basic primitives to operate:
redirected in order to not print to the same screen.
1. A TTY instance
2. An instance of Vaxis
3. An event loop
The library provides a general purpose posix TTY implementation, as well as a
multi-threaded event loop implementation. Users of the library are encouraged to
use the event loop of their choice. The event loop is responsible for reading
the TTY, passing the read bytes to the vaxis parser, and handling events.
A core feature of Vaxis is it's ability to detect features via terminal queries
instead of relying on a terminfo database. This requires that the event loop
also handle these query responses and update the Vaxis.caps struct accordingly.
See the `Loop` implementation to see how this is done if writing your own event
loop.
## Example
```zig ```zig
const std = @import("std"); const std = @import("std");
@ -76,21 +84,35 @@ pub fn main() !void {
} }
const alloc = gpa.allocator(); const alloc = gpa.allocator();
// Initialize a tty
var tty = try vaxis.Tty.init();
defer tty.deinit();
// Initialize Vaxis // Initialize Vaxis
var vx = try vaxis.init(alloc, .{}); var vx = try vaxis.init(alloc, .{});
// deinit takes an optional allocator. If your program is exiting, you can // deinit takes an optional allocator. If your program is exiting, you can
// choose to pass a null allocator to save some exit time. // choose to pass a null allocator to save some exit time.
defer vx.deinit(alloc); defer vx.deinit(alloc, tty.anyWriter());
var loop: vaxis.Loop(Event) = .{}; // The event loop requires an intrusive init. We create an instance with
// stable points to Vaxis and our TTY, then init the instance. Doing so
// installs a signal handler for SIGWINCH on posix TTYs
//
// This event loop is thread safe. It reads the tty in a separate thread
var loop: vaxis.Loop(Event) = .{
.tty = &tty,
.vaxis = &vaxis,
};
try loop.init();
// Start the read loop. This puts the terminal in raw mode and begins // Start the read loop. This puts the terminal in raw mode and begins
// reading user input // reading user input
try loop.run(); try loop.start();
defer loop.stop(); defer loop.stop();
// Optionally enter the alternate screen // Optionally enter the alternate screen
try vx.enterAltScreen(); try vx.enterAltScreen(tty.anyWriter());
// We'll adjust the color index every keypress for the border // We'll adjust the color index every keypress for the border
var color_idx: u8 = 0; var color_idx: u8 = 0;
@ -100,12 +122,10 @@ pub fn main() !void {
var text_input = TextInput.init(alloc); var text_input = TextInput.init(alloc);
defer text_input.deinit(); defer text_input.deinit();
// Sends queries to terminal to detect certain features. This should // Sends queries to terminal to detect certain features. This should always
// _always_ be called, but is left to the application to decide when // be called after entering the alt screen, if you are using the alt screen
try vx.queryTerminal(); try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
// The main event loop. Vaxis provides a thread safe, blocking, buffered
// queue which can serve as the primary event queue for an application
while (true) { while (true) {
// nextEvent blocks until an event is in the queue // nextEvent blocks until an event is in the queue
const event = loop.nextEvent(); const event = loop.nextEvent();
@ -141,7 +161,7 @@ pub fn main() !void {
// more than one byte will incur an allocation on the first render // more than one byte will incur an allocation on the first render
// after it is drawn. Thereafter, it will not allocate unless the // after it is drawn. Thereafter, it will not allocate unless the
// screen is resized // screen is resized
.winsize => |ws| try vx.resize(alloc, ws), .winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
else => {}, else => {},
} }
@ -175,8 +195,9 @@ pub fn main() !void {
// Draw the text_input in the child window // Draw the text_input in the child window
text_input.draw(child); text_input.draw(child);
// Render the screen // Render the screen. Using a buffered writer will offer much better
try vx.render(); // performance, but is not required
try vx.render(tty.anyWriter());
} }
} }
``` ```

View file

@ -14,15 +14,19 @@ pub fn main() !void {
} }
const alloc = gpa.allocator(); const alloc = gpa.allocator();
var tty = try vaxis.Tty.init();
defer tty.deinit();
var vx = try vaxis.init(alloc, .{}); var vx = try vaxis.init(alloc, .{});
defer vx.deinit(alloc); defer vx.deinit(alloc, tty.anyWriter());
var loop: vaxis.Loop(Event) = .{ .vaxis = &vx }; var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
try loop.init();
try loop.run(); try loop.start();
defer loop.stop(); defer loop.stop();
try vx.queryTerminal(); try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
var text_input = TextInput.init(alloc, &vx.unicode); var text_input = TextInput.init(alloc, &vx.unicode);
defer text_input.deinit(); defer text_input.deinit();
@ -70,7 +74,7 @@ pub fn main() !void {
} }
}, },
.winsize => |ws| { .winsize => |ws| {
try vx.resize(alloc, ws); try vx.resize(alloc, tty.anyWriter(), ws);
}, },
else => {}, else => {},
} }
@ -91,7 +95,7 @@ pub fn main() !void {
_ = try win.print(&seg, .{ .row_offset = j + 1 }); _ = try win.print(&seg, .{ .row_offset = j + 1 });
} }
} }
try vx.render(); try vx.render(tty.anyWriter());
} }
} }

View file

@ -18,24 +18,27 @@ pub fn main() !void {
} }
const alloc = gpa.allocator(); const alloc = gpa.allocator();
var tty = try vaxis.Tty.init();
defer tty.deinit();
var vx = try vaxis.init(alloc, .{}); var vx = try vaxis.init(alloc, .{});
defer vx.deinit(alloc); defer vx.deinit(alloc, tty.anyWriter());
var loop: vaxis.Loop(Event) = .{ .vaxis = &vx }; var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
try loop.init();
try loop.run(); try loop.start();
defer loop.stop(); defer loop.stop();
try vx.enterAltScreen(); try vx.enterAltScreen(tty.anyWriter());
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
try vx.queryTerminal();
const imgs = [_]vaxis.Image{ const imgs = [_]vaxis.Image{
try vx.loadImage(alloc, .{ .path = "examples/zig.png" }), try vx.loadImage(alloc, tty.anyWriter(), .{ .path = "examples/zig.png" }),
try vx.loadImage(alloc, .{ .path = "examples/vaxis.png" }), try vx.loadImage(alloc, tty.anyWriter(), .{ .path = "examples/vaxis.png" }),
}; };
defer vx.freeImage(imgs[0].id); defer vx.freeImage(tty.anyWriter(), imgs[0].id);
defer vx.freeImage(imgs[1].id); defer vx.freeImage(tty.anyWriter(), imgs[1].id);
var n: usize = 0; var n: usize = 0;
@ -54,7 +57,7 @@ pub fn main() !void {
else if (key.matches('k', .{})) else if (key.matches('k', .{}))
clip_y -|= 1; clip_y -|= 1;
}, },
.winsize => |ws| try vx.resize(alloc, ws), .winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
} }
n = (n + 1) % imgs.len; n = (n + 1) % imgs.len;
@ -68,6 +71,6 @@ pub fn main() !void {
.y = clip_y, .y = clip_y,
} }); } });
try vx.render(); try vx.render(tty.anyWriter());
} }
} }

View file

@ -14,19 +14,20 @@ pub fn main() !void {
} }
const alloc = gpa.allocator(); const alloc = gpa.allocator();
// Initialize Vaxis var tty = try vaxis.Tty.init();
defer tty.deinit();
var vx = try vaxis.init(alloc, .{}); var vx = try vaxis.init(alloc, .{});
defer vx.deinit(alloc); defer vx.deinit(alloc, tty.anyWriter());
var loop: vaxis.Loop(Event) = .{ .vaxis = &vx }; var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
try loop.init();
// Start the read loop. This puts the terminal in raw mode and begins try loop.start();
// reading user input
try loop.run();
defer loop.stop(); defer loop.stop();
// Optionally enter the alternate screen // Optionally enter the alternate screen
try vx.enterAltScreen(); try vx.enterAltScreen(tty.anyWriter());
// We'll adjust the color index every keypress // We'll adjust the color index every keypress
var color_idx: u8 = 0; var color_idx: u8 = 0;
@ -51,7 +52,7 @@ pub fn main() !void {
} }
}, },
.winsize => |ws| { .winsize => |ws| {
try vx.resize(alloc, ws); try vx.resize(alloc, tty.anyWriter(), ws);
}, },
else => {}, else => {},
} }
@ -85,7 +86,7 @@ pub fn main() !void {
child.writeCell(i, 0, cell); child.writeCell(i, 0, cell);
} }
// Render the screen // Render the screen
try vx.render(); try vx.render(tty.anyWriter());
} }
} }

View file

@ -23,18 +23,22 @@ pub fn main() !void {
} }
const alloc = gpa.allocator(); const alloc = gpa.allocator();
var tty = try vaxis.Tty.init();
defer tty.deinit();
// Initialize Vaxis // Initialize Vaxis
var vx = try vaxis.init(alloc, .{}); var vx = try vaxis.init(alloc, .{});
defer vx.deinit(alloc); defer vx.deinit(alloc, tty.anyWriter());
var loop: vaxis.Loop(Event) = .{ .vaxis = &vx }; var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
try loop.init();
try loop.run(); try loop.start();
defer loop.stop(); defer loop.stop();
// Optionally enter the alternate screen // Optionally enter the alternate screen
try vx.enterAltScreen(); // try vx.enterAltScreen(tty.anyWriter());
try vx.queryTerminal(); try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
var nvim = try vaxis.widgets.nvim.Nvim(Event).init(alloc, &loop); var nvim = try vaxis.widgets.nvim.Nvim(Event).init(alloc, &loop);
try nvim.spawn(); try nvim.spawn();
@ -53,7 +57,7 @@ pub fn main() !void {
try nvim.update(.{ .key_press = key }); try nvim.update(.{ .key_press = key });
}, },
.winsize => |ws| { .winsize => |ws| {
try vx.resize(alloc, ws); try vx.resize(alloc, tty.anyWriter(), ws);
}, },
.nvim => |nvim_event| { .nvim => |nvim_event| {
switch (nvim_event) { switch (nvim_event) {
@ -74,6 +78,6 @@ pub fn main() !void {
}, },
); );
try nvim.draw(child); try nvim.draw(child);
try vx.render(); try vx.render(tty.anyWriter());
} }
} }

View file

@ -1,61 +0,0 @@
const std = @import("std");
const vaxis = @import("vaxis");
const Cell = vaxis.Cell;
const log = std.log.scoped(.main);
const Event = union(enum) {
winsize: vaxis.Winsize,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator();
var vx = try vaxis.init(alloc, .{});
defer vx.deinit(alloc);
var loop: vaxis.Loop(Event) = .{ .vaxis = &vx };
try loop.run();
defer loop.stop();
try vx.enterAltScreen();
try vx.queryTerminal();
while (true) {
const event = loop.nextEvent();
switch (event) {
.winsize => |ws| {
try vx.resize(alloc, ws);
break;
},
}
}
const timer_start = std.time.microTimestamp();
var iter: usize = 0;
while (iter < 10_000) : (iter += 1) {
const win = vx.window();
const child = win.initChild(0, 0, .{ .limit = 20 }, .{ .limit = 20 });
win.clear();
var row: usize = 0;
while (row < child.height) : (row += 1) {
var col: usize = 0;
while (col < child.width) : (col += 1) {
child.writeCell(col, row, .{
.char = .{
.grapheme = " ",
.width = 1,
},
.style = .{
.bg = .{ .index = @truncate(col + iter) },
},
});
}
}
try vx.render();
}
try vx.exitAltScreen();
const took = std.time.microTimestamp() - timer_start;
const stdout = std.io.getStdOut().writer();
try stdout.print("\r\ntook {d}ms to render 10,000 times\r\n", .{@divTrunc(took, std.time.us_per_ms)});
}

View file

@ -31,20 +31,17 @@ pub fn main() !void {
} }
const alloc = gpa.allocator(); const alloc = gpa.allocator();
// Initialize Vaxis with our event type var tty = try vaxis.Tty.init();
var vx = try vaxis.init(Event, .{}); defer tty.deinit();
// 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);
// Start the read loop. This puts the terminal in raw mode and begins var vx = try vaxis.init(alloc, .{});
// reading user input defer vx.deinit(alloc, tty.anyWriter());
try vx.startReadThread();
defer vx.stopReadThread();
// Optionally enter the alternate screen var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
try vx.enterAltScreen(); try loop.init();
defer vx.exitAltScreen() catch {};
try loop.start();
defer loop.stop();
// We'll adjust the color index every keypress for the border // We'll adjust the color index every keypress for the border
var color_idx: u8 = 0; var color_idx: u8 = 0;
@ -56,7 +53,7 @@ pub fn main() !void {
// Sends queries to terminal to detect certain features. This should // Sends queries to terminal to detect certain features. This should
// _always_ be called, but is left to the application to decide when // _always_ be called, but is left to the application to decide when
try vx.queryTerminal(); try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
try vx.setMouseMode(true); try vx.setMouseMode(true);
@ -78,8 +75,6 @@ pub fn main() !void {
break; break;
} else if (key.matches('l', .{ .ctrl = true })) { } else if (key.matches('l', .{ .ctrl = true })) {
vx.queueRefresh(); vx.queueRefresh();
} else if (key.matches('n', .{ .ctrl = true })) {
try vx.notify("vaxis", "hello from vaxis");
} else if (key.matches('z', .{ .ctrl = true })) { } else if (key.matches('z', .{ .ctrl = true })) {
try openDirVim(alloc, &vx, "examples"); try openDirVim(alloc, &vx, "examples");
} else { } else {
@ -102,7 +97,7 @@ pub fn main() !void {
// more than one byte will incur an allocation on the first render // more than one byte will incur an allocation on the first render
// after it is drawn. Thereafter, it will not allocate unless the // after it is drawn. Thereafter, it will not allocate unless the
// screen is resized // screen is resized
.winsize => |ws| try vx.resize(alloc, ws), .winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
else => {}, else => {},
} }
@ -128,7 +123,7 @@ pub fn main() !void {
text_input.draw(border.all(child, style)); text_input.draw(border.all(child, style));
// Render the screen // Render the screen
try vx.render(); try vx.render(tty.anyWriter());
} }
} }

View file

@ -1,75 +0,0 @@
const std = @import("std");
const vaxis = @import("vaxis");
const Cell = vaxis.Cell;
const TextInput = vaxis.widgets.TextInput;
const border = vaxis.widgets.border;
const log = std.log.scoped(.main);
const Event = union(enum) {
key_press: vaxis.Key,
winsize: vaxis.Winsize,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
const deinit_status = gpa.deinit();
//fail test; can't try in defer as defer is executed after we return
if (deinit_status == .leak) {
log.err("memory leak", .{});
}
}
const alloc = gpa.allocator();
var line: ?[]const u8 = null;
defer {
// do this in defer so that vaxis cleans up terminal state before we
// print to stdout
if (line) |_| {
const stdout = std.io.getStdOut().writer();
stdout.print("\n{s}\n", .{line.?}) catch {};
alloc.free(line.?);
}
}
var vx = try vaxis.init(alloc, .{});
defer vx.deinit(alloc);
var loop: vaxis.Loop(Event) = .{ .vaxis = &vx };
try loop.run();
defer loop.stop();
var text_input = TextInput.init(alloc, &vx.unicode);
defer text_input.deinit();
try vx.queryTerminal();
const prompt: vaxis.Segment = .{ .text = "$ " };
while (true) {
const event = loop.nextEvent();
switch (event) {
.key_press => |key| {
if (key.matches('c', .{ .ctrl = true })) {
break;
} else if (key.matches(vaxis.Key.enter, .{})) {
line = try text_input.toOwnedSlice();
text_input.clearAndFree();
break;
} else {
try text_input.update(.{ .key_press = key });
}
},
.winsize => |ws| try vx.resize(alloc, ws),
}
const win = vx.window();
win.clear();
_ = try win.printSegment(prompt, .{});
const input_win = win.child(.{ .x_off = 2 });
text_input.draw(input_win);
try vx.render();
}
}

View file

@ -1,126 +0,0 @@
const std = @import("std");
const vaxis = @import("vaxis");
const Cell = vaxis.Cell;
const TextInput = vaxis.widgets.TextInput;
const log = std.log.scoped(.main);
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
const deinit_status = gpa.deinit();
if (deinit_status == .leak) {
log.err("memory leak", .{});
}
}
const alloc = gpa.allocator();
var vx = try vaxis.init(alloc, .{});
defer vx.deinit(alloc);
var loop: vaxis.Loop(Event) = .{ .vaxis = &vx };
try loop.run();
defer loop.stop();
try vx.queryTerminal();
var text_input = TextInput.init(alloc, &vx.unicode);
defer text_input.deinit();
var selected_option: ?usize = null;
const options = [_][]const u8{
"yes",
"no",
};
// The main event loop. Vaxis provides a thread safe, blocking, buffered
// queue which can serve as the primary event queue for an application
while (true) {
// nextEvent blocks until an event is in the queue
const event = loop.nextEvent();
// exhaustive switching ftw. Vaxis will send events if your Event
// enum has the fields for those events (ie "key_press", "winsize")
switch (event) {
.key_press => |key| {
if (key.codepoint == 'c' and key.mods.ctrl) {
break;
} else if (key.matches(vaxis.Key.tab, .{})) {
if (selected_option == null) {
selected_option = 0;
} else {
selected_option.? = @min(options.len - 1, selected_option.? + 1);
}
} else if (key.matches(vaxis.Key.tab, .{ .shift = true })) {
if (selected_option == null) {
selected_option = 0;
} else {
selected_option.? = selected_option.? -| 1;
}
} else if (key.matches(vaxis.Key.enter, .{})) {
if (selected_option) |i| {
log.err("enter", .{});
try text_input.insertSliceAtCursor(options[i]);
selected_option = null;
}
} else {
if (selected_option == null)
try text_input.update(.{ .key_press = key });
}
},
.winsize => |ws| {
try vx.resize(alloc, ws);
},
else => {},
}
const win = vx.window();
win.clear();
const right_win = win.child(.{
.x_off = win.width - 4,
});
const left_win = win.child(.{
.width = .{ .limit = 8 },
});
var right_prompt = [_]vaxis.Segment{
.{ .text = "👩‍🚀🏳️‍🌈" },
};
var left_prompt = [_]vaxis.Segment{
.{ .text = "👩‍🚀🏳️‍🌈~ ", .style = .{ .fg = .{ .index = 4 } } },
.{ .text = "", .style = .{ .fg = .{ .index = 5 } } },
};
_ = try right_win.print(&right_prompt, .{});
_ = try left_win.print(&left_prompt, .{});
const input_win = win.child(.{
.x_off = 8,
.width = .{ .limit = win.width - 12 },
});
text_input.draw(input_win);
if (selected_option) |i| {
win.hideCursor();
for (options, 0..) |opt, j| {
log.err("i = {d}, j = {d}, opt = {s}", .{ i, j, opt });
var seg = [_]vaxis.Segment{.{
.text = opt,
.style = if (j == i) .{ .reverse = true } else .{},
}};
_ = try win.print(&seg, .{ .row_offset = j + 1 });
}
}
try vx.render();
}
}
// Our Event. This can contain internal events as well as Vaxis events.
// Internal events can be posted into the same queue as vaxis events to allow
// for a single event loop with exhaustive switching. Booya
const Event = union(enum) {
key_press: vaxis.Key,
winsize: vaxis.Winsize,
focus_in,
foo: u8,
};

View file

@ -24,18 +24,20 @@ pub fn main() !void {
const user_list = std.ArrayList(User).fromOwnedSlice(alloc, users_buf); const user_list = std.ArrayList(User).fromOwnedSlice(alloc, users_buf);
defer user_list.deinit(); defer user_list.deinit();
var tty = try vaxis.Tty.init();
var vx = try vaxis.init(alloc, .{}); var vx = try vaxis.init(alloc, .{});
defer vx.deinit(alloc); defer vx.deinit(alloc, tty.anyWriter());
var loop: vaxis.Loop(union(enum) { var loop: vaxis.Loop(union(enum) {
key_press: vaxis.Key, key_press: vaxis.Key,
winsize: vaxis.Winsize, winsize: vaxis.Winsize,
}) = .{ .vaxis = &vx }; }) = .{ .tty = &tty, .vaxis = &vx };
try loop.run(); try loop.start();
defer loop.stop(); defer loop.stop();
try vx.enterAltScreen(); try vx.enterAltScreen(tty.anyWriter());
try vx.queryTerminal(); try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
const logo = const logo =
\\░█░█░█▀█░█░█░▀█▀░█▀▀░░░▀█▀░█▀█░█▀▄░█░░░█▀▀░ \\░█░█░█▀█░█░█░▀█▀░█▀▀░░░▀█▀░█▀█░█▀▄░█░░░█▀▀░
@ -145,7 +147,7 @@ pub fn main() !void {
} }
moving = false; moving = false;
}, },
.winsize => |ws| try vx.resize(alloc, ws), .winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
//else => {}, //else => {},
} }
@ -203,7 +205,7 @@ pub fn main() !void {
cmd_input.draw(bottom_bar); cmd_input.draw(bottom_bar);
// Render the screen // Render the screen
try vx.render(); try vx.render(tty.anyWriter());
} }
} }

View file

@ -40,7 +40,7 @@ pub fn main() !void {
// Initialize Vaxis // Initialize Vaxis
var vx = try vaxis.init(alloc, .{}); var vx = try vaxis.init(alloc, .{});
defer vx.deinit(tty.anyWriter(), alloc); defer vx.deinit(alloc, tty.anyWriter());
var loop: vaxis.Loop(Event) = .{ var loop: vaxis.Loop(Event) = .{
.vaxis = &vx, .vaxis = &vx,
@ -120,7 +120,7 @@ pub fn main() !void {
// more than one byte will incur an allocation on the first render // more than one byte will incur an allocation on the first render
// after it is drawn. Thereafter, it will not allocate unless the // after it is drawn. Thereafter, it will not allocate unless the
// screen is resized // screen is resized
.winsize => |ws| try vx.resize(alloc, ws, tty.anyWriter()), .winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
else => {}, else => {},
} }

View file

@ -18,16 +18,18 @@ pub fn main() !void {
} }
const alloc = gpa.allocator(); const alloc = gpa.allocator();
var tty = try vaxis.Tty.init();
var vx = try vaxis.init(alloc, .{}); var vx = try vaxis.init(alloc, .{});
defer vx.deinit(alloc); defer vx.deinit(alloc, tty.anyWriter());
var loop: vaxis.Loop(Event) = .{ .vaxis = &vx }; var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
try loop.init();
try loop.run(); try loop.start();
defer loop.stop(); defer loop.stop();
try vx.enterAltScreen(); try vx.enterAltScreen(tty.anyWriter());
try vx.queryTerminal(); try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
const lower_limit = 30; const lower_limit = 30;
var color_idx: u8 = lower_limit; var color_idx: u8 = lower_limit;
@ -42,7 +44,7 @@ pub fn main() !void {
switch (event) { switch (event) {
.key_press => |key| if (key.matches('c', .{ .ctrl = true })) return, .key_press => |key| if (key.matches('c', .{ .ctrl = true })) return,
.winsize => |ws| { .winsize => |ws| {
try vx.resize(alloc, ws); try vx.resize(alloc, tty.anyWriter(), ws);
break; break;
}, },
} }
@ -52,7 +54,7 @@ pub fn main() !void {
while (loop.tryEvent()) |event| { while (loop.tryEvent()) |event| {
switch (event) { switch (event) {
.key_press => |key| if (key.matches('c', .{ .ctrl = true })) return, .key_press => |key| if (key.matches('c', .{ .ctrl = true })) return,
.winsize => |ws| try vx.resize(alloc, ws), .winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
} }
} }
@ -67,7 +69,7 @@ pub fn main() !void {
}; };
const center = vaxis.widgets.alignment.center(win, 28, 4); const center = vaxis.widgets.alignment.center(win, 28, 4);
_ = try center.printSegment(segment, .{ .wrap = .grapheme }); _ = try center.printSegment(segment, .{ .wrap = .grapheme });
try vx.render(); try vx.render(tty.anyWriter());
std.time.sleep(8 * std.time.ns_per_ms); std.time.sleep(8 * std.time.ns_per_ms);
switch (dir) { switch (dir) {
.up => { .up => {

View file

@ -9,7 +9,6 @@ const InternalScreen = @import("InternalScreen.zig");
const Key = @import("Key.zig"); const Key = @import("Key.zig");
const Mouse = @import("Mouse.zig"); const Mouse = @import("Mouse.zig");
const Screen = @import("Screen.zig"); const Screen = @import("Screen.zig");
const tty = @import("tty.zig");
const Unicode = @import("Unicode.zig"); const Unicode = @import("Unicode.zig");
const Window = @import("Window.zig"); const Window = @import("Window.zig");
@ -18,7 +17,7 @@ const Hyperlink = Cell.Hyperlink;
const KittyFlags = Key.KittyFlags; const KittyFlags = Key.KittyFlags;
const Shape = Mouse.Shape; const Shape = Mouse.Shape;
const Style = Cell.Style; const Style = Cell.Style;
const Winsize = tty.Winsize; const Winsize = @import("tty.zig").Winsize;
const ctlseqs = @import("ctlseqs.zig"); const ctlseqs = @import("ctlseqs.zig");
const gwidth = @import("gwidth.zig"); const gwidth = @import("gwidth.zig");
@ -57,6 +56,7 @@ opts: Options = .{},
/// if we should redraw the entire screen on the next render /// if we should redraw the entire screen on the next render
refresh: bool = false, refresh: bool = false,
// FIXME: remove before committing
/// blocks the main thread until a DA1 query has been received, or the /// blocks the main thread until a DA1 query has been received, or the
/// futex times out /// futex times out
query_futex: atomic.Value(u32) = atomic.Value(u32).init(0), query_futex: atomic.Value(u32) = atomic.Value(u32).init(0),
@ -106,40 +106,49 @@ pub fn init(alloc: std.mem.Allocator, opts: Options) !Vaxis {
/// passed, this will free resources associated with Vaxis. This is left as an /// passed, this will free resources associated with Vaxis. This is left as an
/// optional so applications can choose to not free resources when the /// optional so applications can choose to not free resources when the
/// application will be exiting anyways /// application will be exiting anyways
pub fn deinit(self: *Vaxis, writer: AnyWriter, alloc: ?std.mem.Allocator) void { pub fn deinit(self: *Vaxis, alloc: ?std.mem.Allocator, tty: AnyWriter) void {
self.resetState(writer) catch {}; self.resetState(tty) catch {};
// always show the cursor on exit // always show the cursor on exit
writer.writeAll(ctlseqs.show_cursor) catch {}; tty.writeAll(ctlseqs.show_cursor) catch {};
if (alloc) |a| { if (alloc) |a| {
self.screen.deinit(a); self.screen.deinit(a);
self.screen_last.deinit(a); self.screen_last.deinit(a);
} }
if (self.renders > 0) { if (self.renders > 0) {
const tpr = @divTrunc(self.render_dur, self.renders); const tpr = @divTrunc(self.render_dur, self.renders);
log.debug("total renders = {d}", .{self.renders}); log.debug("total renders = {d}\r", .{self.renders});
log.debug("microseconds per render = {d}", .{tpr}); log.debug("microseconds per render = {d}\r", .{tpr});
} }
self.unicode.deinit(); self.unicode.deinit();
} }
/// resets enabled features /// resets enabled features, sends cursor to home and clears below cursor
pub fn resetState(self: *Vaxis, writer: AnyWriter) !void { pub fn resetState(self: *Vaxis, tty: AnyWriter) !void {
if (self.state.kitty_keyboard) { if (self.state.kitty_keyboard) {
try writer.writeAll(ctlseqs.csi_u_pop); try tty.writeAll(ctlseqs.csi_u_pop);
self.state.kitty_keyboard = false; self.state.kitty_keyboard = false;
} }
if (self.state.mouse) { if (self.state.mouse) {
try self.setMouseMode(writer, false); try self.setMouseMode(tty, false);
} }
if (self.state.bracketed_paste) { if (self.state.bracketed_paste) {
try self.setBracketedPaste(writer, false); try self.setBracketedPaste(tty, false);
} }
if (self.state.alt_screen) { if (self.state.alt_screen) {
try self.exitAltScreen(writer); try tty.writeAll(ctlseqs.home);
try tty.writeAll(ctlseqs.erase_below_cursor);
try self.exitAltScreen(tty);
} else {
try tty.writeByte('\r');
var i: usize = 0;
while (i < self.state.cursor.row) : (i += 1) {
try tty.writeAll(ctlseqs.ri);
}
try tty.writeAll(ctlseqs.erase_below_cursor);
} }
if (self.state.color_scheme_updates) { if (self.state.color_scheme_updates) {
try writer.writeAll(ctlseqs.color_scheme_reset); try tty.writeAll(ctlseqs.color_scheme_reset);
self.state.color_scheme_updates = false; self.state.color_scheme_updates = false;
} }
} }
@ -148,7 +157,12 @@ pub fn resetState(self: *Vaxis, writer: AnyWriter) !void {
/// required to display the screen (ie width x height). Any previous screen is /// 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 /// freed when resizing. The cursor will be sent to it's home position and a
/// hardware clear-below-cursor will be sent /// hardware clear-below-cursor will be sent
pub fn resize(self: *Vaxis, alloc: std.mem.Allocator, winsize: Winsize, writer: AnyWriter) !void { pub fn resize(
self: *Vaxis,
alloc: std.mem.Allocator,
tty: AnyWriter,
winsize: Winsize,
) !void {
log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows }); log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows });
self.screen.deinit(alloc); self.screen.deinit(alloc);
self.screen = try Screen.init(alloc, winsize, &self.unicode); self.screen = try Screen.init(alloc, winsize, &self.unicode);
@ -159,15 +173,15 @@ pub fn resize(self: *Vaxis, alloc: std.mem.Allocator, winsize: Winsize, writer:
self.screen_last.deinit(alloc); self.screen_last.deinit(alloc);
self.screen_last = try InternalScreen.init(alloc, winsize.cols, winsize.rows); self.screen_last = try InternalScreen.init(alloc, winsize.cols, winsize.rows);
if (self.state.alt_screen) if (self.state.alt_screen)
try writer.writeAll(ctlseqs.home) try tty.writeAll(ctlseqs.home)
else { else {
try writer.writeByte('\r'); try tty.writeByte('\r');
var i: usize = 0; var i: usize = 0;
while (i < self.state.cursor.row) : (i += 1) { while (i < self.state.cursor.row) : (i += 1) {
try writer.writeAll(ctlseqs.ri); try tty.writeAll(ctlseqs.ri);
} }
} }
try writer.writeAll(ctlseqs.erase_below_cursor); try tty.writeAll(ctlseqs.erase_below_cursor);
} }
/// returns a Window comprising of the entire terminal screen /// returns a Window comprising of the entire terminal screen
@ -183,33 +197,34 @@ pub fn window(self: *Vaxis) Window {
/// enter the alternate screen. The alternate screen will automatically /// enter the alternate screen. The alternate screen will automatically
/// be exited if calling deinit while in the alt screen /// be exited if calling deinit while in the alt screen
pub fn enterAltScreen(self: *Vaxis, writer: AnyWriter) !void { pub fn enterAltScreen(self: *Vaxis, tty: AnyWriter) !void {
try writer.writeAll(ctlseqs.smcup); try tty.writeAll(ctlseqs.smcup);
self.state.alt_screen = true; self.state.alt_screen = true;
} }
/// exit the alternate screen /// exit the alternate screen
pub fn exitAltScreen(self: *Vaxis, writer: AnyWriter) !void { pub fn exitAltScreen(self: *Vaxis, tty: AnyWriter) !void {
try writer.writeAll(ctlseqs.rmcup); try tty.writeAll(ctlseqs.rmcup);
self.state.alt_screen = false; self.state.alt_screen = false;
} }
/// write queries to the terminal to determine capabilities. Individual /// write queries to the terminal to determine capabilities. Individual
/// capabilities will be delivered to the client and possibly intercepted by /// capabilities will be delivered to the client and possibly intercepted by
/// Vaxis to enable features /// Vaxis to enable features.
pub fn queryTerminal(self: *Vaxis) !void { ///
try self.queryTerminalSend(); /// This call will block until Vaxis.query_futex is woken up, or the timeout.
/// Event loops can wake up this futex when cap_da1 is received
pub fn queryTerminal(self: *Vaxis, tty: AnyWriter, timeout_ns: u64) !void {
try self.queryTerminalSend(tty);
// 1 second timeout // 1 second timeout
std.Thread.Futex.timedWait(&self.query_futex, 0, 1 * std.time.ns_per_s) catch {}; std.Thread.Futex.timedWait(&self.query_futex, 0, timeout_ns) catch {};
try self.enableDetectedFeatures(tty);
try self.enableDetectedFeatures();
} }
/// write queries to the terminal to determine capabilities. This function /// write queries to the terminal to determine capabilities. This function
/// is only for use with a custom main loop. Call Vaxis.queryTerminal() if /// is only for use with a custom main loop. Call Vaxis.queryTerminal() if
/// you are using Loop.run() /// you are using Loop.run()
pub fn queryTerminalSend(_: Vaxis, writer: AnyWriter) !void { pub fn queryTerminalSend(_: Vaxis, tty: AnyWriter) !void {
// TODO: re-enable this // TODO: re-enable this
// const colorterm = std.posix.getenv("COLORTERM") orelse ""; // const colorterm = std.posix.getenv("COLORTERM") orelse "";
@ -221,30 +236,24 @@ pub fn queryTerminalSend(_: Vaxis, writer: AnyWriter) !void {
// } // }
// } // }
// TODO: XTGETTCAP queries ("RGB", "Smulx")
// TODO: decide if we actually want to query for focus and sync. It // TODO: decide if we actually want to query for focus and sync. It
// doesn't hurt to blindly use them // doesn't hurt to blindly use them
// _ = try tty.write(ctlseqs.decrqm_focus); // _ = try tty.write(ctlseqs.decrqm_focus);
// _ = try tty.write(ctlseqs.decrqm_sync); // _ = try tty.write(ctlseqs.decrqm_sync);
try writer.writeAll(ctlseqs.decrqm_sgr_pixels); try tty.writeAll(ctlseqs.decrqm_sgr_pixels ++
try writer.writeAll(ctlseqs.decrqm_unicode); ctlseqs.decrqm_unicode ++
try writer.writeAll(ctlseqs.decrqm_color_scheme); ctlseqs.decrqm_color_scheme ++
// TODO: XTVERSION has a DCS response. uncomment when we can parse ctlseqs.xtversion ++
// that ctlseqs.csi_u_query ++
// _ = try tty.write(ctlseqs.xtversion); ctlseqs.kitty_graphics_query ++
try writer.writeAll(ctlseqs.csi_u_query); ctlseqs.primary_device_attrs);
try writer.writeAll(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 writer.writeAll(ctlseqs.primary_device_attrs);
} }
/// Enable features detected by responses to queryTerminal. This function /// Enable features detected by responses to queryTerminal. This function
/// is only for use with a custom main loop. Call Vaxis.queryTerminal() if /// is only for use with a custom main loop. Call Vaxis.queryTerminal() if
/// you are using Loop.run() /// you are using Loop.run()
pub fn enableDetectedFeatures(self: *Vaxis) !void { pub fn enableDetectedFeatures(self: *Vaxis, tty: AnyWriter) !void {
// Apply any environment variables // Apply any environment variables
if (std.posix.getenv("ASCIINEMA_REC")) |_| if (std.posix.getenv("ASCIINEMA_REC")) |_|
self.sgr = .legacy; self.sgr = .legacy;
@ -263,10 +272,10 @@ pub fn enableDetectedFeatures(self: *Vaxis) !void {
// enable detected features // enable detected features
if (self.caps.kitty_keyboard) { if (self.caps.kitty_keyboard) {
try self.enableKittyKeyboard(self.opts.kitty_keyboard_flags); try self.enableKittyKeyboard(tty, self.opts.kitty_keyboard_flags);
} }
if (self.caps.unicode == .unicode) { if (self.caps.unicode == .unicode) {
_ = try tty.write(ctlseqs.unicode_set); try tty.writeAll(ctlseqs.unicode_set);
} }
} }
@ -276,7 +285,7 @@ pub fn queueRefresh(self: *Vaxis) void {
} }
/// draws the screen to the terminal /// draws the screen to the terminal
pub fn render(self: *Vaxis, writer: AnyWriter) !void { pub fn render(self: *Vaxis, tty: AnyWriter) !void {
self.renders += 1; self.renders += 1;
self.render_timer.reset(); self.render_timer.reset();
defer { defer {
@ -289,24 +298,21 @@ pub fn render(self: *Vaxis, writer: AnyWriter) !void {
// TODO: optimize sync so we only sync _when we have changes_. This // TODO: optimize sync so we only sync _when we have changes_. This
// requires a smarter buffered writer, we'll probably have to write // requires a smarter buffered writer, we'll probably have to write
// our own // our own
try writer.writeAll(ctlseqs.sync_set); try tty.writeAll(ctlseqs.sync_set);
defer writer.writeAll(ctlseqs.sync_reset) catch {}; defer tty.writeAll(ctlseqs.sync_reset) catch {};
// Send the cursor to 0,0 // Send the cursor to 0,0
// TODO: this needs to move after we optimize writes. We only do // 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 // this if we have an update to make. We also need to hide cursor
// and then reshow it if needed // and then reshow it if needed
try writer.writeAll(ctlseqs.hide_cursor); try tty.writeAll(ctlseqs.hide_cursor);
if (self.state.alt_screen) if (self.state.alt_screen)
try writer.writeAll(ctlseqs.home) try tty.writeAll(ctlseqs.home)
else { else {
try writer.writeAll("\r"); try tty.writeByte('\r');
var i: usize = 0; try tty.writeBytesNTimes(ctlseqs.ri, self.state.cursor.row);
while (i < self.state.cursor.row) : (i += 1) {
try writer.writeAll(ctlseqs.ri);
} }
} try tty.writeAll(ctlseqs.sgr_reset);
try writer.writeAll(ctlseqs.sgr_reset);
// initialize some variables // initialize some variables
var reposition: bool = false; var reposition: bool = false;
@ -321,7 +327,7 @@ pub fn render(self: *Vaxis, writer: AnyWriter) !void {
// Clear all images // Clear all images
if (self.caps.kitty_graphics) if (self.caps.kitty_graphics)
try writer.writeAll(ctlseqs.kitty_graphics_clear); try tty.writeAll(ctlseqs.kitty_graphics_clear);
var i: usize = 0; var i: usize = 0;
while (i < self.screen.buf.len) { while (i < self.screen.buf.len) {
@ -357,7 +363,7 @@ pub fn render(self: *Vaxis, writer: AnyWriter) !void {
// Close any osc8 sequence we might be in before // Close any osc8 sequence we might be in before
// repositioning // repositioning
if (link.uri.len > 0) { if (link.uri.len > 0) {
try writer.writeAll(ctlseqs.osc8_clear); try tty.writeAll(ctlseqs.osc8_clear);
} }
continue; continue;
} }
@ -373,52 +379,53 @@ pub fn render(self: *Vaxis, writer: AnyWriter) !void {
if (reposition) { if (reposition) {
reposition = false; reposition = false;
if (self.state.alt_screen) if (self.state.alt_screen)
try writer.print(ctlseqs.cup, .{ row + 1, col + 1 }) try tty.print(ctlseqs.cup, .{ row + 1, col + 1 })
else { else {
if (cursor_pos.row == row) { if (cursor_pos.row == row) {
const n = col - cursor_pos.col; const n = col - cursor_pos.col;
try writer.print(ctlseqs.cuf, .{n}); if (n > 0)
try tty.print(ctlseqs.cuf, .{n});
} else { } else {
try writer.writeByte('\r'); try tty.writeByte('\r');
const n = row - cursor_pos.row; const n = row - cursor_pos.row;
try writer.writeByteNTimes('\n', n); try tty.writeByteNTimes('\n', n);
}
if (col > 0) if (col > 0)
try writer.print(ctlseqs.cuf, .{col}); try tty.print(ctlseqs.cuf, .{col});
}
} }
} }
if (cell.image) |img| { if (cell.image) |img| {
try writer.print( try tty.print(
ctlseqs.kitty_graphics_preamble, ctlseqs.kitty_graphics_preamble,
.{img.img_id}, .{img.img_id},
); );
if (img.options.pixel_offset) |offset| { if (img.options.pixel_offset) |offset| {
try writer.print( try tty.print(
",X={d},Y={d}", ",X={d},Y={d}",
.{ offset.x, offset.y }, .{ offset.x, offset.y },
); );
} }
if (img.options.clip_region) |clip| { if (img.options.clip_region) |clip| {
if (clip.x) |x| if (clip.x) |x|
try writer.print(",x={d}", .{x}); try tty.print(",x={d}", .{x});
if (clip.y) |y| if (clip.y) |y|
try writer.print(",y={d}", .{y}); try tty.print(",y={d}", .{y});
if (clip.width) |width| if (clip.width) |width|
try writer.print(",w={d}", .{width}); try tty.print(",w={d}", .{width});
if (clip.height) |height| if (clip.height) |height|
try writer.print(",h={d}", .{height}); try tty.print(",h={d}", .{height});
} }
if (img.options.size) |size| { if (img.options.size) |size| {
if (size.rows) |rows| if (size.rows) |rows|
try writer.print(",r={d}", .{rows}); try tty.print(",r={d}", .{rows});
if (size.cols) |cols| if (size.cols) |cols|
try writer.print(",c={d}", .{cols}); try tty.print(",c={d}", .{cols});
} }
if (img.options.z_index) |z| { if (img.options.z_index) |z| {
try writer.print(",z={d}", .{z}); try tty.print(",z={d}", .{z});
} }
try writer.writeAll(ctlseqs.kitty_graphics_closing); try tty.writeAll(ctlseqs.kitty_graphics_closing);
} }
// something is different, so let's loop through everything and // something is different, so let's loop through everything and
@ -427,23 +434,23 @@ pub fn render(self: *Vaxis, writer: AnyWriter) !void {
// foreground // foreground
if (!Cell.Color.eql(cursor.fg, cell.style.fg)) { if (!Cell.Color.eql(cursor.fg, cell.style.fg)) {
switch (cell.style.fg) { switch (cell.style.fg) {
.default => try writer.writeAll(ctlseqs.fg_reset), .default => try tty.writeAll(ctlseqs.fg_reset),
.index => |idx| { .index => |idx| {
switch (idx) { switch (idx) {
0...7 => try writer.print(ctlseqs.fg_base, .{idx}), 0...7 => try tty.print(ctlseqs.fg_base, .{idx}),
8...15 => try writer.print(ctlseqs.fg_bright, .{idx - 8}), 8...15 => try tty.print(ctlseqs.fg_bright, .{idx - 8}),
else => { else => {
switch (self.sgr) { switch (self.sgr) {
.standard => try writer.print(ctlseqs.fg_indexed, .{idx}), .standard => try tty.print(ctlseqs.fg_indexed, .{idx}),
.legacy => try writer.print(ctlseqs.fg_indexed_legacy, .{idx}), .legacy => try tty.print(ctlseqs.fg_indexed_legacy, .{idx}),
} }
}, },
} }
}, },
.rgb => |rgb| { .rgb => |rgb| {
switch (self.sgr) { switch (self.sgr) {
.standard => try writer.print(ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }), .standard => try tty.print(ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }),
.legacy => try writer.print(ctlseqs.fg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), .legacy => try tty.print(ctlseqs.fg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }),
} }
}, },
} }
@ -451,23 +458,23 @@ pub fn render(self: *Vaxis, writer: AnyWriter) !void {
// background // background
if (!Cell.Color.eql(cursor.bg, cell.style.bg)) { if (!Cell.Color.eql(cursor.bg, cell.style.bg)) {
switch (cell.style.bg) { switch (cell.style.bg) {
.default => try writer.writeAll(ctlseqs.bg_reset), .default => try tty.writeAll(ctlseqs.bg_reset),
.index => |idx| { .index => |idx| {
switch (idx) { switch (idx) {
0...7 => try writer.print(ctlseqs.bg_base, .{idx}), 0...7 => try tty.print(ctlseqs.bg_base, .{idx}),
8...15 => try writer.print(ctlseqs.bg_bright, .{idx - 8}), 8...15 => try tty.print(ctlseqs.bg_bright, .{idx - 8}),
else => { else => {
switch (self.sgr) { switch (self.sgr) {
.standard => try writer.print(ctlseqs.bg_indexed, .{idx}), .standard => try tty.print(ctlseqs.bg_indexed, .{idx}),
.legacy => try writer.print(ctlseqs.bg_indexed_legacy, .{idx}), .legacy => try tty.print(ctlseqs.bg_indexed_legacy, .{idx}),
} }
}, },
} }
}, },
.rgb => |rgb| { .rgb => |rgb| {
switch (self.sgr) { switch (self.sgr) {
.standard => try writer.print(ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }), .standard => try tty.print(ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }),
.legacy => try writer.print(ctlseqs.bg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), .legacy => try tty.print(ctlseqs.bg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }),
} }
}, },
} }
@ -475,17 +482,17 @@ pub fn render(self: *Vaxis, writer: AnyWriter) !void {
// underline color // underline color
if (!Cell.Color.eql(cursor.ul, cell.style.ul)) { if (!Cell.Color.eql(cursor.ul, cell.style.ul)) {
switch (cell.style.bg) { switch (cell.style.bg) {
.default => try writer.writeAll(ctlseqs.ul_reset), .default => try tty.writeAll(ctlseqs.ul_reset),
.index => |idx| { .index => |idx| {
switch (self.sgr) { switch (self.sgr) {
.standard => try writer.print(ctlseqs.ul_indexed, .{idx}), .standard => try tty.print(ctlseqs.ul_indexed, .{idx}),
.legacy => try writer.print(ctlseqs.ul_indexed_legacy, .{idx}), .legacy => try tty.print(ctlseqs.ul_indexed_legacy, .{idx}),
} }
}, },
.rgb => |rgb| { .rgb => |rgb| {
switch (self.sgr) { switch (self.sgr) {
.standard => try writer.print(ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }), .standard => try tty.print(ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }),
.legacy => try writer.print(ctlseqs.ul_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), .legacy => try tty.print(ctlseqs.ul_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }),
} }
}, },
} }
@ -500,7 +507,7 @@ pub fn render(self: *Vaxis, writer: AnyWriter) !void {
.dotted => ctlseqs.ul_dotted, .dotted => ctlseqs.ul_dotted,
.dashed => ctlseqs.ul_dashed, .dashed => ctlseqs.ul_dashed,
}; };
try writer.writeAll(seq); try tty.writeAll(seq);
} }
// bold // bold
if (cursor.bold != cell.style.bold) { if (cursor.bold != cell.style.bold) {
@ -508,9 +515,9 @@ pub fn render(self: *Vaxis, writer: AnyWriter) !void {
true => ctlseqs.bold_set, true => ctlseqs.bold_set,
false => ctlseqs.bold_dim_reset, false => ctlseqs.bold_dim_reset,
}; };
try writer.writeAll(seq); try tty.writeAll(seq);
if (cell.style.dim) { if (cell.style.dim) {
try writer.writeAll(ctlseqs.dim_set); try tty.writeAll(ctlseqs.dim_set);
} }
} }
// dim // dim
@ -519,9 +526,9 @@ pub fn render(self: *Vaxis, writer: AnyWriter) !void {
true => ctlseqs.dim_set, true => ctlseqs.dim_set,
false => ctlseqs.bold_dim_reset, false => ctlseqs.bold_dim_reset,
}; };
try writer.writeAll(seq); try tty.writeAll(seq);
if (cell.style.bold) { if (cell.style.bold) {
try writer.writeAll(ctlseqs.bold_set); try tty.writeAll(ctlseqs.bold_set);
} }
} }
// dim // dim
@ -530,7 +537,7 @@ pub fn render(self: *Vaxis, writer: AnyWriter) !void {
true => ctlseqs.italic_set, true => ctlseqs.italic_set,
false => ctlseqs.italic_reset, false => ctlseqs.italic_reset,
}; };
try writer.writeAll(seq); try tty.writeAll(seq);
} }
// dim // dim
if (cursor.blink != cell.style.blink) { if (cursor.blink != cell.style.blink) {
@ -538,7 +545,7 @@ pub fn render(self: *Vaxis, writer: AnyWriter) !void {
true => ctlseqs.blink_set, true => ctlseqs.blink_set,
false => ctlseqs.blink_reset, false => ctlseqs.blink_reset,
}; };
try writer.writeAll(seq); try tty.writeAll(seq);
} }
// reverse // reverse
if (cursor.reverse != cell.style.reverse) { if (cursor.reverse != cell.style.reverse) {
@ -546,7 +553,7 @@ pub fn render(self: *Vaxis, writer: AnyWriter) !void {
true => ctlseqs.reverse_set, true => ctlseqs.reverse_set,
false => ctlseqs.reverse_reset, false => ctlseqs.reverse_reset,
}; };
try writer.writeAll(seq); try tty.writeAll(seq);
} }
// invisible // invisible
if (cursor.invisible != cell.style.invisible) { if (cursor.invisible != cell.style.invisible) {
@ -554,7 +561,7 @@ pub fn render(self: *Vaxis, writer: AnyWriter) !void {
true => ctlseqs.invisible_set, true => ctlseqs.invisible_set,
false => ctlseqs.invisible_reset, false => ctlseqs.invisible_reset,
}; };
try writer.writeAll(seq); try tty.writeAll(seq);
} }
// strikethrough // strikethrough
if (cursor.strikethrough != cell.style.strikethrough) { if (cursor.strikethrough != cell.style.strikethrough) {
@ -562,7 +569,7 @@ pub fn render(self: *Vaxis, writer: AnyWriter) !void {
true => ctlseqs.strikethrough_set, true => ctlseqs.strikethrough_set,
false => ctlseqs.strikethrough_reset, false => ctlseqs.strikethrough_reset,
}; };
try writer.writeAll(seq); try tty.writeAll(seq);
} }
// url // url
@ -573,15 +580,15 @@ pub fn render(self: *Vaxis, writer: AnyWriter) !void {
// a url // a url
ps = ""; ps = "";
} }
try writer.print(ctlseqs.osc8, .{ ps, cell.link.uri }); try tty.print(ctlseqs.osc8, .{ ps, cell.link.uri });
} }
try writer.writeAll(cell.char.grapheme); try tty.writeAll(cell.char.grapheme);
cursor_pos.col = col + w; cursor_pos.col = col + w;
cursor_pos.row = row; cursor_pos.row = row;
} }
if (self.screen.cursor_vis) { if (self.screen.cursor_vis) {
if (self.state.alt_screen) { if (self.state.alt_screen) {
try writer.print( try tty.print(
ctlseqs.cup, ctlseqs.cup,
.{ .{
self.screen.cursor_row + 1, self.screen.cursor_row + 1,
@ -590,30 +597,30 @@ pub fn render(self: *Vaxis, writer: AnyWriter) !void {
); );
} else { } else {
// TODO: position cursor relative to current location // TODO: position cursor relative to current location
try writer.writeByte('\r'); try tty.writeByte('\r');
if (self.screen.cursor_row >= cursor_pos.row) if (self.screen.cursor_row >= cursor_pos.row)
try writer.writeByteNTimes('\n', self.screen.cursor_row - cursor_pos.row) try tty.writeByteNTimes('\n', self.screen.cursor_row - cursor_pos.row)
else else
try writer.writeBytesNTimes(ctlseqs.ri, cursor_pos.row - self.screen.cursor_row); try tty.writeBytesNTimes(ctlseqs.ri, cursor_pos.row - self.screen.cursor_row);
if (self.screen.cursor_col > 0) if (self.screen.cursor_col > 0)
try writer.print(ctlseqs.cuf, .{self.screen.cursor_col}); try tty.print(ctlseqs.cuf, .{self.screen.cursor_col});
} }
self.state.cursor.row = self.screen.cursor_row; self.state.cursor.row = self.screen.cursor_row;
self.state.cursor.col = self.screen.cursor_col; self.state.cursor.col = self.screen.cursor_col;
try writer.writeAll(ctlseqs.show_cursor); try tty.writeAll(ctlseqs.show_cursor);
} else { } else {
self.state.cursor.row = cursor_pos.row; self.state.cursor.row = cursor_pos.row;
self.state.cursor.col = cursor_pos.col; self.state.cursor.col = cursor_pos.col;
} }
if (self.screen.mouse_shape != self.screen_last.mouse_shape) { if (self.screen.mouse_shape != self.screen_last.mouse_shape) {
try writer.print( try tty.print(
ctlseqs.osc22_mouse_shape, ctlseqs.osc22_mouse_shape,
.{@tagName(self.screen.mouse_shape)}, .{@tagName(self.screen.mouse_shape)},
); );
self.screen_last.mouse_shape = self.screen.mouse_shape; self.screen_last.mouse_shape = self.screen.mouse_shape;
} }
if (self.screen.cursor_shape != self.screen_last.cursor_shape) { if (self.screen.cursor_shape != self.screen_last.cursor_shape) {
try writer.print( try tty.print(
ctlseqs.cursor_shape, ctlseqs.cursor_shape,
.{@intFromEnum(self.screen.cursor_shape)}, .{@intFromEnum(self.screen.cursor_shape)},
); );
@ -621,34 +628,34 @@ pub fn render(self: *Vaxis, writer: AnyWriter) !void {
} }
} }
fn enableKittyKeyboard(self: *Vaxis, writer: AnyWriter, flags: Key.KittyFlags) !void { fn enableKittyKeyboard(self: *Vaxis, tty: AnyWriter, flags: Key.KittyFlags) !void {
const flag_int: u5 = @bitCast(flags); const flag_int: u5 = @bitCast(flags);
try writer.print(ctlseqs.csi_u_push, .{flag_int}); try tty.print(ctlseqs.csi_u_push, .{flag_int});
self.state.kitty_keyboard = true; self.state.kitty_keyboard = true;
} }
/// send a system notification /// send a system notification
pub fn notify(_: *Vaxis, writer: AnyWriter, title: ?[]const u8, body: []const u8) !void { pub fn notify(_: *Vaxis, tty: AnyWriter, title: ?[]const u8, body: []const u8) !void {
if (title) |t| if (title) |t|
try writer.print(ctlseqs.osc777_notify, .{ t, body }) try tty.print(ctlseqs.osc777_notify, .{ t, body })
else else
try writer.print(ctlseqs.osc9_notify, .{body}); try tty.print(ctlseqs.osc9_notify, .{body});
} }
/// sets the window title /// sets the window title
pub fn setTitle(_: *Vaxis, writer: AnyWriter, title: []const u8) !void { pub fn setTitle(_: *Vaxis, tty: AnyWriter, title: []const u8) !void {
try writer.print(ctlseqs.osc2_set_title, .{title}); try tty.print(ctlseqs.osc2_set_title, .{title});
} }
// turn bracketed paste on or off. An event will be sent at the // turn bracketed paste on or off. An event will be sent at the
// beginning and end of a detected paste. All keystrokes between these // beginning and end of a detected paste. All keystrokes between these
// events were pasted // events were pasted
pub fn setBracketedPaste(self: *Vaxis, writer: AnyWriter, enable: bool) !void { pub fn setBracketedPaste(self: *Vaxis, tty: AnyWriter, enable: bool) !void {
const seq = if (enable) const seq = if (enable)
ctlseqs.bp_set ctlseqs.bp_set
else else
ctlseqs.bp_reset; ctlseqs.bp_reset;
try writer.writeAll(seq); try tty.writeAll(seq);
self.state.bracketed_paste = enable; self.state.bracketed_paste = enable;
} }
@ -658,19 +665,19 @@ pub fn setMouseShape(self: *Vaxis, shape: Shape) void {
} }
/// Change the mouse reporting mode /// Change the mouse reporting mode
pub fn setMouseMode(self: *Vaxis, writer: AnyWriter, enable: bool) !void { pub fn setMouseMode(self: *Vaxis, tty: AnyWriter, enable: bool) !void {
if (enable) { if (enable) {
self.state.mouse = true; self.state.mouse = true;
if (self.caps.sgr_pixels) { if (self.caps.sgr_pixels) {
log.debug("enabling mouse mode: pixel coordinates", .{}); log.debug("enabling mouse mode: pixel coordinates", .{});
self.state.pixel_mouse = true; self.state.pixel_mouse = true;
try writer.writeAll(ctlseqs.mouse_set_pixels); try tty.writeAll(ctlseqs.mouse_set_pixels);
} else { } else {
log.debug("enabling mouse mode: cell coordinates", .{}); log.debug("enabling mouse mode: cell coordinates", .{});
try writer.writeAll(ctlseqs.mouse_set); try tty.writeAll(ctlseqs.mouse_set);
} }
} else { } else {
try writer.writeAll(ctlseqs.mouse_reset); try tty.writeAll(ctlseqs.mouse_reset);
} }
} }
@ -709,7 +716,7 @@ pub fn translateMouse(self: Vaxis, mouse: Mouse) Mouse {
pub fn loadImage( pub fn loadImage(
self: *Vaxis, self: *Vaxis,
alloc: std.mem.Allocator, alloc: std.mem.Allocator,
writer: AnyWriter, tty: AnyWriter,
src: Image.Source, src: Image.Source,
) !Image { ) !Image {
if (!self.caps.kitty_graphics) return error.NoGraphicsCapability; if (!self.caps.kitty_graphics) return error.NoGraphicsCapability;
@ -730,7 +737,7 @@ pub fn loadImage(
const id = self.next_img_id; const id = self.next_img_id;
if (encoded.len < 4096) { if (encoded.len < 4096) {
try writer.print( try tty.print(
"\x1b_Gf=100,i={d};{s}\x1b\\", "\x1b_Gf=100,i={d};{s}\x1b\\",
.{ .{
id, id,
@ -740,14 +747,14 @@ pub fn loadImage(
} else { } else {
var n: usize = 4096; var n: usize = 4096;
try writer.print( try tty.print(
"\x1b_Gf=100,i={d},m=1;{s}\x1b\\", "\x1b_Gf=100,i={d},m=1;{s}\x1b\\",
.{ id, encoded[0..n] }, .{ id, encoded[0..n] },
); );
while (n < encoded.len) : (n += 4096) { while (n < encoded.len) : (n += 4096) {
const end: usize = @min(n + 4096, encoded.len); const end: usize = @min(n + 4096, encoded.len);
const m: u2 = if (end == encoded.len) 0 else 1; const m: u2 = if (end == encoded.len) 0 else 1;
try writer.print( try tty.print(
"\x1b_Gm={d};{s}\x1b\\", "\x1b_Gm={d};{s}\x1b\\",
.{ .{
m, m,
@ -764,28 +771,28 @@ pub fn loadImage(
} }
/// deletes an image from the terminal's memory /// deletes an image from the terminal's memory
pub fn freeImage(_: Vaxis, writer: AnyWriter, id: u32) void { pub fn freeImage(_: Vaxis, tty: AnyWriter, id: u32) void {
writer.print("\x1b_Ga=d,d=I,i={d};\x1b\\", .{id}) catch |err| { tty.print("\x1b_Ga=d,d=I,i={d};\x1b\\", .{id}) catch |err| {
log.err("couldn't delete image {d}: {}", .{ id, err }); log.err("couldn't delete image {d}: {}", .{ id, err });
return; return;
}; };
} }
pub fn copyToSystemClipboard(_: Vaxis, writer: AnyWriter, text: []const u8, encode_allocator: std.mem.Allocator) !void { pub fn copyToSystemClipboard(_: Vaxis, tty: AnyWriter, text: []const u8, encode_allocator: std.mem.Allocator) !void {
const encoder = std.base64.standard.Encoder; const encoder = std.base64.standard.Encoder;
const size = encoder.calcSize(text.len); const size = encoder.calcSize(text.len);
const buf = try encode_allocator.alloc(u8, size); const buf = try encode_allocator.alloc(u8, size);
const b64 = encoder.encode(buf, text); const b64 = encoder.encode(buf, text);
defer encode_allocator.free(buf); defer encode_allocator.free(buf);
try writer.print( try tty.print(
ctlseqs.osc52_clipboard_copy, ctlseqs.osc52_clipboard_copy,
.{b64}, .{b64},
); );
} }
pub fn requestSystemClipboard(self: Vaxis, writer: AnyWriter) !void { pub fn requestSystemClipboard(self: Vaxis, tty: AnyWriter) !void {
if (self.opts.system_clipboard_allocator == null) return error.NoClipboardAllocator; if (self.opts.system_clipboard_allocator == null) return error.NoClipboardAllocator;
try writer.print( try tty.print(
ctlseqs.osc52_clipboard_request, ctlseqs.osc52_clipboard_request,
.{}, .{},
); );
@ -794,12 +801,12 @@ pub fn requestSystemClipboard(self: Vaxis, writer: AnyWriter) !void {
/// Request a color report from the terminal. Note: not all terminals support /// Request a color report from the terminal. Note: not all terminals support
/// reporting colors. It is always safe to try, but you may not receive a /// reporting colors. It is always safe to try, but you may not receive a
/// response. /// response.
pub fn queryColor(_: Vaxis, writer: AnyWriter, kind: Cell.Color.Kind) !void { pub fn queryColor(_: Vaxis, tty: AnyWriter, kind: Cell.Color.Kind) !void {
switch (kind) { switch (kind) {
.fg => try writer.writeAll(ctlseqs.osc10_query), .fg => try tty.writeAll(ctlseqs.osc10_query),
.bg => try writer.writeAll(ctlseqs.osc11_query), .bg => try tty.writeAll(ctlseqs.osc11_query),
.cursor => try writer.writeAll(ctlseqs.osc12_query), .cursor => try tty.writeAll(ctlseqs.osc12_query),
.index => |idx| try writer.print(ctlseqs.osc4_query, .{idx}), .index => |idx| try tty.print(ctlseqs.osc4_query, .{idx}),
} }
} }
@ -808,12 +815,12 @@ pub fn queryColor(_: Vaxis, writer: AnyWriter, kind: Cell.Color.Kind) !void {
/// capability. Support can be detected by checking the value of /// capability. Support can be detected by checking the value of
/// vaxis.caps.color_scheme_updates. The initial scheme will be reported when /// vaxis.caps.color_scheme_updates. The initial scheme will be reported when
/// subscribing. /// subscribing.
pub fn subscribeToColorSchemeUpdates(self: Vaxis, writer: AnyWriter) !void { pub fn subscribeToColorSchemeUpdates(self: Vaxis, tty: AnyWriter) !void {
try writer.writeAll(ctlseqs.color_scheme_request); try tty.writeAll(ctlseqs.color_scheme_request);
try writer.writeAll(ctlseqs.color_scheme_set); try tty.writeAll(ctlseqs.color_scheme_set);
self.state.color_scheme_updates = true; self.state.color_scheme_updates = true;
} }
pub fn deviceStatusReport(_: Vaxis, writer: AnyWriter) !void { pub fn deviceStatusReport(_: Vaxis, tty: AnyWriter) !void {
try writer.writeAll(ctlseqs.device_status_report); try tty.writeAll(ctlseqs.device_status_report);
} }