use vaxis for tui

This commit is contained in:
Fabien Freling 2024-10-04 14:32:48 +02:00
parent 9c6bae6963
commit bd336e5ba8
3 changed files with 201 additions and 6 deletions

View file

@ -99,6 +99,12 @@ fn buildTui(b: *Build, target: ResolvedTarget, optimize: OptimizeMode) !void {
.optimize = optimize, .optimize = optimize,
}); });
const vaxis_dep = b.dependency("vaxis", .{
.target = target,
.optimize = optimize,
});
tui.root_module.addImport("vaxis", vaxis_dep.module("vaxis"));
b.installArtifact(tui); b.installArtifact(tui);
b.step("run-tui", "Run TUI").dependOn(&b.addRunArtifact(tui).step); b.step("run-tui", "Run TUI").dependOn(&b.addRunArtifact(tui).step);
} }

View file

@ -14,5 +14,9 @@
.cimgui = .{ .cimgui = .{
.path = "3rd-party/cimgui", .path = "3rd-party/cimgui",
}, },
.vaxis = .{
.url = "git+https://github.com/rockorager/libvaxis?ref=main#235e0bb27bc89655d8c66fc6378b31d4a98a117a",
.hash = "1220fef553676a4c90035db27c13ad609d5823198a94cc971e95ab9c680e3dcd2df0",
},
}, },
} }

View file

