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.
This commit is contained in:
Mitchell Hashimoto
2026-04-06 14:13:20 -07:00
parent 05fb57dd40
commit f567f7f46d
5 changed files with 109 additions and 1 deletions

1
example/.gitignore vendored
View File

@@ -3,3 +3,4 @@ dist/
node_modules/
example.wasm*
build/
.build/

View File

@@ -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"
),
]
)

View File

@@ -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
```

View File

@@ -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<GhosttyFormatterTerminalOptions>.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<UInt8>?
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)

View File

@@ -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,
}},
});