diff --git a/build.zig b/build.zig index 8869bef..f8c55ef 100644 --- a/build.zig +++ b/build.zig @@ -99,6 +99,12 @@ fn buildTui(b: *Build, target: ResolvedTarget, optimize: OptimizeMode) !void { .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.step("run-tui", "Run TUI").dependOn(&b.addRunArtifact(tui).step); } diff --git a/build.zig.zon b/build.zig.zon index 51b1b1e..9d26fdd 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -14,5 +14,9 @@ .cimgui = .{ .path = "3rd-party/cimgui", }, + .vaxis = .{ + .url = "git+https://github.com/rockorager/libvaxis?ref=main#235e0bb27bc89655d8c66fc6378b31d4a98a117a", + .hash = "1220fef553676a4c90035db27c13ad609d5823198a94cc971e95ab9c680e3dcd2df0", + }, }, } diff --git a/src/tui/main.zig b/src/tui/main.zig index a0aa24c..47d24a0 100644 --- a/src/tui/main.zig +++ b/src/tui/main.zig @@ -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 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 { - 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(); + defer { + const deinit_status = gpa.deinit(); + //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(); }