Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237const 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 terminalpub 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 datavar decompressed_data: []const u8 = undefined;
/// Heap allocated list of frames. The underlying frame data references decompressed_datavar frames: []const []const u8 = undefined;
/// Decompress the frames into a slice of individual framesfn 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);}