libghostty: add cpp-vt-stream example and fix C++ header compatibility

Add a cpp-vt-stream example that verifies libghostty headers compile
cleanly in C++ mode. The example is a simplified C++ port of
c-vt-stream.

The headers used the C idiom `typedef struct Foo* Foo` for opaque
handles, which is invalid in C++ because struct tags and typedefs
share the same namespace. Fix all 12 opaque handle typedefs across the
headers to use a distinct struct tag with an Impl suffix, e.g.
`typedef struct GhosttyTerminalImpl* GhosttyTerminal`. This is a
source-compatible change for existing C consumers since the struct
tags were never referenced directly.
This commit is contained in:
Mitchell Hashimoto
2026-03-28 18:36:23 -07:00
parent 741f1d129a
commit 1fcd80daab
13 changed files with 146 additions and 12 deletions

View File

@@ -0,0 +1,19 @@
# Example: VT Stream Processing in C++
This contains a simple example of how to use `ghostty_terminal_vt_write`
to parse and process VT sequences in C++. This is a simplified C++ port
of the `c-vt-stream` example that verifies libghostty compiles in C++
mode.
> [!IMPORTANT]
>
> **`libghostty` is a C library.** This example is only here so our CI
> verifies that the library can be built in used from C++ files.
## Usage
Run the program:
```shell-session
zig build run
```

View File

@@ -0,0 +1,42 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const run_step = b.step("run", "Run the app");
const exe_mod = b.createModule(.{
.target = target,
.optimize = optimize,
});
exe_mod.addCSourceFiles(.{
.root = b.path("src"),
.files = &.{"main.cpp"},
});
// You'll want to use a lazy dependency here so that ghostty is only
// downloaded if you actually need it.
if (b.lazyDependency("ghostty", .{
// Setting simd to false will force a pure static build that
// doesn't even require libc, but it has a significant performance
// penalty. If your embedding app requires libc anyway, you should
// always keep simd enabled.
// .simd = false,
})) |dep| {
exe_mod.linkLibrary(dep.artifact("ghostty-vt"));
}
// Exe
const exe = b.addExecutable(.{
.name = "cpp_vt_stream",
.root_module = exe_mod,
});
b.installArtifact(exe);
// Run
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args);
run_step.dependOn(&run_cmd.step);
}

View File

@@ -0,0 +1,24 @@
.{
.name = .cpp_vt_stream,
.version = "0.0.0",
.fingerprint = 0x112f5d044ef8c2ac,
.minimum_zig_version = "0.15.1",
.dependencies = .{
// Ghostty dependency. In reality, you'd probably use a URL-based
// dependency like the one showed (and commented out) below this one.
// We use a path dependency here for simplicity and to ensure our
// examples always test against the source they're bundled with.
.ghostty = .{ .path = "../../" },
// Example of what a URL-based dependency looks like:
// .ghostty = .{
// .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz",
// .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s",
// },
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

View File

@@ -0,0 +1,49 @@
#include <cassert>
#include <cstdio>
#include <cstring>
#include <ghostty/vt.h>
int main() {
// Create a terminal
GhosttyTerminal terminal;
GhosttyTerminalOptions opts = {
.cols = 80,
.rows = 24,
.max_scrollback = 0,
};
GhosttyResult result = ghostty_terminal_new(nullptr, &terminal, opts);
assert(result == GHOSTTY_SUCCESS);
// Feed VT data into the terminal
const char *text = "Hello from C++!\r\n";
ghostty_terminal_vt_write(terminal, reinterpret_cast<const uint8_t *>(text), std::strlen(text));
text = "\x1b[1;32mGreen Text\x1b[0m\r\n";
ghostty_terminal_vt_write(terminal, reinterpret_cast<const uint8_t *>(text), std::strlen(text));
text = "\x1b[1;1HTop-left corner\r\n";
ghostty_terminal_vt_write(terminal, reinterpret_cast<const uint8_t *>(text), std::strlen(text));
// Get the final terminal state as a plain string
GhosttyFormatterTerminalOptions fmt_opts =
GHOSTTY_INIT_SIZED(GhosttyFormatterTerminalOptions);
fmt_opts.emit = GHOSTTY_FORMATTER_FORMAT_PLAIN;
fmt_opts.trim = true;
GhosttyFormatter formatter;
result = ghostty_formatter_terminal_new(nullptr, &formatter, terminal, fmt_opts);
assert(result == GHOSTTY_SUCCESS);
uint8_t *buf = nullptr;
size_t len = 0;
result = ghostty_formatter_format_alloc(formatter, nullptr, &buf, &len);
assert(result == GHOSTTY_SUCCESS);
std::fwrite(buf, 1, len, stdout);
std::printf("\n");
ghostty_free(nullptr, buf, len);
ghostty_formatter_free(formatter);
ghostty_terminal_free(terminal);
return 0;
}