//! Playpen: a terminal with vertical tabs, web panes and saved layouts, 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 builtin = @import("builtin"); const adw = @import("adw"); const gio = @import("gio"); const Settings = @import("Settings.zig"); const Window = @import("Window.zig"); const appearance = @import("appearance.zig"); const icons = @import("icons.zig"); const review = @import("review.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, }; /// The process allocator, which everything in the app shares: the GTK main /// loop, every terminal's parser, and every review connection thread. /// /// `DebugAllocator` only in a debug build, and this is not a matter of taste. /// It is thread-safe by way of one mutex around every allocation and every /// free, and its backing allocator is the page allocator — so a large /// allocation is an `mmap` and its release an `munmap`, both taken under that /// single process-wide lock. A review pane's poll allocates a whole `git diff` /// and drops it a moment later, several times a second across a handful of open /// reviews, and each one of those was stalling the main loop and every other /// request behind the same lock. `c_allocator` has a per-thread cache and no /// global lock; libc is already linked for GTK. var debug_gpa: std.heap.DebugAllocator(.{}) = .init; fn gpa() std.mem.Allocator { return if (builtin.mode == .Debug) debug_gpa.allocator() else std.heap.c_allocator; } pub fn main() u8 { defer if (builtin.mode == .Debug) { _ = debug_gpa.deinit(); }; // Before the allocator's own teardown, since the settings arena comes out // of it. A no-op if the app never got as far as activating. defer Settings.deinit(); // Also before the allocator, and after the window: the review server owns // threads, and stopping it waits for the ones still inside a request rather // than freeing the memory they are reading. A no-op if it never started. defer review.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.playpen", .{ .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 { // The file both of the next two read from: the scheme, and the tabs the // window opens itself with. Settings.init(gpa()); // Before the window, so that the first frame is drawn in the scheme the // user chose rather than repainted into it a moment later. appearance.init(); // Same reasoning: the bundled icons are registered before anything asks // for one by name. icons.init(); // Before the window, because every tab is announced to the server as it is // created and every terminal is handed the endpoint of the tab it opens in. // Started unconditionally rather than on the first review pane, so an agent // running in a tab has a `PLAYPEN_REVIEW_URL` from the moment it starts. review.init(gpa()); const window = Window.create(gpa(), app) catch |err| { std.log.err("failed to create window: {s}", .{@errorName(err)}); return; }; window.present(); }