aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/cli/boo.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/cli/boo.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/cli/boo.zig')
-rw-r--r--vendor/libghostty-vt/src/cli/boo.zig236
1 files changed, 236 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/cli/boo.zig b/vendor/libghostty-vt/src/cli/boo.zig
new file mode 100644
index 00000000..2834eadb
--- /dev/null
+++ b/vendor/libghostty-vt/src/cli/boo.zig
@@ -0,0 +1,236 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const args = @import("args.zig");
+const Action = @import("ghostty.zig").Action;
+const Allocator = std.mem.Allocator;
+const vaxis = @import("vaxis");
+
+const framedata = @import("framedata").compressed;
+
+const vxfw = vaxis.vxfw;
+
+pub const Options = struct {
+ pub fn deinit(self: Options) void {
+ _ = self;
+ }
+
+ /// Enables `-h` and `--help` to work.
+ pub fn help(self: Options) !void {
+ _ = self;
+ return Action.help_error;
+ }
+};
+
+const Boo = struct {
+ frame: u8,
+ framerate: u32, // 30 fps
+ // We know the size of this at compile time, but we heap allocate the slice to prevent the
+ // binary from increasing too much in size
+ buffer: [frame_width * frame_height]vaxis.Cell = undefined,
+
+ ghostty_style: vaxis.Style,
+ outline_style: vaxis.Style,
+
+ // Width of a single frame
+ const frame_width = 100;
+ // Height of a single frame
+ const frame_height = 41;
+
+ fn widget(self: *Boo) vxfw.Widget {
+ return .{
+ .userdata = self,
+ .eventHandler = Boo.typeErasedEventHandler,
+ .drawFn = Boo.typeErasedDrawFn,
+ };
+ }
+
+ fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
+ const self: *Boo = @ptrCast(@alignCast(ptr));
+ switch (event) {
+ .init,
+ .tick,
+ => {
+ self.updateFrame();
+ ctx.redraw = true;
+ return ctx.tick(self.framerate, self.widget());
+ },
+ .key_press => |key| {
+ if (key.matches('c', .{ .ctrl = true }) or
+ key.matches(vaxis.Key.escape, .{}))
+ {
+ ctx.quit = true;
+ return;
+ }
+ },
+ else => {},
+ }
+ }
+
+ fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
+ const self: *Boo = @ptrCast(@alignCast(ptr));
+ const max = ctx.max.size();
+
+ // Warn for screen size
+ if (max.width < frame_width or max.height < frame_height) {
+ const text: vxfw.Text = .{ .text = "Screen must be at least 100w x 41h" };
+ const center: vxfw.Center = .{ .child = text.widget() };
+ return center.draw(ctx);
+ }
+
+ // Calculate x and y offsets to center the animation frame
+ const offset_y = (max.height - frame_height) / 2;
+ const offset_x = (max.width - frame_width) / 2;
+
+ // Create the animation surface
+ const child: vxfw.Surface = .{
+ .size = .{ .width = @intCast(frame_width), .height = @intCast(frame_height) },
+ .widget = self.widget(),
+ .buffer = &self.buffer,
+ .children = &.{},
+ };
+
+ // Allocate a slice of child surfaces
+ var children = try ctx.arena.alloc(vxfw.SubSurface, 1);
+ children[0] = .{
+ .origin = .{ .row = @intCast(offset_y), .col = @intCast(offset_x) },
+ .surface = child,
+ };
+
+ return .{
+ .size = max,
+ .widget = self.widget(),
+ .buffer = &.{},
+ .children = children,
+ };
+ }
+
+ /// Updates our internal buffer with the current frame, then advances the frame index
+ fn updateFrame(self: *Boo) void {
+ const frame = frames[self.frame];
+ // A frame is characters with html spans. When we encounter a span, we use the outline style
+ // until the span ends. That is, when we find a '<', we parse until '>'. Then we use the
+ // outline styule until the next '<', and skip until the next '>'
+
+ const State = enum {
+ normal,
+ span,
+ in_tag,
+ in_closing_tag,
+ };
+
+ var cell_idx: usize = 0;
+
+ var line_iter = std.mem.splitScalar(u8, frame, '\n');
+ while (line_iter.next()) |line| {
+ var state: State = .normal;
+ var style = self.ghostty_style;
+ var cp_iter: std.unicode.Utf8Iterator = .{ .bytes = line, .i = 0 };
+ while (cp_iter.nextCodepointSlice()) |char| {
+ switch (state) {
+ .normal => if (std.mem.eql(u8, "<", char)) {
+ state = .in_tag;
+ // We will be entering a span
+ style = self.outline_style;
+ continue;
+ },
+ .span => if (std.mem.eql(u8, "<", char)) {
+ state = .in_tag;
+ style = self.ghostty_style;
+ continue;
+ },
+ .in_tag => {
+ // If we encounter a '/', we are a closing tag
+ // If we parse all the way to a '>' we are an opening tag: we are now in a span
+ if (std.mem.eql(u8, "/", char))
+ state = .in_closing_tag
+ else if (std.mem.eql(u8, ">", char))
+ state = .span;
+ continue;
+ },
+ .in_closing_tag => {
+ // If we are closing a tag, we will enter the normal state
+ if (std.mem.eql(u8, ">", char)) state = .normal;
+ continue;
+ },
+ }
+ self.buffer[cell_idx] = .{
+ .char = .{
+ .grapheme = char,
+ .width = 1,
+ },
+ .style = style,
+ };
+ cell_idx += 1;
+ }
+ }
+ std.debug.assert(cell_idx == self.buffer.len);
+
+ // Lastly, update the frame index
+ self.frame += 1;
+ if (self.frame == frames.len) self.frame = 0;
+ }
+};
+
+/// The `boo` command is used to display the animation from the Ghostty website in the terminal
+pub fn run(gpa: Allocator) !u8 {
+ // Disable on non-desktop systems.
+ switch (builtin.os.tag) {
+ .windows, .macos, .linux, .freebsd => {},
+ else => return 1,
+ }
+
+ var opts: Options = .{};
+ defer opts.deinit();
+
+ {
+ var iter = try args.argsIterator(gpa);
+ defer iter.deinit();
+ try args.parse(Options, gpa, &opts, &iter);
+ }
+
+ try decompressFrames(gpa);
+ defer {
+ gpa.free(frames);
+ gpa.free(decompressed_data);
+ }
+
+ var app = try vxfw.App.init(gpa);
+ defer app.deinit();
+
+ var boo: Boo = undefined;
+ boo.frame = 0;
+ boo.framerate = 1000 / 30;
+ boo.ghostty_style = .{};
+ boo.outline_style = .{ .fg = .{ .index = 4 } };
+ @memset(&boo.buffer, .{});
+
+ try app.run(boo.widget(), .{});
+
+ return 0;
+}
+
+/// We store a global ref to the decompressed data. All of our frames reference into this data
+var decompressed_data: []const u8 = undefined;
+
+/// Heap allocated list of frames. The underlying frame data references decompressed_data
+var frames: []const []const u8 = undefined;
+
+/// Decompress the frames into a slice of individual frames
+fn decompressFrames(gpa: Allocator) !void {
+ var src: std.Io.Reader = .fixed(framedata);
+
+ // var buf: [std.compress.flate.max_window_len]u8 = undefined;
+ var decompress: std.compress.flate.Decompress = .init(&src, .raw, &.{});
+
+ var out: std.Io.Writer.Allocating = .init(gpa);
+ _ = try decompress.reader.streamRemaining(&out.writer);
+ decompressed_data = try out.toOwnedSlice();
+
+ var frame_list: std.ArrayList([]const u8) = try .initCapacity(gpa, 235);
+
+ var frame_iter = std.mem.splitScalar(u8, decompressed_data, '\x01');
+ while (frame_iter.next()) |frame| {
+ try frame_list.append(gpa, frame);
+ }
+ frames = try frame_list.toOwnedSlice(gpa);
+}