@ -1,11 +1,196 @@
// const std = @import("std");
// pub fn main() !void {
// const stdout = std.io.getStdOut().writer();
// try stdout.print("Can spawn process: {}\n", .{std.process.can_spawn});
// var gpa = std.heap.GeneralPurposeAllocator(.{}){};
// const alloc = gpa.allocator();
// var lldb_child = std.process.Child.init(&[_][]const u8{"lldb-vscode"}, alloc);
// _ = try lldb_child.spawnAndWait();
// }
const std = @import("std"); const std = @import("std");
const vaxis = @import("vaxis");
/// Set the default panic handler to the vaxis panic_handler. This will clean up the terminal if any
/// panics occur
pub const panic = vaxis.panic_handler;
/// Set some scope levels for the vaxis scopes
pub const std_options: std.Options = .{
.log_scope_levels = &.{
.{ .scope = .vaxis, .level = .warn },
.{ .scope = .vaxis_parser, .level = .warn },
},
};
/// Tagged union of all events our application will handle. These can be generated by Vaxis or your
/// own custom events
const Event = union(enum) {
key_press: vaxis.Key,
key_release: vaxis.Key,
mouse: vaxis.Mouse,
focus_in, // window has gained focus
focus_out, // window has lost focus
paste_start, // bracketed paste start
paste_end, // bracketed paste end
paste: []const u8, // osc 52 paste, caller must free
color_report: vaxis.Color.Report, // osc 4, 10, 11, 12 response
color_scheme: vaxis.Color.Scheme, // light / dark OS theme changes
winsize: vaxis.Winsize, // the window size has changed. This event is always sent when the loop
// is started
};
/// The application state
const MyApp = struct {
allocator: std.mem.Allocator,
// A flag for if we should quit
should_quit: bool,
/// The tty we are talking to
tty: vaxis.Tty,
/// The vaxis instance
vx: vaxis.Vaxis,
/// A mouse event that we will handle in the draw cycle
mouse: ?vaxis.Mouse,
pub fn init(allocator: std.mem.Allocator) !MyApp {
return .{
.allocator = allocator,
.should_quit = false,
.tty = try vaxis.Tty.init(),
.vx = try vaxis.init(allocator, .{}),
.mouse = null,
};
}
pub fn deinit(self: *MyApp) void {
// Deinit takes an optional allocator. You can choose to pass an allocator to clean up
// memory, or pass null if your application is shutting down and let the OS clean up the
// memory
self.vx.deinit(self.allocator, self.tty.anyWriter());
self.tty.deinit();
}
pub fn run(self: *MyApp) !void {
// Initialize our event loop. This particular loop requires intrusive init
var loop: vaxis.Loop(Event) = .{
.tty = &self.tty,
.vaxis = &self.vx,
};
try loop.init();
// Start the event loop. Events will now be queued
try loop.start();
try self.vx.enterAltScreen(self.tty.anyWriter());
// Query the terminal to detect advanced features, such as kitty keyboard protocol, etc.
// This will automatically enable the features in the screen you are in, so you will want to
// call it after entering the alt screen if you are a full screen application. The second
// arg is a timeout for the terminal to send responses. Typically the response will be very
// fast, however it could be slow on ssh connections.
try self.vx.queryTerminal(self.tty.anyWriter(), 1 * std.time.ns_per_s);
// Enable mouse events
try self.vx.setMouseMode(self.tty.anyWriter(), true);
// This is the main event loop. The basic structure is
// 1. Handle events
// 2. Draw application
// 3. Render
while (!self.should_quit) {
// pollEvent blocks until we have an event
loop.pollEvent();
// tryEvent returns events until the queue is empty
while (loop.tryEvent()) |event| {
try self.update(event);
}
// Draw our application after handling events
self.draw();
// It's best to use a buffered writer for the render method. TTY provides one, but you
// may use your own. The provided bufferedWriter has a buffer size of 4096
var buffered = self.tty.bufferedWriter();
// Render the application to the screen
try self.vx.render(buffered.writer().any());
try buffered.flush();
}
}
/// Update our application state from an event
pub fn update(self: *MyApp, event: Event) !void {
switch (event) {
.key_press => |key| {
// key.matches does some basic matching algorithms. Key matching can be complex in
// the presence of kitty keyboard encodings, this will generally be a good approach.
// There are other matching functions available for specific purposes, as well
if (key.matches('c', .{ .ctrl = true }))
self.should_quit = true;
},
.mouse => |mouse| self.mouse = mouse,
.winsize => |ws| try self.vx.resize(self.allocator, self.tty.anyWriter(), ws),
else => {},
}
}
/// Draw our current state
pub fn draw(self: *MyApp) void {
const msg = "Hello, world!";
// Window is a bounded area with a view to the screen. You cannot draw outside of a windows
// bounds. They are light structures, not intended to be stored.
const win = self.vx.window();
// Clearing the window has the effect of setting each cell to it's "default" state. Vaxis
// applications typically will be immediate mode, and you will redraw your entire
// application during the draw cycle.
win.clear();
// In addition to clearing our window, we want to clear the mouse shape state since we may
// be changing that as well
self.vx.setMouseShape(.default);
const child = win.child(.{
.x_off = (win.width / 2) - 7,
.y_off = win.height / 2 + 1,
.width = .{ .limit = msg.len },
.height = .{ .limit = 1 },
});
// mouse events are much easier to handle in the draw cycle. Windows have a helper method to
// determine if the event occurred in the target window. This method returns null if there
// is no mouse event, or if it occurred outside of the window
const style: vaxis.Style = if (child.hasMouse(self.mouse)) |_| blk: {
// We handled the mouse event, so set it to null
self.mouse = null;
self.vx.setMouseShape(.pointer);
break :blk .{ .reverse = true };
} else .{};
// Print a text segment to the screen. This is a helper function which iterates over the
// text field for graphemes. Alternatively, you can implement your own print functions and
// use the writeCell API.
_ = try child.printSegment(.{ .text = msg, .style = style }, .{});
}
};
/// Keep our main function small. Typically handling arg parsing and initialization only
pub fn main() !void { pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("Can spawn process: {}\n", .{std.process.can_spawn});
var gpa = std.heap.GeneralPurposeAllocator(.{}){}; var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator(); defer {
var lldb_child = std.process.Child.init(&[_][]const u8{"lldb-vscode"}, alloc); const deinit_status = gpa.deinit();
_ = try lldb_child.spawnAndWait(); //fail test; can't try in defer as defer is executed after we return
if (deinit_status == .leak) {
std.log.err("memory leak", .{});
}
}
const allocator = gpa.allocator();
// Initialize our application
var app = try MyApp.init(allocator);
defer app.deinit();
// Run the application
try app.run();
} }