From f567f7f46d0b60da3fddb18070e74b1c0deb074f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 14:13:20 -0700 Subject: [PATCH] build: add GhosttyVt module map to xcframework and Swift example The xcframework now generates its own headers directory with a GhosttyVt module map instead of reusing include/ directly, which contains the GhosttyKit module map for the macOS app. The generated directory copies the ghostty headers and adds a module.modulemap that exposes ghostty/vt.h as the umbrella header. A new swift-vt-xcframework example demonstrates consuming the xcframework from a Swift Package. It creates a terminal, writes VT sequences, and formats the output as plain text, verifying the full round-trip works with swift build and swift run. --- example/.gitignore | 1 + example/swift-vt-xcframework/Package.swift | 21 +++++++++ example/swift-vt-xcframework/README.md | 23 ++++++++++ .../swift-vt-xcframework/Sources/main.swift | 45 +++++++++++++++++++ src/build/GhosttyLibVt.zig | 20 ++++++++- 5 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 example/swift-vt-xcframework/Package.swift create mode 100644 example/swift-vt-xcframework/README.md create mode 100644 example/swift-vt-xcframework/Sources/main.swift diff --git a/example/.gitignore b/example/.gitignore index 6f372bc4d..9f88ccfeb 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -3,3 +3,4 @@ dist/ node_modules/ example.wasm* build/ +.build/ diff --git a/example/swift-vt-xcframework/Package.swift b/example/swift-vt-xcframework/Package.swift new file mode 100644 index 000000000..a831a42c8 --- /dev/null +++ b/example/swift-vt-xcframework/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "swift-vt-xcframework", + platforms: [.macOS(.v13)], + targets: [ + .executableTarget( + name: "swift-vt-xcframework", + dependencies: ["GhosttyVt"], + path: "Sources", + linkerSettings: [ + .linkedLibrary("c++"), + ] + ), + .binaryTarget( + name: "GhosttyVt", + path: "../../zig-out/lib/ghostty-vt.xcframework" + ), + ] +) diff --git a/example/swift-vt-xcframework/README.md b/example/swift-vt-xcframework/README.md new file mode 100644 index 000000000..3bbe8948c --- /dev/null +++ b/example/swift-vt-xcframework/README.md @@ -0,0 +1,23 @@ +# swift-vt-xcframework + +Demonstrates consuming libghostty-vt from a Swift Package using the +pre-built XCFramework. Creates a terminal, writes VT sequences into it, +and formats the screen contents as plain text. + +This example requires the XCFramework to be built first. + +## Building + +First, build the XCFramework from the repository root: + +```shell-session +zig build -Demit-lib-vt +``` + +Then build and run the Swift package: + +```shell-session +cd example/swift-vt-xcframework +swift build +swift run +``` diff --git a/example/swift-vt-xcframework/Sources/main.swift b/example/swift-vt-xcframework/Sources/main.swift new file mode 100644 index 000000000..e7d15fb89 --- /dev/null +++ b/example/swift-vt-xcframework/Sources/main.swift @@ -0,0 +1,45 @@ +import GhosttyVt + +// Create a terminal with a small grid +var terminal: GhosttyTerminal? +var opts = GhosttyTerminalOptions( + cols: 80, + rows: 24, + max_scrollback: 0 +) +let result = ghostty_terminal_new(nil, &terminal, opts) +guard result == GHOSTTY_SUCCESS, let terminal else { + fatalError("Failed to create terminal") +} + +// Write some VT-encoded content +let text = "Hello from \u{1b}[1mSwift\u{1b}[0m via xcframework!\r\n" +text.withCString { ptr in + ghostty_terminal_vt_write(terminal, ptr, strlen(ptr)) +} + +// Format the terminal contents as plain text +var fmtOpts = GhosttyFormatterTerminalOptions() +fmtOpts.size = MemoryLayout.size +fmtOpts.emit = GHOSTTY_FORMATTER_FORMAT_PLAIN +fmtOpts.trim = true + +var formatter: GhosttyFormatter? +let fmtResult = ghostty_formatter_terminal_new(nil, &formatter, terminal, fmtOpts) +guard fmtResult == GHOSTTY_SUCCESS, let formatter else { + fatalError("Failed to create formatter") +} + +var buf: UnsafeMutablePointer? +var len: Int = 0 +let allocResult = ghostty_formatter_format_alloc(formatter, nil, &buf, &len) +guard allocResult == GHOSTTY_SUCCESS, let buf else { + fatalError("Failed to format") +} + +print("Plain text (\(len) bytes):") +print(String(cString: buf)) + +ghostty_free(nil, buf, len) +ghostty_formatter_free(formatter) +ghostty_terminal_free(terminal) diff --git a/src/build/GhosttyLibVt.zig b/src/build/GhosttyLibVt.zig index 55f22e232..904954650 100644 --- a/src/build/GhosttyLibVt.zig +++ b/src/build/GhosttyLibVt.zig @@ -342,12 +342,30 @@ pub fn xcframework( ) *XCFrameworkStep { assert(lib_vt.kind == .static); const b = lib_vt.step.owner; + + // Generate a headers directory with a module map for Swift PM. + // We can't use include/ directly because it contains a module map + // for GhosttyKit (the macOS app library). + const wf = b.addWriteFiles(); + _ = wf.addCopyDirectory( + b.path("include/ghostty"), + "ghostty", + .{ .include_extensions = &.{".h"} }, + ); + _ = wf.add("module.modulemap", + \\module GhosttyVt { + \\ umbrella header "ghostty/vt.h" + \\ export * + \\} + \\ + ); + return XCFrameworkStep.create(b, .{ .name = "ghostty-vt", .out_path = b.pathJoin(&.{ b.install_prefix, "lib/ghostty-vt.xcframework" }), .libraries = &.{.{ .library = lib_vt.output, - .headers = b.path("include/ghostty"), + .headers = wf.getDirectory(), .dsym = null, }}, });