start extracting core terminal zig module

pull/8840/head
Mitchell Hashimoto 2025-09-20 14:37:04 -07:00
parent 485b6b73bf
commit a42193b997
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
15 changed files with 215 additions and 69 deletions

View File

@ -8,12 +8,25 @@ comptime {
}
pub fn build(b: *std.Build) !void {
// This defines all the available build options (e.g. `-D`).
// This defines all the available build options (e.g. `-D`). If you
// want to know what options are available, you can run `--help` or
// you can read `src/build/Config.zig`.
const config = try buildpkg.Config.init(b);
const test_filter = b.option(
[]const u8,
const test_filters = b.option(
[][]const u8,
"test-filter",
"Filter for test. Only applies to Zig tests.",
) orelse &[0][]const u8{};
// Ghostty dependencies used by many artifacts.
const deps = try buildpkg.SharedDeps.init(b, &config);
// The modules exported for Zig consumers of libghostty. If you're
// writing a Zig program that uses libghostty, read this file.
const mod = try buildpkg.GhosttyZig.init(
b,
&config,
&deps,
);
// All our steps which we'll hook up later. The steps are shown
@ -24,6 +37,10 @@ pub fn build(b: *std.Build) !void {
"Run the app under valgrind",
);
const test_step = b.step("test", "Run tests");
const test_lib_vt_step = b.step(
"test-lib-vt",
"Run libghostty-vt tests",
);
const test_valgrind_step = b.step(
"test-valgrind",
"Run tests under valgrind",
@ -37,10 +54,6 @@ pub fn build(b: *std.Build) !void {
const resources = try buildpkg.GhosttyResources.init(b, &config);
const i18n = if (config.i18n) try buildpkg.GhosttyI18n.init(b, &config) else null;
// Ghostty dependencies used by many artifacts.
const deps = try buildpkg.SharedDeps.init(b, &config);
if (config.emit_helpgen) deps.help_strings.install();
// Ghostty executable, the actual runnable Ghostty program.
const exe = try buildpkg.GhosttyExe.init(b, &config, &deps);
@ -83,6 +96,9 @@ pub fn build(b: *std.Build) !void {
&deps,
);
// Helpgen
if (config.emit_helpgen) deps.help_strings.install();
// Runtime "none" is libghostty, anything else is an executable.
if (config.app_runtime != .none) {
if (config.emit_exe) {
@ -185,7 +201,7 @@ pub fn build(b: *std.Build) !void {
run_step.dependOn(&macos_app_native_only.open.step);
// If we have no test filters, install the tests too
if (test_filter == null) {
if (test_filters.len == 0) {
macos_app_native_only.addTestStepDependencies(test_step);
}
}
@ -216,11 +232,23 @@ pub fn build(b: *std.Build) !void {
run_valgrind_step.dependOn(&run_cmd.step);
}
// Zig module tests
{
const mod_vt_test = b.addTest(.{
.root_module = mod.vt,
.target = config.target,
.optimize = config.optimize,
.filters = test_filters,
});
test_lib_vt_step.dependOn(&mod_vt_test.step);
}
// Tests
{
// Full unit tests
const test_exe = b.addTest(.{
.name = "ghostty-test",
.filters = if (test_filter) |v| &.{v} else &.{},
.filters = test_filters,
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = config.baselineTarget(),
@ -230,7 +258,6 @@ pub fn build(b: *std.Build) !void {
.unwind_tables = .sync,
}),
});
if (config.emit_test_exe) b.installArtifact(test_exe);
_ = try deps.add(test_exe);

36
src/build/GhosttyZig.zig Normal file
View File

@ -0,0 +1,36 @@
//! GhosttyZig generates the Zig modules that Ghostty exports
//! for downstream usage.
const GhosttyZig = @This();
const std = @import("std");
const Config = @import("Config.zig");
const SharedDeps = @import("SharedDeps.zig");
const vt_options = @import("../terminal/build_options.zig");
vt: *std.Build.Module,
pub fn init(
b: *std.Build,
cfg: *const Config,
deps: *const SharedDeps,
) !GhosttyZig {
const vt = b.addModule("ghostty-vt", .{
.root_source_file = b.path("src/lib_vt.zig"),
.target = cfg.target,
.optimize = cfg.optimize,
});
deps.unicode_tables.addModuleImport(vt);
vt_options.addOptions(b, vt, .{
.artifact = .lib,
.slow_runtime_safety = switch (cfg.optimize) {
.Debug => true,
.ReleaseSafe,
.ReleaseSmall,
.ReleaseFast,
=> false,
},
});
return .{ .vt = vt };
}

View File

@ -107,6 +107,21 @@ pub fn add(
// Every exe gets build options populated
step.root_module.addOptions("build_options", self.options);
// Every exe needs the terminal options
{
const vt_options = @import("../terminal/build_options.zig");
vt_options.addOptions(b, step.root_module, .{
.artifact = .ghostty,
.slow_runtime_safety = switch (optimize) {
.Debug => true,
.ReleaseSafe,
.ReleaseSmall,
.ReleaseFast,
=> false,
},
});
}
// Freetype
_ = b.systemIntegrationOption("freetype", .{}); // Shows it in help
if (self.config.font_backend.hasFreetype()) {

View File

@ -64,11 +64,19 @@ pub fn init(b: *std.Build) !UnicodeTables {
/// Add the "unicode_tables" import.
pub fn addImport(self: *const UnicodeTables, step: *std.Build.Step.Compile) void {
self.props_output.addStepDependencies(&step.step);
step.root_module.addAnonymousImport("unicode_tables", .{
self.symbols_output.addStepDependencies(&step.step);
self.addModuleImport(step.root_module);
}
/// Add the "unicode_tables" import to a module.
pub fn addModuleImport(
self: *const UnicodeTables,
module: *std.Build.Module,
) void {
module.addAnonymousImport("unicode_tables", .{
.root_source_file = self.props_output,
});
self.symbols_output.addStepDependencies(&step.step);
step.root_module.addAnonymousImport("symbols_tables", .{
module.addAnonymousImport("symbols_tables", .{
.root_source_file = self.symbols_output,
});
}

View File

@ -18,6 +18,7 @@ pub const GhosttyI18n = @import("GhosttyI18n.zig");
pub const GhosttyXcodebuild = @import("GhosttyXcodebuild.zig");
pub const GhosttyXCFramework = @import("GhosttyXCFramework.zig");
pub const GhosttyWebdata = @import("GhosttyWebdata.zig");
pub const GhosttyZig = @import("GhosttyZig.zig");
pub const HelpStrings = @import("HelpStrings.zig");
pub const SharedDeps = @import("SharedDeps.zig");
pub const UnicodeTables = @import("UnicodeTables.zig");

View File

@ -19,7 +19,6 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const global_state = &@import("../global.zig").state;
const fontpkg = @import("../font/main.zig");
const inputpkg = @import("../input.zig");
const terminal = @import("../terminal/main.zig");
const internal_os = @import("../os/main.zig");
const cli = @import("../cli.zig");
@ -39,6 +38,16 @@ const RepeatableStringMap = @import("RepeatableStringMap.zig");
pub const Path = @import("path.zig").Path;
pub const RepeatablePath = @import("path.zig").RepeatablePath;
// We do this instead of importing all of terminal/main.zig to
// limit the dependency graph. This is important because some things
// like the `ghostty-build-data` binary depend on the Config but don't
// want to include all the other stuff.
const terminal = struct {
const CursorStyle = @import("../terminal/cursor.zig").Style;
const color = @import("../terminal/color.zig");
const x11_color = @import("../terminal/x11_color.zig");
};
const log = std.log.scoped(.config);
/// Used on Unixes for some defaults.

5
src/lib_vt.zig Normal file
View File

@ -0,0 +1,5 @@
const terminal = @import("terminal/main.zig");
test {
_ = terminal;
}

View File

@ -6,7 +6,8 @@ const std = @import("std");
const builtin = @import("builtin");
const testing = std.testing;
const Dir = std.fs.Dir;
const internal_os = @import("main.zig");
const allocTmpDir = @import("file.zig").allocTmpDir;
const freeTmpDir = @import("file.zig").freeTmpDir;
const log = std.log.scoped(.tempdir);
@ -31,8 +32,8 @@ pub fn init() !TempDir {
const dir = dir: {
const cwd = std.fs.cwd();
const tmp_dir = internal_os.allocTmpDir(std.heap.page_allocator) orelse break :dir cwd;
defer internal_os.freeTmpDir(std.heap.page_allocator, tmp_dir);
const tmp_dir = allocTmpDir(std.heap.page_allocator) orelse break :dir cwd;
defer freeTmpDir(std.heap.page_allocator, tmp_dir);
break :dir try cwd.openDir(tmp_dir, .{});
};

View File

@ -4,7 +4,7 @@
const PageList = @This();
const std = @import("std");
const build_config = @import("../build_config.zig");
const build_options = @import("terminal_options");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const fastmem = @import("../fastmem.zig");
@ -1492,7 +1492,7 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void {
},
}
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
assert(self.totalRows() >= self.rows);
}
}
@ -2524,7 +2524,7 @@ pub fn pin(self: *const PageList, pt: point.Point) ?Pin {
/// pin points to is removed completely, the tracked pin will be updated
/// to the top-left of the screen.
pub fn trackPin(self: *PageList, p: Pin) Allocator.Error!*Pin {
if (build_config.slow_runtime_safety) assert(self.pinIsValid(p));
if (build_options.slow_runtime_safety) assert(self.pinIsValid(p));
// Create our tracked pin
const tracked = try self.pool.pins.create();
@ -2556,7 +2556,7 @@ pub fn countTrackedPins(self: *const PageList) usize {
pub fn pinIsValid(self: *const PageList, p: Pin) bool {
// This is very slow so we want to ensure we only ever
// call this during slow runtime safety builds.
comptime assert(build_config.slow_runtime_safety);
comptime assert(build_options.slow_runtime_safety);
var it = self.pages.first;
while (it) |node| : (it = node.next) {
@ -3234,7 +3234,7 @@ pub fn pageIterator(
else
self.getBottomRight(tl_pt) orelse return .{ .row = null };
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
assert(tl_pin.eql(bl_pin) or tl_pin.before(bl_pin));
}
@ -3510,7 +3510,7 @@ pub const Pin = struct {
direction: Direction,
limit: ?Pin,
) PageIterator {
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
if (limit) |l| {
// Check the order according to the iteration direction.
switch (direction) {
@ -3560,7 +3560,7 @@ pub const Pin = struct {
// Note: this is primarily unit tested as part of the Kitty
// graphics deletion code.
pub fn isBetween(self: Pin, top: Pin, bottom: Pin) bool {
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
if (top.node == bottom.node) {
// If top is bottom, must be ordered.
assert(top.y <= bottom.y);

View File

@ -1,7 +1,7 @@
const Screen = @This();
const std = @import("std");
const build_config = @import("../build_config.zig");
const build_options = @import("terminal_options");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const ansi = @import("ansi.zig");
@ -24,6 +24,8 @@ const Row = pagepkg.Row;
const Cell = pagepkg.Cell;
const Pin = PageList.Pin;
pub const CursorStyle = @import("cursor.zig").Style;
const log = std.log.scoped(.screen);
/// The general purpose allocator to use for all memory allocations.
@ -141,22 +143,6 @@ pub const Cursor = struct {
}
};
/// The visual style of the cursor. Whether or not it blinks
/// is determined by mode 12 (modes.zig). This mode is synchronized
/// with CSI q, the same as xterm.
pub const CursorStyle = enum {
bar, // DECSCUSR 5, 6
block, // DECSCUSR 1, 2
underline, // DECSCUSR 3, 4
/// The cursor styles below aren't known by DESCUSR and are custom
/// implemented in Ghostty. They are reported as some standard style
/// if requested, though.
/// Hollow block cursor. This is a block cursor with the center empty.
/// Reported as DECSCUSR 1 or 2 (block).
block_hollow,
};
/// Saved cursor state.
pub const SavedCursor = struct {
x: size.CellCountInt,
@ -232,7 +218,7 @@ pub fn deinit(self: *Screen) void {
/// tests. This only asserts the screen specific data so callers should
/// ensure they're also calling page integrity checks if necessary.
pub fn assertIntegrity(self: *const Screen) void {
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
// We don't run integrity checks on Valgrind because its soooooo slow,
// Valgrind is our integrity checker, and we run these during unit
// tests (non-Valgrind) anyways so we're verifying anyways.
@ -772,7 +758,7 @@ pub fn cursorDownScroll(self: *Screen) !void {
// These assertions help catch some pagelist math errors. Our
// x/y should be unchanged after the grow.
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
const active = self.pages.pointFromPin(
.active,
page_pin,

View File

@ -0,0 +1,31 @@
const std = @import("std");
pub const Options = struct {
/// The target artifact to build. This will gate some functionality.
artifact: Artifact = .ghostty,
/// True if we should enable the "slow" runtime safety checks. These
/// are runtime safety checks that are slower than typical and should
/// generally be disabled in production builds.
slow_runtime_safety: bool = false,
};
pub const Artifact = enum {
/// Ghostty application
ghostty,
/// libghostty-vt, Zig module
lib,
};
/// Add the required build options for the terminal module.
pub fn addOptions(
b: *std.Build,
m: *std.Build.Module,
v: Options,
) void {
const opts = b.addOptions();
opts.addOption(Artifact, "artifact", v.artifact);
opts.addOption(bool, "slow_runtime_safety", v.slow_runtime_safety);
m.addOptions("terminal_options", opts);
}

15
src/terminal/cursor.zig Normal file
View File

@ -0,0 +1,15 @@
/// The visual style of the cursor. Whether or not it blinks
/// is determined by mode 12 (modes.zig). This mode is synchronized
/// with CSI q, the same as xterm.
pub const Style = enum {
bar, // DECSCUSR 5, 6
block, // DECSCUSR 1, 2
underline, // DECSCUSR 3, 4
/// The cursor styles below aren't known by DESCUSR and are custom
/// implemented in Ghostty. They are reported as some standard style
/// if requested, though.
/// Hollow block cursor. This is a block cursor with the center empty.
/// Reported as DECSCUSR 1 or 2 (block).
block_hollow,
};

View File

@ -9,9 +9,14 @@ const fastmem = @import("../../fastmem.zig");
const command = @import("graphics_command.zig");
const point = @import("../point.zig");
const PageList = @import("../PageList.zig");
const internal_os = @import("../../os/main.zig");
const wuffs = @import("wuffs");
const temp_dir = struct {
const TempDir = @import("../../os/TempDir.zig");
const allocTmpDir = @import("../../os/file.zig").allocTmpDir;
const freeTmpDir = @import("../../os/file.zig").freeTmpDir;
};
const log = std.log.scoped(.kitty_gfx);
/// Maximum width or height of an image. Taken directly from Kitty.
@ -276,8 +281,8 @@ pub const LoadingImage = struct {
fn isPathInTempDir(path: []const u8) bool {
if (std.mem.startsWith(u8, path, "/tmp")) return true;
if (std.mem.startsWith(u8, path, "/dev/shm")) return true;
if (internal_os.allocTmpDir(std.heap.page_allocator)) |dir| {
defer internal_os.freeTmpDir(std.heap.page_allocator, dir);
if (temp_dir.allocTmpDir(std.heap.page_allocator)) |dir| {
defer temp_dir.freeTmpDir(std.heap.page_allocator, dir);
if (std.mem.startsWith(u8, path, dir)) return true;
// The temporary dir is sometimes a symlink. On macOS for
@ -690,7 +695,7 @@ test "image load: temporary file without correct path" {
const testing = std.testing;
const alloc = testing.allocator;
var tmp_dir = try internal_os.TempDir.init();
var tmp_dir = try temp_dir.TempDir.init();
defer tmp_dir.deinit();
const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data");
try tmp_dir.dir.writeFile(.{
@ -723,7 +728,7 @@ test "image load: rgb, not compressed, temporary file" {
const testing = std.testing;
const alloc = testing.allocator;
var tmp_dir = try internal_os.TempDir.init();
var tmp_dir = try temp_dir.TempDir.init();
defer tmp_dir.deinit();
const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data");
try tmp_dir.dir.writeFile(.{
@ -760,7 +765,7 @@ test "image load: rgb, not compressed, regular file" {
const testing = std.testing;
const alloc = testing.allocator;
var tmp_dir = try internal_os.TempDir.init();
var tmp_dir = try temp_dir.TempDir.init();
defer tmp_dir.deinit();
const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data");
try tmp_dir.dir.writeFile(.{
@ -795,7 +800,7 @@ test "image load: png, not compressed, regular file" {
const testing = std.testing;
const alloc = testing.allocator;
var tmp_dir = try internal_os.TempDir.init();
var tmp_dir = try temp_dir.TempDir.init();
defer tmp_dir.deinit();
const data = @embedFile("testdata/image-png-none-50x76-2147483647-raw.data");
try tmp_dir.dir.writeFile(.{

View File

@ -1,5 +1,5 @@
const std = @import("std");
const build_config = @import("../build_config.zig");
const build_options = @import("terminal_options");
/// The possible cursor shapes. Not all app runtimes support these shapes.
/// The shapes are always based on the W3C supported cursor styles so we
@ -48,13 +48,20 @@ pub const MouseShape = enum(c_int) {
}
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
.gtk => @import("gobject").ext.defineEnum(
MouseShape,
.{ .name = "GhosttyMouseShape" },
),
pub const getGObjectType = gtk: {
switch (build_options.artifact) {
.ghostty => break :gtk void,
.lib => {},
}
.none => void,
break :gtk switch (@import("../build_config.zig").app_runtime) {
.gtk => @import("gobject").ext.defineEnum(
MouseShape,
.{ .name = "GhosttyMouseShape" },
),
.none => void,
};
};
};

View File

@ -1,6 +1,6 @@
const std = @import("std");
const builtin = @import("builtin");
const build_config = @import("../build_config.zig");
const build_options = @import("terminal_options");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const assert = std.debug.assert;
@ -182,8 +182,8 @@ pub const Page = struct {
/// If this is true then verifyIntegrity will do nothing. This is
/// only present with runtime safety enabled.
pause_integrity_checks: if (build_config.slow_runtime_safety) usize else void =
if (build_config.slow_runtime_safety) 0 else {},
pause_integrity_checks: if (build_options.slow_runtime_safety) usize else void =
if (build_options.slow_runtime_safety) 0 else {},
/// Initialize a new page, allocating the required backing memory.
/// The size of the initialized page defaults to the full capacity.
@ -307,7 +307,7 @@ pub const Page = struct {
/// doing a lot of operations that would trigger integrity check
/// violations but you know the page will end up in a consistent state.
pub fn pauseIntegrityChecks(self: *Page, v: bool) void {
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
if (v) {
self.pause_integrity_checks += 1;
} else {
@ -320,7 +320,7 @@ pub const Page = struct {
/// when runtime safety is enabled. This is a no-op when runtime
/// safety is disabled. This uses the libc allocator.
pub fn assertIntegrity(self: *const Page) void {
if (comptime build_config.slow_runtime_safety) {
if (comptime build_options.slow_runtime_safety) {
self.verifyIntegrity(std.heap.c_allocator) catch |err| {
log.err("page integrity violation, crashing. err={}", .{err});
@panic("page integrity violation");
@ -351,7 +351,7 @@ pub const Page = struct {
// tests (non-Valgrind) anyways so we're verifying anyways.
if (std.valgrind.runningOnValgrind() > 0) return;
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
if (self.pause_integrity_checks > 0) return;
}
@ -760,7 +760,7 @@ pub const Page = struct {
// This is an integrity check: if the row claims it doesn't
// have managed memory then all cells must also not have
// managed memory.
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
for (other_cells) |cell| {
assert(!cell.hasGrapheme());
assert(!cell.hyperlink);
@ -787,7 +787,7 @@ pub const Page = struct {
if (src_cell.hasGrapheme()) {
// To prevent integrity checks flipping. This will
// get fixed up when we check the style id below.
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
dst_cell.style_id = style.default_id;
}
@ -914,7 +914,7 @@ pub const Page = struct {
/// Get the cells for a row.
pub fn getCells(self: *const Page, row: *Row) []Cell {
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
const rows = self.rows.ptr(self.memory);
const cells = self.cells.ptr(self.memory);
assert(@intFromPtr(row) >= @intFromPtr(rows));
@ -1363,7 +1363,7 @@ pub const Page = struct {
pub fn appendGrapheme(self: *Page, row: *Row, cell: *Cell, cp: u21) Allocator.Error!void {
defer self.assertIntegrity();
if (build_config.slow_runtime_safety) assert(cell.codepoint() != 0);
if (build_options.slow_runtime_safety) assert(cell.codepoint() != 0);
const cell_offset = getOffset(Cell, self.memory, cell);
var map = self.grapheme_map.map(self.memory);
@ -1436,7 +1436,7 @@ pub const Page = struct {
/// there are scenarios where we want to move graphemes without changing
/// the content tag. Callers beware but assertIntegrity should catch this.
fn moveGrapheme(self: *Page, src: *Cell, dst: *Cell) void {
if (build_config.slow_runtime_safety) {
if (build_options.slow_runtime_safety) {
assert(src.hasGrapheme());
assert(!dst.hasGrapheme());
}
@ -1453,7 +1453,7 @@ pub const Page = struct {
/// Clear the graphemes for a given cell.
pub fn clearGrapheme(self: *Page, row: *Row, cell: *Cell) void {
defer self.assertIntegrity();
if (build_config.slow_runtime_safety) assert(cell.hasGrapheme());
if (build_options.slow_runtime_safety) assert(cell.hasGrapheme());
// Get our entry in the map, which must exist
const cell_offset = getOffset(Cell, self.memory, cell);