From 9bd527fe00a89aa34f396e82609e8913c2410e31 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 14 Feb 2023 15:53:28 -0800 Subject: [PATCH] macos: config API --- build.zig | 12 ---- include/ghostty.h | 16 ++++- macos/Sources/GhosttyApp.swift | 61 +++++++++++++++++-- src/App.zig | 10 +--- src/config.zig | 53 +++++++++++++++++ src/main.zig | 89 +--------------------------- src/main_c.zig | 103 ++++++++++++++++++++++++++++++++- 7 files changed, 229 insertions(+), 115 deletions(-) diff --git a/build.zig b/build.zig index f417039f3..b877f1383 100644 --- a/build.zig +++ b/build.zig @@ -134,18 +134,6 @@ pub fn build(b: *std.build.Builder) !void { b.installFile("dist/macos/Ghostty.icns", "Ghostty.app/Contents/Resources/Ghostty.icns"); } - // c lib - { - const static_lib = b.addStaticLibrary("ghostty", "src/main_c.zig"); - static_lib.setBuildMode(mode); - static_lib.setTarget(target); - static_lib.install(); - static_lib.linkLibC(); - static_lib.addOptions("build_options", exe_options); - _ = try addDeps(b, static_lib, true); - b.default_step.dependOn(&static_lib.step); - } - // On Mac we can build the app. const macapp = b.step("macapp", "Build macOS app using XCode."); if (builtin.target.isDarwin()) { diff --git a/include/ghostty.h b/include/ghostty.h index 18940d6c5..36935c815 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -1,3 +1,10 @@ +// Ghostty embedding API. The documentation for the embedding API is +// only within the Zig source files that define the implementations. This +// isn't meant to be a general purpose embedding API (yet) so there hasn't +// been documentation or example work beyond that. +// +// The only consumer of this API is the macOS app, but the API is built to +// be more general purpose. #ifndef GHOSTTY_H #define GHOSTTY_H @@ -7,9 +14,14 @@ extern "C" { #include -#define GHOSTTY_SUCCESS 0 +typedef void *ghostty_t; +typedef void *ghostty_config_t; -int ghostty_init(void); +ghostty_t ghostty_init(void); +ghostty_config_t ghostty_config_new(ghostty_t); +void ghostty_config_free(ghostty_t, ghostty_config_t); +void ghostty_config_load_string(ghostty_t, ghostty_config_t, const char *, uintptr_t); +void ghostty_config_finalize(ghostty_config_t); #ifdef __cplusplus } diff --git a/macos/Sources/GhosttyApp.swift b/macos/Sources/GhosttyApp.swift index 53829b678..2373ea0a3 100644 --- a/macos/Sources/GhosttyApp.swift +++ b/macos/Sources/GhosttyApp.swift @@ -1,15 +1,68 @@ +import OSLog import SwiftUI import GhosttyKit @main struct GhosttyApp: App { - init() { - assert(ghostty_init() == GHOSTTY_SUCCESS, "ghostty failed to initialize"); - } + static let logger = Logger( + subsystem: Bundle.main.bundleIdentifier!, + category: String(describing: GhosttyApp.self) + ) + + /// The ghostty global state. Only one per process. + @StateObject private var ghostty = GhosttyState() var body: some Scene { WindowGroup { - Text("Hello!").font(.largeTitle) + switch ghostty.readiness { + case .error: + Text("Error") + case .ready: + Text("Hello!").font(.largeTitle) + } } } } + +class GhosttyState: ObservableObject { + enum Readiness { + case error, ready + } + + /// The readiness value of the state. + var readiness: Readiness { ghostty != nil ? .ready : .error } + + /// The ghostty global state. + var ghostty: ghostty_t? = nil + + /// The ghostty global configuration. + var config: ghostty_config_t? = nil + + init() { + // Initialize ghostty global state. This happens once per process. + guard let g = ghostty_init() else { + GhosttyApp.logger.critical("ghostty_init failed") + return + } + + // Initialize the global configuration. + guard let cfg = ghostty_config_new(g) else { + GhosttyApp.logger.critical("ghostty_config_new failed") + return + } + + // TODO: we'd probably do some config loading here... for now we'd + // have to do this synchronously. When we support config updating we can do + // this async and update later. + + // Finalize will make our defaults available. + ghostty_config_finalize(cfg) + + ghostty = g; + config = cfg; + } + + deinit { + ghostty_config_free(ghostty, config) + } +} diff --git a/src/App.zig b/src/App.zig index 402937751..aaeca8f86 100644 --- a/src/App.zig +++ b/src/App.zig @@ -297,13 +297,5 @@ pub const Wasm = if (!builtin.target.isWasm()) struct {} else struct { }; pub const CAPI = struct { - const ProcessState = @import("main.zig").ProcessState; - var state: ?ProcessState = null; - - export fn ghostty_init() c_int { - assert(state == null); - state = undefined; - ProcessState.init(&state.?); - return 0; - } + const Ghostty = @import("main_c.zig").Ghostty; }; diff --git a/src/config.zig b/src/config.zig index 0e9ee8943..ece999dac 100644 --- a/src/config.zig +++ b/src/config.zig @@ -583,6 +583,59 @@ pub const Wasm = if (!builtin.target.isWasm()) struct {} else struct { } }; +// Wasm API. +pub const CAPI = struct { + const Ghostty = @import("main_c.zig").Ghostty; + const cli_args = @import("cli_args.zig"); + + /// Create a new configuration filled with the initial default values. + export fn ghostty_config_new(g: *Ghostty) ?*Config { + const result = g.alloc.create(Config) catch |err| { + log.err("error allocating config err={}", .{err}); + return null; + }; + + result.* = Config.default(g.alloc) catch |err| { + log.err("error creating config err={}", .{err}); + return null; + }; + + return result; + } + + export fn ghostty_config_free(g: *Ghostty, ptr: ?*Config) void { + if (ptr) |v| { + v.deinit(); + g.alloc.destroy(v); + } + } + + /// Load the configuration from a string in the same format as + /// the file-based syntax for the desktop version of the terminal. + export fn ghostty_config_load_string( + g: *Ghostty, + self: *Config, + str: [*]const u8, + len: usize, + ) void { + config_load_string_(g, self, str[0..len]) catch |err| { + log.err("error loading config err={}", .{err}); + }; + } + + fn config_load_string_(g: *Ghostty, self: *Config, str: []const u8) !void { + var fbs = std.io.fixedBufferStream(str); + var iter = cli_args.lineIterator(fbs.reader()); + try cli_args.parse(Config, g.alloc, self, &iter); + } + + export fn ghostty_config_finalize(self: *Config) void { + self.finalize() catch |err| { + log.err("error finalizing config err={}", .{err}); + }; + } +}; + test { std.testing.refAllDecls(@This()); } diff --git a/src/main.zig b/src/main.zig index dad323b31..b4b1b7baf 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,100 +2,17 @@ const std = @import("std"); const builtin = @import("builtin"); const options = @import("build_options"); const glfw = @import("glfw"); -const fontconfig = @import("fontconfig"); -const freetype = @import("freetype"); -const harfbuzz = @import("harfbuzz"); const macos = @import("macos"); -const tracy = @import("tracy"); -const xev = @import("xev"); -const renderer = @import("renderer.zig"); const xdg = @import("xdg.zig"); -const internal_os = @import("os/main.zig"); const App = @import("App.zig"); const cli_args = @import("cli_args.zig"); const Config = @import("config.zig").Config; - -/// ProcessState 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. -/// -/// ProcessState.init should be one of the first things ever called -/// when using Ghostty. Ghostty calls this for you so this is more of a note -/// for maintainers. -pub const ProcessState = struct { - const GPA = std.heap.GeneralPurposeAllocator(.{}); - - gpa: ?GPA, - alloc: std.mem.Allocator, - - pub fn init(self: *ProcessState) void { - // Output some debug information right away - std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()}); - if (options.fontconfig) { - 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. - internal_os.ensureLocale(); - - // Initialize ourself to nothing so we don't have any extra state. - self.* = .{ - .gpa = null, - .alloc = undefined, - }; - errdefer self.deinit(); - - self.gpa = gpa: { - // Use the libc allocator if it is available beacuse 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 = alloc: { - const base = if (self.gpa) |*value| - value.allocator() - else if (builtin.link_libc) - std.heap.c_allocator - else - unreachable; - - // If we're tracing, wrap the allocator - if (!tracy.enabled) break :alloc base; - var tracy_alloc = tracy.allocator(base, null); - break :alloc tracy_alloc.allocator(); - }; - } - - pub fn deinit(self: *ProcessState) void { - 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(); - } - } -}; +const Ghostty = @import("main_c.zig").Ghostty; pub fn main() !void { - var state: ProcessState = undefined; - ProcessState.init(&state); + var state: Ghostty = undefined; + Ghostty.init(&state); defer state.deinit(); const alloc = state.alloc; diff --git a/src/main_c.zig b/src/main_c.zig index d00a5e62b..04cde6282 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -2,10 +2,109 @@ // within other applications. Depending on the build settings some APIs // may not be available (i.e. embedding into macOS exposes various Metal // support). +// +// 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"); +const options = @import("build_options"); +const fontconfig = @import("fontconfig"); +const harfbuzz = @import("harfbuzz"); +const renderer = @import("renderer.zig"); +const tracy = @import("tracy"); +const xev = @import("xev"); +const internal_os = @import("os/main.zig"); const main = @import("main.zig"); -pub usingnamespace @import("App.zig").CAPI; - +/// Global options so we can log. This is identical to main. pub const std_options = main.std_options; + +pub usingnamespace @import("App.zig").CAPI; +pub usingnamespace @import("config.zig").CAPI; + +/// Initialize ghostty global state. It is possible to have more than +/// one global state but it has zero practical benefit. +export fn ghostty_init() ?*Ghostty { + assert(builtin.link_libc); + const alloc = std.heap.c_allocator; + const g = alloc.create(Ghostty) catch return null; + Ghostty.init(g); + return g; +} + +/// 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. +/// +/// init should be one of the first things ever called when using Ghostty. +pub const Ghostty = struct { + const GPA = std.heap.GeneralPurposeAllocator(.{}); + + gpa: ?GPA, + alloc: std.mem.Allocator, + + pub fn init(self: *Ghostty) void { + // Output some debug information right away + std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()}); + if (options.fontconfig) { + 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. + internal_os.ensureLocale(); + + // Initialize ourself to nothing so we don't have any extra state. + self.* = .{ + .gpa = null, + .alloc = undefined, + }; + errdefer self.deinit(); + + self.gpa = gpa: { + // Use the libc allocator if it is available beacuse 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 = alloc: { + const base = if (self.gpa) |*value| + value.allocator() + else if (builtin.link_libc) + std.heap.c_allocator + else + unreachable; + + // If we're tracing, wrap the allocator + if (!tracy.enabled) break :alloc base; + var tracy_alloc = tracy.allocator(base, null); + break :alloc tracy_alloc.allocator(); + }; + } + + pub fn deinit(self: *Ghostty) void { + 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(); + } + } +};