vtabs: terminal with vertical tabs on libghostty-vt + GTK4

Uses libghostty-vt (the API Ghostty documents for external embedders) for
the terminal core. Ghostty's other C API, ghostty.h, exposes a full terminal
surface but only supports macOS and iOS platform tags, so it cannot be
embedded on Linux.

We supply the layers libghostty-vt deliberately leaves out: PTY and process
management, a Cairo/Pango cell renderer, and a GTK4/libadwaita UI with a
Zen-style vertical tab sidebar.

Nix pins the whole toolchain (Zig 0.16 via zig-overlay, GTK 4.22, libadwaita)
so no host setup is needed.
This commit is contained in:
Greyson Parrelli
2026-08-11 08:36:10 -04:00
commit 738cead680
16 changed files with 2359 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
//! vtabs: a terminal with vertical tabs, built on libghostty-vt.
//!
//! libghostty-vt supplies the terminal emulator core (escape sequence
//! parsing, screen and scrollback state, key/mouse encoding). Everything
//! else — process management, rendering, and the GTK4 UI — lives here.
const std = @import("std");
const adw = @import("adw");
const gdk = @import("gdk");
const gio = @import("gio");
const gtk = @import("gtk");
const Window = @import("Window.zig");
/// libghostty-vt logs unimplemented sequences at debug level, which is very
/// chatty against a real shell. Keep the app's own warnings and errors.
pub const std_options: std.Options = .{
.log_level = .info,
};
const css = @embedFile("style.css");
var gpa: std.heap.DebugAllocator(.{}) = .init;
pub fn main() u8 {
defer _ = gpa.deinit();
// Non-unique so every launch is its own process. The default GApplication
// behavior hands off to an already-running instance over D-Bus, which for
// a terminal means a second launch silently does nothing visible here and
// opens a window in whatever session owns the first one.
const app = adw.Application.new("dev.greyson.vtabs", .{ .non_unique = true });
defer app.unref();
_ = gio.Application.signals.activate.connect(app, ?*anyopaque, &onActivate, null, .{});
const status = gio.Application.run(app.as(gio.Application), 0, null);
return @intCast(status);
}
fn onActivate(app: *adw.Application, _: ?*anyopaque) callconv(.c) void {
loadCss();
const window = Window.create(gpa.allocator(), app) catch |err| {
std.log.err("failed to create window: {s}", .{@errorName(err)});
return;
};
window.present();
}
fn loadCss() void {
const display = gdk.Display.getDefault() orelse return;
const provider = gtk.CssProvider.new();
defer provider.unref();
provider.loadFromString(css);
gtk.StyleContext.addProviderForDisplay(
display,
provider.as(gtk.StyleProvider),
gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}