From 6833b39ffe311f0f1e39b6669b946d712d7dcd93 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 16 Jun 2026 23:04:59 -0300 Subject: ghostty setup --- build.zig | 56 +++++++++++++++++--- build.zig.zon | 18 ++++++- main.zig | 49 +++++++++-------- uucode_config.zig | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 247 insertions(+), 31 deletions(-) create mode 100644 uucode_config.zig diff --git a/build.zig b/build.zig index 14066cf3..9bc1d2b6 100644 --- a/build.zig +++ b/build.zig @@ -4,11 +4,6 @@ pub fn build(b: *std.Build) void { const optimize = b.standardOptimizeOption(.{}); const target = b.standardTargetOptions(.{}); - const vaxis_dep = b.dependency("vaxis", .{ - .target = target, - .optimize = optimize, - }); - const exe = b.addExecutable(.{ .name = "pardes", .root_module = b.createModule(.{ @@ -18,7 +13,56 @@ pub fn build(b: *std.Build) void { }), }); - exe.root_module.addImport("vaxis", vaxis_dep.module("vaxis")); + // ghostty-vt and vaxis both depend on uucode, but Zig 0.16 forbids the + // same source file from belonging to two modules, so they can't each have + // their own uucode instance. We build a single uucode module here (with our + // own uucode_config.zig) and hand it to both. + // + // Our config matches ghostty's exactly except the tables are `.unpacked`; + // see uucode_config.zig for why that's required on Zig 0.16. + const uucode_config = b.path("uucode_config.zig"); + const uucode_tables = b.dependency("uucode", .{ + .target = target, + .optimize = optimize, + .build_config_path = uucode_config, + }).namedLazyPath("tables.zig"); + const uucode_mod = b.dependency("uucode", .{ + .target = target, + .optimize = optimize, + .build_config_path = uucode_config, + .tables_path = uucode_tables, + }).module("uucode"); + + // vaxis is built with `external_uucode = true` so it skips its built-in + // uucode dependency and lets us wire one in. + const vaxis_dep = b.dependency("vaxis", .{ + .target = target, + .optimize = optimize, + .external_uucode = true, + }); + const vaxis_mod = vaxis_dep.module("vaxis"); + + // You'll want to use a lazy dependency here so that ghostty is only + // downloaded if you actually need it. + if (b.lazyDependency("ghostty", .{ + // Setting simd to false will force a pure static build that + // doesn't even require libc, but it has a significant performance + // penalty. If your embedding app requires libc anyway, you should + // always keep simd enabled. + // .simd = false, + })) |dep| { + // Accessing ghostty's modules runs its build(), which (because vaxis is + // a shared dependency, deduplicated to the same module) wires ghostty's + // own uucode into vaxis. We undo that below by overriding both imports + // with ours — order matters, so this must happen after this point. + const ghostty_vt = dep.module("ghostty-vt"); + ghostty_vt.addImport("uucode", uucode_mod); + exe.root_module.addImport("ghostty-vt", ghostty_vt); + } + + // Override uucode on vaxis last, so it wins over ghostty's build() above. + vaxis_mod.addImport("uucode", uucode_mod); + exe.root_module.addImport("vaxis", vaxis_mod); const run_exe = b.addRunArtifact(exe); diff --git a/build.zig.zon b/build.zig.zon index 4c7fb6e7..2484f418 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,9 +2,23 @@ .name = ._02_pardes_code, .version = "0.0.1", .dependencies = .{ + // Pinned to the exact libvaxis commit ghostty is built against, so it + // is compatible with ghostty's uucode configuration (the one we share + // with it via `external_uucode` in build.zig). .vaxis = .{ - .url = "git+https://github.com/rockorager/libvaxis.git#a367b89da09bfe5e1b628501940de5b4f858f5f3", - .hash = "vaxis-0.6.0-BWNV_HHwCQB451KS7A8SMykALblPmGwHnzSfiJHjN3_9", + .url = "git+https://github.com/rockorager/libvaxis.git#1dbbe575dff4586fe51e3217aa5c3fecdcbb6089", + .hash = "vaxis-0.6.0-BWNV_CrbCQCscGpzsAlR402rYQ_tV3aAl081c2iRRkka", + }, + .ghostty = .{ + .url = "git+https://github.com/ghostty-org/ghostty.git#ba38b493d410fab522798550e38b74c9ccb242a2", + .hash = "ghostty-1.3.2-dev-5UdBC1v_EAUhcFYXrdrTN3gmvLnqILPQwD1KlIoPhCxT", + }, + // Same uucode commit ghostty and vaxis both depend on. We build it + // ourselves (with uucode_config.zig) so ghostty-vt and vaxis can share + // a single uucode module. See build.zig for the why. + .uucode = .{ + .url = "git+https://github.com/jacobsandlund/uucode#2826a37a4562284fdacd8fa029d49509cc9bffcd", + .hash = "uucode-0.2.0-ZZjBPlK5VADj7fdoq7G8LIHzD5o6FSkcBXXrRWr4jnrA", }, }, .minimum_zig_version = "0.16.0", diff --git a/main.zig b/main.zig index e591b7d0..a55ad637 100644 --- a/main.zig +++ b/main.zig @@ -1,5 +1,6 @@ const std = @import("std"); const vaxis = @import("vaxis"); +const ghostty_vt = @import("ghostty-vt"); // basic file navigation // basic shell io @@ -40,6 +41,15 @@ pub fn main(init: std.process.Init) !void { const io = init.io; const gpa = init.gpa; + var t: ghostty_vt.Terminal = try .init(gpa, .{ + .cols = 6, + .rows = 40, + }); + defer t.deinit(gpa); + + try t.printString("hello there!\nhowdy!"); + const str = try t.plainString(gpa); + var tty_buf: [0x10000]u8 = undefined; var lines: [0x1000][0x1000]u8 = undefined; @@ -78,17 +88,8 @@ pub fn main(init: std.process.Init) !void { while (true) { const event = try loop.nextEvent(); const win = vx.window(); - - for (0..MAX_FILES) |i| { - if (i > MAX_FILES) break; - - // const str: [:0]u8 = std.mem.absorbSentinel(&line); - const len = std.mem.findSentinel(u8, 0, @ptrCast(&lines[i])); - _ = win.printSegment(.{ .text = lines[i][0..len] }, .{ - .row_offset = @intCast(i), - .wrap = .none, - }); - } + const win_child = win.child(.{ .border = .{ .where = .all }, .height = 8, .width = 42 }); + _ = win_child.printSegment(.{ .text = "hey" }, .{}); switch (event) { .key_press => |key| { @@ -97,20 +98,22 @@ pub fn main(init: std.process.Init) !void { } }, .mouse => |m| { + _ = win.printSegment(.{ .text = str }, .{ .col_offset = 1, .row_offset = 1 }); + if (m.type == .press) { // const txt = try std.fmt.allocPrint(gpa, "col={} row={} xo={} yo={}", .{ m.col, m.row, m.xoffset, m.yoffset }); - const len = std.mem.findSentinel(u8, 0, @ptrCast(&lines[@intCast(m.row)])); - _ = win.printSegment( - .{ - .text = lines[@intCast(m.row)][0..len], - .style = .{ .fg = .{ .rgb = .{ 0x00, 0xd0, 0xaa } } }, - }, - .{ - .col_offset = @as(u16, @intCast(m.col)), - .row_offset = @as(u16, @intCast(m.row)), - .wrap = .none, - }, - ); + // constlen = std.mem.findSentinel(u8, 0, @ptrCast(&lines[@intCast(m.row)])); + // _ = win.printSegment( + // .{ + // .text = lines[@intCast(m.row)][0..len], + // .style = .{ .fg = .{ .rgb = .{ 0x00, 0xd0, 0xaa } } }, + // }, + // .{ + // .col_offset = @as(u16, @intCast(m.col)), + // .row_offset = @as(u16, @intCast(m.row)), + // .wrap = .none, + // }, + // ); } }, .winsize => |ws| try vx.resize(gpa, tty.writer(), ws), diff --git a/uucode_config.zig b/uucode_config.zig new file mode 100644 index 00000000..a9606914 --- /dev/null +++ b/uucode_config.zig @@ -0,0 +1,155 @@ +//! uucode build config shared by ghostty-vt and libvaxis. +//! +//! This is a copy of ghostty's `src/build/uucode_config.zig` (so ghostty-vt +//! gets exactly the fields/components it expects), with one change: every +//! table is forced to `.packing = .unpacked`. +//! +//! Why: libvaxis calls `uucode.get(...)`, which comptime-forces evaluation of +//! the whole generated `tables` value. With the default `.auto` packing the +//! `buildtime` table is emitted as a packed `@bitCast` of a huge `[N]u10` +//! array, and Zig 0.16.0 cannot evaluate that at comptime (it panics with +//! "TODO implement writeToPackedMemory for more types"). ghostty-vt itself +//! never forces that evaluation, so ghostty's own build doesn't hit it, but we +//! do the moment vaxis touches uucode. Unpacked tables are byte-aligned and +//! evaluate fine (see also the raised eval branch quota in main.zig). +const std = @import("std"); +const assert = std.debug.assert; +const config = @import("config.zig"); + +const Allocator = std.mem.Allocator; + +pub const fields = &config.mergeFields(config.fields, &.{ + .{ .name = "width", .type = u2 }, + .{ .name = "is_symbol", .type = bool }, +}); +pub const build_components = &config.mergeComponents(config.build_components, &.{ + .{ + .Impl = WidthComponent, + .inputs = &.{ + "wcwidth_standalone", + "wcwidth_zero_in_grapheme", + "is_emoji_modifier", + "grapheme_break_no_control", + }, + .fields = &.{"width"}, + }, + .{ + .Impl = IsSymbolComponent, + .inputs = &.{ "block", "general_category" }, + .fields = &.{"is_symbol"}, + }, +}); + +pub const get_components: []const config.Component = &.{}; + +pub const tables = [_]config.Table{ + .{ + .name = "runtime", + .packing = .unpacked, + .fields = &.{ + "is_emoji_presentation", + "case_folding_full", + }, + }, + .{ + // Fields that libvaxis needs that aren't included in the `runtime` + // table. + .name = "libvaxis_only", + .packing = .unpacked, + .fields = &.{ + "east_asian_width", + "general_category", + "grapheme_break", + }, + }, + .{ + .name = "buildtime", + .packing = .unpacked, + .fields = &.{ + "width", + "wcwidth_zero_in_grapheme", + "grapheme_break_no_control", + "is_symbol", + "is_emoji_vs_base", + }, + }, +}; + +const WidthComponent = struct { + pub fn build( + comptime InputRow: type, + comptime Row: type, + allocator: std.mem.Allocator, + io: std.Io, + inputs: config.MultiSlice(InputRow), + rows: *config.MultiSlice(Row), + backing: anytype, + tracking: anytype, + ) !void { + _ = allocator; + _ = io; + _ = backing; + _ = tracking; + + rows.len = config.num_code_points; + const items = rows.items(.width); + const standalone = inputs.items(.wcwidth_standalone); + const zero_in_grapheme = inputs.items(.wcwidth_zero_in_grapheme); + const is_emoji_modifier = inputs.items(.is_emoji_modifier); + const grapheme_break_no_control = inputs.items(.grapheme_break_no_control); + + // This condition is needed as Ghostty currently has a singular concept for + // the `width` of a code point, while `uucode` splits the concept into + // `wcwidth_standalone` and `wcwidth_zero_in_grapheme`. The two cases where + // we want to use the `wcwidth_standalone` despite the code point occupying + // zero width in a grapheme (`wcwidth_zero_in_grapheme`) are emoji + // modifiers and prepend code points. For emoji modifiers we want to + // support displaying them in isolation as color patches, and if prepend + // characters were to be width 0 they would disappear from the output with + // Ghostty's current width 0 handling. Future work will take advantage of + // the new uucode `wcwidth_standalone` vs `wcwidth_zero_in_grapheme` split. + for (0..config.num_code_points) |i| { + if (zero_in_grapheme[i] and !is_emoji_modifier[i] and grapheme_break_no_control[i] != .prepend) { + items[i] = 0; + } else { + items[i] = @min(2, standalone[i]); + } + } + } +}; + +const IsSymbolComponent = struct { + pub fn build( + comptime InputRow: type, + comptime Row: type, + allocator: std.mem.Allocator, + io: std.Io, + inputs: config.MultiSlice(InputRow), + rows: *config.MultiSlice(Row), + backing: anytype, + tracking: anytype, + ) !void { + _ = allocator; + _ = io; + _ = backing; + _ = tracking; + + rows.len = config.num_code_points; + const items = rows.items(.is_symbol); + const block = inputs.items(.block); + const general_category = inputs.items(.general_category); + + for (0..config.num_code_points) |i| { + items[i] = + general_category[i] == .other_private_use or + block[i] == .arrows or + block[i] == .dingbats or + block[i] == .emoticons or + block[i] == .miscellaneous_symbols or + block[i] == .enclosed_alphanumerics or + block[i] == .enclosed_alphanumeric_supplement or + block[i] == .miscellaneous_symbols_and_pictographs or + block[i] == .transport_and_map_symbols; + } + } +}; -- cgit v1.3