summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-06-16 23:04:59 -0300
committerGabriel Schneider <[email protected]>2026-06-17 21:39:58 -0300
commitffed14b9857b53a2aae4a0c39dee47949f1331f2 (patch)
tree8e06a0fb498cd76e2417b4af2b90d164dbfd2163
parent3a39c44dd814477289c5f17bd09440a74d0c8208 (diff)
downloadpardes-ffed14b9857b53a2aae4a0c39dee47949f1331f2.tar.gz
pardes-ffed14b9857b53a2aae4a0c39dee47949f1331f2.zip
ghostty setup
-rw-r--r--build.zig56
-rw-r--r--build.zig.zon18
-rw-r--r--main.zig49
-rw-r--r--uucode_config.zig155
4 files changed, 247 insertions, 31 deletions
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;
+ }
+ }
+};