diff --git a/build.zig b/build.zig index 8047c457f..9d07d22a3 100644 --- a/build.zig +++ b/build.zig @@ -9,7 +9,8 @@ const font = @import("src/font/main.zig"); const renderer = @import("src/renderer.zig"); const terminfo = @import("src/terminfo/main.zig"); const config_vim = @import("src/config/vim.zig"); -const BuildConfig = @import("src/build_config.zig").BuildConfig; +const build_config = @import("src/build_config.zig"); +const BuildConfig = build_config.BuildConfig; const WasmTarget = @import("src/os/wasm/target.zig").Target; const LibtoolStep = @import("src/build/LibtoolStep.zig"); const LipoStep = @import("src/build/LipoStep.zig"); @@ -159,7 +160,7 @@ pub fn build(b: *std.Build) !void { "If not specified, git will be used. This must be a semantic version.", ); - const version: std.SemanticVersion = if (version_string) |v| + config.version = if (version_string) |v| try std.SemanticVersion.parse(v) else version: { const vsn = try Version.detect(b); @@ -198,7 +199,7 @@ pub fn build(b: *std.Build) !void { // Help exe. This must be run before any dependent executables because // otherwise the build will be cached without emit. That's clunky but meh. - if (emit_helpgen) addHelp(b, null); + if (emit_helpgen) try addHelp(b, null, config); // Add our benchmarks try benchSteps(b, target, optimize, config, emit_bench); @@ -211,19 +212,8 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, }) else null; - const exe_options = b.addOptions(); - config.addOptions(exe_options); - exe_options.addOption(std.SemanticVersion, "app_version", version); - exe_options.addOption([:0]const u8, "app_version_string", try std.fmt.allocPrintZ( - b.allocator, - "{}", - .{version}, - )); - // Exe if (exe_) |exe| { - exe.root_module.addOptions("build_options", exe_options); - // Add the shared dependencies _ = try addDeps(b, exe, config); @@ -428,7 +418,7 @@ pub fn build(b: *std.Build) !void { } // Documenation - if (emit_docs) buildDocumentation(b, version); + if (emit_docs) try buildDocumentation(b, config); // App (Linux) if (target.result.os.tag == .linux and config.app_runtime != .none) { @@ -464,7 +454,6 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, .target = target, }); - lib.root_module.addOptions("build_options", exe_options); _ = try addDeps(b, lib, config); const lib_install = b.addInstallLibFile( @@ -482,7 +471,6 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, .target = target, }); - lib.root_module.addOptions("build_options", exe_options); _ = try addDeps(b, lib, config); const lib_install = b.addInstallLibFile( @@ -510,7 +498,6 @@ pub fn build(b: *std.Build) !void { b, optimize, config, - exe_options, ); // Add our library to zig-out @@ -526,7 +513,6 @@ pub fn build(b: *std.Build) !void { null, optimize, config, - exe_options, ); // Add our library to zig-out @@ -542,7 +528,6 @@ pub fn build(b: *std.Build) !void { .simulator, optimize, config, - exe_options, ); // Add our library to zig-out @@ -605,6 +590,11 @@ pub fn build(b: *std.Build) !void { }), }; + // Whether we're using wasm shared memory. Some behaviors change. + // For now we require this but I wanted to make the code handle both + // up front. + const wasm_shared: bool = true; + // Modify our build configuration for wasm builds. const wasm_config: BuildConfig = config: { var copy = config; @@ -616,26 +606,19 @@ pub fn build(b: *std.Build) !void { // Backends that are fixed for wasm copy.font_backend = .web_canvas; + // Wasm-specific options + copy.wasm_shared = wasm_shared; + copy.wasm_target = wasm_target; + break :config copy; }; - // Whether we're using wasm shared memory. Some behaviors change. - // For now we require this but I wanted to make the code handle both - // up front. - const wasm_shared: bool = true; - exe_options.addOption(bool, "wasm_shared", wasm_shared); - - // We want to support alternate wasm targets in the future (i.e. - // server side) so we have this now although its hardcoded. - exe_options.addOption(WasmTarget, "wasm_target", wasm_target); - const wasm = b.addSharedLibrary(.{ .name = "ghostty-wasm", .root_source_file = .{ .path = "src/main_wasm.zig" }, .target = b.resolveTargetQuery(wasm_crosstarget), .optimize = optimize, }); - wasm.root_module.addOptions("build_options", exe_options); // So that we can use web workers with our wasm binary wasm.import_memory = true; @@ -666,7 +649,6 @@ pub fn build(b: *std.Build) !void { .target = b.resolveTargetQuery(wasm_crosstarget), }); - main_test.root_module.addOptions("build_options", exe_options); _ = try addDeps(b, main_test, wasm_config); test_step.dependOn(&main_test.step); } @@ -709,7 +691,6 @@ pub fn build(b: *std.Build) !void { copy.static = true; break :config copy; }); - main_test.root_module.addOptions("build_options", exe_options); const test_run = b.addRunArtifact(main_test); test_step.dependOn(&test_run.step); @@ -755,7 +736,6 @@ fn createMacOSLib( b: *std.Build, optimize: std.builtin.OptimizeMode, config: BuildConfig, - exe_options: *std.Build.Step.Options, ) !struct { *std.Build.Step, std.Build.LazyPath } { // Modify our build configuration for macOS builds. const macos_config: BuildConfig = config: { @@ -781,7 +761,6 @@ fn createMacOSLib( }); lib.bundle_compiler_rt = true; lib.linkLibC(); - lib.root_module.addOptions("build_options", exe_options); // Create a single static lib with all our dependencies merged var lib_list = try addDeps(b, lib, macos_config); @@ -810,7 +789,6 @@ fn createMacOSLib( }); lib.bundle_compiler_rt = true; lib.linkLibC(); - lib.root_module.addOptions("build_options", exe_options); // Create a single static lib with all our dependencies merged var lib_list = try addDeps(b, lib, macos_config); @@ -847,7 +825,6 @@ fn createIOSLib( abi: ?std.Target.Abi, optimize: std.builtin.OptimizeMode, config: BuildConfig, - exe_options: *std.Build.Step.Options, ) !struct { *std.Build.Step, std.Build.LazyPath } { const ios_config: BuildConfig = config: { var copy = config; @@ -868,7 +845,6 @@ fn createIOSLib( }); lib.bundle_compiler_rt = true; lib.linkLibC(); - lib.root_module.addOptions("build_options", exe_options); // Create a single static lib with all our dependencies merged var lib_list = try addDeps(b, lib, ios_config); @@ -895,6 +871,11 @@ fn addDeps( step: *std.Build.Step.Compile, config: BuildConfig, ) !LazyPathList { + // All object targets get access to a standard build_options module + const exe_options = b.addOptions(); + try config.addOptions(exe_options); + step.root_module.addOptions("build_options", exe_options); + var static_libs = LazyPathList.init(b.allocator); errdefer static_libs.deinit(); @@ -1126,7 +1107,7 @@ fn addDeps( } } - addHelp(b, step); + try addHelp(b, step, config); return static_libs; } @@ -1135,7 +1116,8 @@ fn addDeps( fn addHelp( b: *std.Build, step_: ?*std.Build.Step.Compile, -) void { + config: BuildConfig, +) !void { // Our static state between runs. We memoize our help strings // so that we only execute the help generation once. const HelpState = struct { @@ -1150,6 +1132,15 @@ fn addHelp( }); if (step_ == null) b.installArtifact(help_exe); + const help_config = config: { + var copy = config; + copy.exe_entrypoint = .helpgen; + break :config copy; + }; + const options = b.addOptions(); + try help_config.addOptions(options); + help_exe.root_module.addOptions("build_options", options); + const help_run = b.addRunArtifact(help_exe); HelpState.generated = help_run.captureStdOut(); break :strings HelpState.generated.?; @@ -1166,8 +1157,8 @@ fn addHelp( /// Generate documentation (manpages, etc.) from help strings fn buildDocumentation( b: *std.Build, - version: std.SemanticVersion, -) void { + config: BuildConfig, +) !void { const manpages = [_]struct { name: []const u8, section: []const u8, @@ -1179,15 +1170,22 @@ fn buildDocumentation( inline for (manpages) |manpage| { const generate_markdown = b.addExecutable(.{ .name = "mdgen_" ++ manpage.name ++ "_" ++ manpage.section, - .root_source_file = .{ - .path = "src/mdgen_" ++ manpage.name ++ "_" ++ manpage.section ++ ".zig", - }, + .root_source_file = .{ .path = "src/main.zig" }, .target = b.host, }); - addHelp(b, generate_markdown); + try addHelp(b, generate_markdown, config); + + const gen_config = config: { + var copy = config; + copy.exe_entrypoint = @field( + build_config.ExeEntrypoint, + "mdgen_" ++ manpage.name ++ "_" ++ manpage.section, + ); + break :config copy; + }; const generate_markdown_options = b.addOptions(); - generate_markdown_options.addOption(std.SemanticVersion, "version", version); + try gen_config.addOptions(generate_markdown_options); generate_markdown.root_module.addOptions("build_options", generate_markdown_options); const generate_markdown_step = b.addRunArtifact(generate_markdown); @@ -1254,24 +1252,26 @@ fn benchSteps( // Name of the conformance app and full path to the entrypoint. const name = entry.name[0..index]; - const path = try fs.path.join(b.allocator, &[_][]const u8{ - c_dir_path, - entry.name, - }); // Executable builder. const bin_name = try std.fmt.allocPrint(b.allocator, "bench-{s}", .{name}); const c_exe = b.addExecutable(.{ .name = bin_name, - .root_source_file = .{ .path = path }, + .root_source_file = .{ .path = "src/main.zig" }, .target = target, .optimize = optimize, - // .main_pkg_path = .{ .path = "./src" }, }); if (install) b.installArtifact(c_exe); _ = try addDeps(b, c_exe, config: { var copy = config; copy.static = true; + + var buf: [64]u8 = undefined; + copy.exe_entrypoint = std.meta.stringToEnum( + build_config.ExeEntrypoint, + try std.fmt.bufPrint(&buf, "bench_{s}", .{name}), + ).?; + break :config copy; }); } diff --git a/src/bench/parser.zig b/src/bench/parser.zig index 452d61690..ee6c3ee94 100644 --- a/src/bench/parser.zig +++ b/src/bench/parser.zig @@ -13,7 +13,7 @@ const std = @import("std"); const ArenaAllocator = std.heap.ArenaAllocator; -const cli_args = @import("../cli_args.zig"); +const cli = @import("../cli.zig"); const terminal = @import("../terminal/main.zig"); pub fn main() !void { @@ -29,7 +29,7 @@ pub fn main() !void { errdefer args.deinit(); var iter = try std.process.argsWithAllocator(alloc); defer iter.deinit(); - try cli_args.parse(Args, alloc, &args, &iter); + try cli.args.parse(Args, alloc, &args, &iter); break :args args; }; defer args.deinit(); diff --git a/src/mdgen_ghostty_1.zig b/src/build/mdgen/main_ghostty_1.zig similarity index 54% rename from src/mdgen_ghostty_1.zig rename to src/build/mdgen/main_ghostty_1.zig index eeb85c900..b3663de8d 100644 --- a/src/mdgen_ghostty_1.zig +++ b/src/build/mdgen/main_ghostty_1.zig @@ -1,13 +1,13 @@ const std = @import("std"); -const gen = @import("build/mdgen/mdgen.zig"); +const gen = @import("mdgen.zig"); pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const alloc = gpa.allocator(); const writer = std.io.getStdOut().writer(); - try gen.substitute(alloc, @embedFile("build/mdgen/ghostty_1_header.md"), writer); + try gen.substitute(alloc, @embedFile("ghostty_1_header.md"), writer); try gen.genActions(writer); try gen.genConfig(writer, true); - try gen.substitute(alloc, @embedFile("build/mdgen/ghostty_1_footer.md"), writer); + try gen.substitute(alloc, @embedFile("ghostty_1_footer.md"), writer); } diff --git a/src/mdgen_ghostty_5.zig b/src/build/mdgen/main_ghostty_5.zig similarity index 55% rename from src/mdgen_ghostty_5.zig rename to src/build/mdgen/main_ghostty_5.zig index 8773b433a..77c72b946 100644 --- a/src/mdgen_ghostty_5.zig +++ b/src/build/mdgen/main_ghostty_5.zig @@ -1,13 +1,13 @@ const std = @import("std"); -const gen = @import("build/mdgen/mdgen.zig"); +const gen = @import("mdgen.zig"); pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const alloc = gpa.allocator(); const output = std.io.getStdOut().writer(); - try gen.substitute(alloc, @embedFile("build/mdgen/ghostty_5_header.md"), output); + try gen.substitute(alloc, @embedFile("ghostty_5_header.md"), output); try gen.genConfig(output, false); try gen.genKeybindActions(output); - try gen.substitute(alloc, @embedFile("build/mdgen/ghostty_5_footer.md"), output); + try gen.substitute(alloc, @embedFile("ghostty_5_footer.md"), output); } diff --git a/src/build/mdgen/mdgen.zig b/src/build/mdgen/mdgen.zig index d94691441..ec443b254 100644 --- a/src/build/mdgen/mdgen.zig +++ b/src/build/mdgen/mdgen.zig @@ -1,23 +1,20 @@ const std = @import("std"); const help_strings = @import("help_strings"); -const build_options = @import("build_options"); +const build_config = @import("../../build_config.zig"); const Config = @import("../../config/Config.zig"); const Action = @import("../../cli/action.zig").Action; const KeybindAction = @import("../../input/Binding.zig").Action; pub fn substitute(alloc: std.mem.Allocator, input: []const u8, writer: anytype) !void { - const version_string = try std.fmt.allocPrint(alloc, "{}", .{build_options.version}); - defer alloc.free(version_string); - const output = try alloc.alloc(u8, std.mem.replacementSize( u8, input, "@@VERSION@@", - version_string, + build_config.version_string, )); defer alloc.free(output); - _ = std.mem.replace(u8, input, "@@VERSION@@", version_string, output); + _ = std.mem.replace(u8, input, "@@VERSION@@", build_config.version_string, output); try writer.writeAll(output); } diff --git a/src/build_config.zig b/src/build_config.zig index 6a9a55085..52e975717 100644 --- a/src/build_config.zig +++ b/src/build_config.zig @@ -9,6 +9,7 @@ const assert = std.debug.assert; const apprt = @import("apprt.zig"); const font = @import("font/main.zig"); const rendererpkg = @import("renderer.zig"); +const WasmTarget = @import("os/wasm/target.zig").Target; /// The build configuratin options. This may not be all available options /// to `zig build` but it contains all the options that the Ghostty source @@ -18,6 +19,7 @@ const rendererpkg = @import("renderer.zig"); /// between options, make it easy to copy and mutate options for different /// build types, etc. pub const BuildConfig = struct { + version: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 }, static: bool = false, flatpak: bool = false, libadwaita: bool = false, @@ -25,8 +27,17 @@ pub const BuildConfig = struct { renderer: rendererpkg.Impl = .opengl, font_backend: font.Backend = .freetype, + /// The entrypoint for exe targets. + exe_entrypoint: ExeEntrypoint = .ghostty, + + /// The target runtime for the wasm build and whether to use wasm shared + /// memory or not. These are both legacy wasm-specific options that we + /// will probably have to revisit when we get back to work on wasm. + wasm_target: WasmTarget = .browser, + wasm_shared: bool = true, + /// Configure the build options with our values. - pub fn addOptions(self: BuildConfig, step: *std.Build.Step.Options) void { + pub fn addOptions(self: BuildConfig, step: *std.Build.Step.Options) !void { // We need to break these down individual because addOption doesn't // support all types. step.addOption(bool, "flatpak", self.flatpak); @@ -34,6 +45,19 @@ pub const BuildConfig = struct { step.addOption(apprt.Runtime, "app_runtime", self.app_runtime); step.addOption(font.Backend, "font_backend", self.font_backend); step.addOption(rendererpkg.Impl, "renderer", self.renderer); + step.addOption(ExeEntrypoint, "exe_entrypoint", self.exe_entrypoint); + step.addOption(WasmTarget, "wasm_target", self.wasm_target); + step.addOption(bool, "wasm_shared", self.wasm_shared); + + // Our version. We also add the string version so we don't need + // to do any allocations at runtime. + var buf: [64]u8 = undefined; + step.addOption(std.SemanticVersion, "app_version", self.version); + step.addOption([:0]const u8, "app_version_string", try std.fmt.bufPrintZ( + &buf, + "{}", + .{self.version}, + )); } /// Rehydrate our BuildConfig from the comptime options. Note that not all @@ -41,11 +65,15 @@ pub const BuildConfig = struct { /// to see what is and isn't available. pub fn fromOptions() BuildConfig { return .{ + .version = options.app_version, .flatpak = options.flatpak, .libadwaita = options.libadwaita, .app_runtime = std.meta.stringToEnum(apprt.Runtime, @tagName(options.app_runtime)).?, .font_backend = std.meta.stringToEnum(font.Backend, @tagName(options.font_backend)).?, .renderer = std.meta.stringToEnum(rendererpkg.Impl, @tagName(options.renderer)).?, + .exe_entrypoint = std.meta.stringToEnum(ExeEntrypoint, @tagName(options.exe_entrypoint)).?, + .wasm_target = std.meta.stringToEnum(WasmTarget, @tagName(options.wasm_target)).?, + .wasm_shared = options.wasm_shared, }; } }; @@ -62,6 +90,7 @@ pub const artifact = Artifact.detect(); /// top-level so its a bit cleaner to use throughout the code. See the doc /// comments in BuildConfig for details on each. pub const config = BuildConfig.fromOptions(); +pub const exe_entrypoint = config.exe_entrypoint; pub const flatpak = options.flatpak; pub const app_runtime: apprt.Runtime = config.app_runtime; pub const font_backend: font.Backend = config.font_backend; @@ -94,3 +123,20 @@ pub const Artifact = enum { }; } }; + +/// The possible entrypoints for the exe artifact. This has no effect on +/// other artifact types (i.e. lib, wasm_module). +/// +/// The whole existence of this enum is to workaround the fact that Zig +/// doesn't allow the main function to be in a file in a subdirctory +/// from the "root" of the module, and I don't want to pollute our root +/// directory with a bunch of individual zig files for each entrypoint. +/// +/// Therefore, main.zig uses this to switch between the different entrypoints. +pub const ExeEntrypoint = enum { + ghostty, + helpgen, + mdgen_ghostty_1, + mdgen_ghostty_5, + bench_parser, +}; diff --git a/src/main.zig b/src/main.zig index fe3c24925..b5307340d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,309 +1,10 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const Allocator = std.mem.Allocator; const build_config = @import("build_config.zig"); -const options = @import("build_options"); -const glfw = @import("glfw"); -const glslang = @import("glslang"); -const macos = @import("macos"); -const oni = @import("oniguruma"); -const cli = @import("cli.zig"); -const internal_os = @import("os/main.zig"); -const xev = @import("xev"); -const fontconfig = @import("fontconfig"); -const harfbuzz = @import("harfbuzz"); -const renderer = @import("renderer.zig"); -const apprt = @import("apprt.zig"); -const App = @import("App.zig"); -const Ghostty = @import("main_c.zig").Ghostty; - -/// Global process state. This is initialized in main() for exe artifacts -/// and by ghostty_init() for lib artifacts. This should ONLY be used by -/// the C API. The Zig API should NOT use any global state and should -/// rely on allocators being passed in as parameters. -pub var state: GlobalState = undefined; - -/// The return type for main() depends on the build artifact. -const MainReturn = switch (build_config.artifact) { - .lib => noreturn, - else => void, +// See build_config.ExeEntrypoint for why we do this. +pub usingnamespace switch (build_config.exe_entrypoint) { + .ghostty => @import("main_ghostty.zig"), + .helpgen => @import("helpgen.zig"), + .mdgen_ghostty_1 => @import("build/mdgen/main_ghostty_1.zig"), + .mdgen_ghostty_5 => @import("build/mdgen/main_ghostty_5.zig"), + .bench_parser => @import("bench/parser.zig"), }; - -pub fn main() !MainReturn { - // We first start by initializing our global state. This will setup - // process-level state we need to run the terminal. The reason we use - // a global is because the C API needs to be able to access this state; - // no other Zig code should EVER access the global state. - state.init() catch |err| { - const stderr = std.io.getStdErr().writer(); - defer std.os.exit(1); - const ErrSet = @TypeOf(err) || error{Unknown}; - switch (@as(ErrSet, @errorCast(err))) { - error.MultipleActions => try stderr.print( - "Error: multiple CLI actions specified. You must specify only one\n" ++ - "action starting with the `+` character.\n", - .{}, - ), - - error.InvalidAction => try stderr.print( - "Error: unknown CLI action specified. CLI actions are specified with\n" ++ - "the '+' character.\n", - .{}, - ), - - else => try stderr.print("invalid CLI invocation err={}\n", .{err}), - } - }; - defer state.deinit(); - const alloc = state.alloc; - - if (comptime builtin.mode == .Debug) { - std.log.warn("This is a debug build. Performance will be very poor.", .{}); - std.log.warn("You should only use a debug build for developing Ghostty.", .{}); - std.log.warn("Otherwise, please rebuild in a release mode.", .{}); - } - - // Execute our action if we have one - if (state.action) |action| { - std.log.info("executing CLI action={}", .{action}); - std.os.exit(action.run(alloc) catch |err| err: { - std.log.err("CLI action failed error={}", .{err}); - break :err 1; - }); - return; - } - - if (comptime build_config.app_runtime == .none) { - const stdout = std.io.getStdOut().writer(); - try stdout.print("Usage: ghostty + [flags]\n\n", .{}); - try stdout.print( - \\This is the Ghostty helper CLI that accompanies the graphical Ghostty app. - \\To launch the terminal directly, please launch the graphical app - \\(i.e. Ghostty.app on macOS). This CLI can be used to perform various - \\actions such as inspecting the version, listing fonts, etc. - \\ - \\We don't have proper help output yet, sorry! Please refer to the - \\source code or Discord community for help for now. We'll fix this in time. - , - .{}, - ); - - std.os.exit(0); - } - - // Create our app state - var app = try App.create(alloc); - defer app.destroy(); - - // Create our runtime app - var app_runtime = try apprt.App.init(app, .{}); - defer app_runtime.terminate(); - - // Run the GUI event loop - try app_runtime.run(); -} - -pub const std_options = struct { - // Our log level is always at least info in every build mode. - pub const log_level: std.log.Level = switch (builtin.mode) { - .Debug => .debug, - else => .info, - }; - - // The function std.log will call. - pub fn logFn( - comptime level: std.log.Level, - comptime scope: @TypeOf(.EnumLiteral), - comptime format: []const u8, - args: anytype, - ) void { - // Stuff we can do before the lock - const level_txt = comptime level.asText(); - const prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; - - // Lock so we are thread-safe - std.debug.getStderrMutex().lock(); - defer std.debug.getStderrMutex().unlock(); - - // On Mac, we use unified logging. To view this: - // - // sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"' - // - if (builtin.target.isDarwin()) { - // Convert our levels to Mac levels - const mac_level: macos.os.LogType = switch (level) { - .debug => .debug, - .info => .info, - .warn => .err, - .err => .fault, - }; - - // Initialize a logger. This is slow to do on every operation - // but we shouldn't be logging too much. - const logger = macos.os.Log.create("com.mitchellh.ghostty", @tagName(scope)); - defer logger.release(); - logger.log(std.heap.c_allocator, mac_level, format, args); - } - - switch (state.logging) { - .disabled => {}, - - .stderr => { - // Always try default to send to stderr - const stderr = std.io.getStdErr().writer(); - nosuspend stderr.print(level_txt ++ prefix ++ format ++ "\n", args) catch return; - }, - } - } -}; - -/// This represents the global process state. There should only -/// be one of these at any given moment. This is extracted into a dedicated -/// struct because it is reused by main and the static C lib. -pub const GlobalState = struct { - const GPA = std.heap.GeneralPurposeAllocator(.{}); - - gpa: ?GPA, - alloc: std.mem.Allocator, - action: ?cli.Action, - logging: Logging, - - /// The app resources directory, equivalent to zig-out/share when we build - /// from source. This is null if we can't detect it. - resources_dir: ?[]const u8, - - /// Where logging should go - pub const Logging = union(enum) { - disabled: void, - stderr: void, - }; - - /// Initialize the global state. - pub fn init(self: *GlobalState) !void { - // Initialize ourself to nothing so we don't have any extra state. - // IMPORTANT: this MUST be initialized before any log output because - // the log function uses the global state. - self.* = .{ - .gpa = null, - .alloc = undefined, - .action = null, - .logging = .{ .stderr = {} }, - .resources_dir = null, - }; - errdefer self.deinit(); - - self.gpa = gpa: { - // Use the libc allocator if it is available because it is WAY - // faster than GPA. We only do this in release modes so that we - // can get easy memory leak detection in debug modes. - if (builtin.link_libc) { - if (switch (builtin.mode) { - .ReleaseSafe, .ReleaseFast => true, - - // We also use it if we can detect we're running under - // Valgrind since Valgrind only instruments the C allocator - else => std.valgrind.runningOnValgrind() > 0, - }) break :gpa null; - } - - break :gpa GPA{}; - }; - - self.alloc = if (self.gpa) |*value| - value.allocator() - else if (builtin.link_libc) - std.heap.c_allocator - else - unreachable; - - // We first try to parse any action that we may be executing. - self.action = try cli.Action.detectCLI(self.alloc); - - // If we have an action executing, we disable logging by default - // since we write to stderr we don't want logs messing up our - // output. - if (self.action != null) self.logging = .{ .disabled = {} }; - - // For lib mode we always disable stderr logging by default. - if (comptime build_config.app_runtime == .none) { - self.logging = .{ .disabled = {} }; - } - - // I don't love the env var name but I don't have it in my heart - // to parse CLI args 3 times (once for actions, once for config, - // maybe once for logging) so for now this is an easy way to do - // this. Env vars are useful for logging too because they are - // easy to set. - if ((try internal_os.getenv(self.alloc, "GHOSTTY_LOG"))) |v| { - defer v.deinit(self.alloc); - if (v.value.len > 0) { - self.logging = .{ .stderr = {} }; - } - } - - // Output some debug information right away - std.log.info("ghostty version={s}", .{build_config.version_string}); - std.log.info("runtime={}", .{build_config.app_runtime}); - std.log.info("font_backend={}", .{build_config.font_backend}); - std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()}); - if (comptime build_config.font_backend.hasFontconfig()) { - std.log.info("dependency fontconfig={d}", .{fontconfig.version()}); - } - std.log.info("renderer={}", .{renderer.Renderer}); - std.log.info("libxev backend={}", .{xev.backend}); - - // First things first, we fix our file descriptors - internal_os.fixMaxFiles(); - - // We need to make sure the process locale is set properly. Locale - // affects a lot of behaviors in a shell. - try internal_os.ensureLocale(self.alloc); - - // Initialize glslang for shader compilation - try glslang.init(); - - // Initialize oniguruma for regex - try oni.init(&.{oni.Encoding.utf8}); - - // Find our resources directory once for the app so every launch - // hereafter can use this cached value. - self.resources_dir = try internal_os.resourcesDir(self.alloc); - errdefer if (self.resources_dir) |dir| self.alloc.free(dir); - } - - /// Cleans up the global state. This doesn't _need_ to be called but - /// doing so in dev modes will check for memory leaks. - pub fn deinit(self: *GlobalState) void { - if (self.resources_dir) |dir| self.alloc.free(dir); - - if (self.gpa) |*value| { - // We want to ensure that we deinit the GPA because this is - // the point at which it will output if there were safety violations. - _ = value.deinit(); - } - } -}; -test { - _ = @import("circ_buf.zig"); - _ = @import("pty.zig"); - _ = @import("Command.zig"); - _ = @import("font/main.zig"); - _ = @import("apprt.zig"); - _ = @import("renderer.zig"); - _ = @import("termio.zig"); - _ = @import("input.zig"); - _ = @import("cli.zig"); - _ = @import("surface_mouse.zig"); - - // Libraries - _ = @import("segmented_pool.zig"); - _ = @import("inspector/main.zig"); - _ = @import("terminal/main.zig"); - _ = @import("terminfo/main.zig"); - - // TODO - _ = @import("blocking_queue.zig"); - _ = @import("config.zig"); - _ = @import("lru.zig"); -} diff --git a/src/main_c.zig b/src/main_c.zig index 992c20d19..6999341d1 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -6,6 +6,7 @@ // This currently isn't supported as a general purpose embedding API. // This is currently used only to embed ghostty within a macOS app. However, // it could be expanded to be general purpose in the future. + const std = @import("std"); const assert = std.debug.assert; const builtin = @import("builtin"); diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig new file mode 100644 index 000000000..6c2958ec9 --- /dev/null +++ b/src/main_ghostty.zig @@ -0,0 +1,315 @@ +//! The main entrypoint for the `ghostty` application. This also serves +//! as the process initialization code for the `libghostty` library. + +const std = @import("std"); +const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; +const build_config = @import("build_config.zig"); +const options = @import("build_options"); +const glfw = @import("glfw"); +const glslang = @import("glslang"); +const macos = @import("macos"); +const oni = @import("oniguruma"); +const cli = @import("cli.zig"); +const internal_os = @import("os/main.zig"); +const xev = @import("xev"); +const fontconfig = @import("fontconfig"); +const harfbuzz = @import("harfbuzz"); +const renderer = @import("renderer.zig"); +const apprt = @import("apprt.zig"); + +const App = @import("App.zig"); +const Ghostty = @import("main_c.zig").Ghostty; + +/// Global process state. This is initialized in main() for exe artifacts +/// and by ghostty_init() for lib artifacts. This should ONLY be used by +/// the C API. The Zig API should NOT use any global state and should +/// rely on allocators being passed in as parameters. +pub var state: GlobalState = undefined; + +/// The return type for main() depends on the build artifact. The lib build +/// also calls "main" in order to run the CLI actions, but it calls it as +/// an API and not an entrypoint. +const MainReturn = switch (build_config.artifact) { + .lib => noreturn, + else => void, +}; + +pub fn main() !MainReturn { + // We first start by initializing our global state. This will setup + // process-level state we need to run the terminal. The reason we use + // a global is because the C API needs to be able to access this state; + // no other Zig code should EVER access the global state. + state.init() catch |err| { + const stderr = std.io.getStdErr().writer(); + defer std.os.exit(1); + const ErrSet = @TypeOf(err) || error{Unknown}; + switch (@as(ErrSet, @errorCast(err))) { + error.MultipleActions => try stderr.print( + "Error: multiple CLI actions specified. You must specify only one\n" ++ + "action starting with the `+` character.\n", + .{}, + ), + + error.InvalidAction => try stderr.print( + "Error: unknown CLI action specified. CLI actions are specified with\n" ++ + "the '+' character.\n", + .{}, + ), + + else => try stderr.print("invalid CLI invocation err={}\n", .{err}), + } + }; + defer state.deinit(); + const alloc = state.alloc; + + if (comptime builtin.mode == .Debug) { + std.log.warn("This is a debug build. Performance will be very poor.", .{}); + std.log.warn("You should only use a debug build for developing Ghostty.", .{}); + std.log.warn("Otherwise, please rebuild in a release mode.", .{}); + } + + // Execute our action if we have one + if (state.action) |action| { + std.log.info("executing CLI action={}", .{action}); + std.os.exit(action.run(alloc) catch |err| err: { + std.log.err("CLI action failed error={}", .{err}); + break :err 1; + }); + return; + } + + if (comptime build_config.app_runtime == .none) { + const stdout = std.io.getStdOut().writer(); + try stdout.print("Usage: ghostty + [flags]\n\n", .{}); + try stdout.print( + \\This is the Ghostty helper CLI that accompanies the graphical Ghostty app. + \\To launch the terminal directly, please launch the graphical app + \\(i.e. Ghostty.app on macOS). This CLI can be used to perform various + \\actions such as inspecting the version, listing fonts, etc. + \\ + \\We don't have proper help output yet, sorry! Please refer to the + \\source code or Discord community for help for now. We'll fix this in time. + , + .{}, + ); + + std.os.exit(0); + } + + // Create our app state + var app = try App.create(alloc); + defer app.destroy(); + + // Create our runtime app + var app_runtime = try apprt.App.init(app, .{}); + defer app_runtime.terminate(); + + // Run the GUI event loop + try app_runtime.run(); +} + +pub const std_options = struct { + // Our log level is always at least info in every build mode. + pub const log_level: std.log.Level = switch (builtin.mode) { + .Debug => .debug, + else => .info, + }; + + // The function std.log will call. + pub fn logFn( + comptime level: std.log.Level, + comptime scope: @TypeOf(.EnumLiteral), + comptime format: []const u8, + args: anytype, + ) void { + // Stuff we can do before the lock + const level_txt = comptime level.asText(); + const prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; + + // Lock so we are thread-safe + std.debug.getStderrMutex().lock(); + defer std.debug.getStderrMutex().unlock(); + + // On Mac, we use unified logging. To view this: + // + // sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"' + // + if (builtin.target.isDarwin()) { + // Convert our levels to Mac levels + const mac_level: macos.os.LogType = switch (level) { + .debug => .debug, + .info => .info, + .warn => .err, + .err => .fault, + }; + + // Initialize a logger. This is slow to do on every operation + // but we shouldn't be logging too much. + const logger = macos.os.Log.create("com.mitchellh.ghostty", @tagName(scope)); + defer logger.release(); + logger.log(std.heap.c_allocator, mac_level, format, args); + } + + switch (state.logging) { + .disabled => {}, + + .stderr => { + // Always try default to send to stderr + const stderr = std.io.getStdErr().writer(); + nosuspend stderr.print(level_txt ++ prefix ++ format ++ "\n", args) catch return; + }, + } + } +}; + +/// This represents the global process state. There should only +/// be one of these at any given moment. This is extracted into a dedicated +/// struct because it is reused by main and the static C lib. +pub const GlobalState = struct { + const GPA = std.heap.GeneralPurposeAllocator(.{}); + + gpa: ?GPA, + alloc: std.mem.Allocator, + action: ?cli.Action, + logging: Logging, + + /// The app resources directory, equivalent to zig-out/share when we build + /// from source. This is null if we can't detect it. + resources_dir: ?[]const u8, + + /// Where logging should go + pub const Logging = union(enum) { + disabled: void, + stderr: void, + }; + + /// Initialize the global state. + pub fn init(self: *GlobalState) !void { + // Initialize ourself to nothing so we don't have any extra state. + // IMPORTANT: this MUST be initialized before any log output because + // the log function uses the global state. + self.* = .{ + .gpa = null, + .alloc = undefined, + .action = null, + .logging = .{ .stderr = {} }, + .resources_dir = null, + }; + errdefer self.deinit(); + + self.gpa = gpa: { + // Use the libc allocator if it is available because it is WAY + // faster than GPA. We only do this in release modes so that we + // can get easy memory leak detection in debug modes. + if (builtin.link_libc) { + if (switch (builtin.mode) { + .ReleaseSafe, .ReleaseFast => true, + + // We also use it if we can detect we're running under + // Valgrind since Valgrind only instruments the C allocator + else => std.valgrind.runningOnValgrind() > 0, + }) break :gpa null; + } + + break :gpa GPA{}; + }; + + self.alloc = if (self.gpa) |*value| + value.allocator() + else if (builtin.link_libc) + std.heap.c_allocator + else + unreachable; + + // We first try to parse any action that we may be executing. + self.action = try cli.Action.detectCLI(self.alloc); + + // If we have an action executing, we disable logging by default + // since we write to stderr we don't want logs messing up our + // output. + if (self.action != null) self.logging = .{ .disabled = {} }; + + // For lib mode we always disable stderr logging by default. + if (comptime build_config.app_runtime == .none) { + self.logging = .{ .disabled = {} }; + } + + // I don't love the env var name but I don't have it in my heart + // to parse CLI args 3 times (once for actions, once for config, + // maybe once for logging) so for now this is an easy way to do + // this. Env vars are useful for logging too because they are + // easy to set. + if ((try internal_os.getenv(self.alloc, "GHOSTTY_LOG"))) |v| { + defer v.deinit(self.alloc); + if (v.value.len > 0) { + self.logging = .{ .stderr = {} }; + } + } + + // Output some debug information right away + std.log.info("ghostty version={s}", .{build_config.version_string}); + std.log.info("runtime={}", .{build_config.app_runtime}); + std.log.info("font_backend={}", .{build_config.font_backend}); + std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()}); + if (comptime build_config.font_backend.hasFontconfig()) { + std.log.info("dependency fontconfig={d}", .{fontconfig.version()}); + } + std.log.info("renderer={}", .{renderer.Renderer}); + std.log.info("libxev backend={}", .{xev.backend}); + + // First things first, we fix our file descriptors + internal_os.fixMaxFiles(); + + // We need to make sure the process locale is set properly. Locale + // affects a lot of behaviors in a shell. + try internal_os.ensureLocale(self.alloc); + + // Initialize glslang for shader compilation + try glslang.init(); + + // Initialize oniguruma for regex + try oni.init(&.{oni.Encoding.utf8}); + + // Find our resources directory once for the app so every launch + // hereafter can use this cached value. + self.resources_dir = try internal_os.resourcesDir(self.alloc); + errdefer if (self.resources_dir) |dir| self.alloc.free(dir); + } + + /// Cleans up the global state. This doesn't _need_ to be called but + /// doing so in dev modes will check for memory leaks. + pub fn deinit(self: *GlobalState) void { + if (self.resources_dir) |dir| self.alloc.free(dir); + + if (self.gpa) |*value| { + // We want to ensure that we deinit the GPA because this is + // the point at which it will output if there were safety violations. + _ = value.deinit(); + } + } +}; + +test { + _ = @import("circ_buf.zig"); + _ = @import("pty.zig"); + _ = @import("Command.zig"); + _ = @import("font/main.zig"); + _ = @import("apprt.zig"); + _ = @import("renderer.zig"); + _ = @import("termio.zig"); + _ = @import("input.zig"); + _ = @import("cli.zig"); + _ = @import("surface_mouse.zig"); + + // Libraries + _ = @import("segmented_pool.zig"); + _ = @import("inspector/main.zig"); + _ = @import("terminal/main.zig"); + _ = @import("terminfo/main.zig"); + + // TODO + _ = @import("blocking_queue.zig"); + _ = @import("config.zig"); + _ = @import("lru.zig"); +}