From 1fcd80daab898e1543409986cd07d1db9e393570 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 28 Mar 2026 18:36:23 -0700 Subject: [PATCH] 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. --- example/cpp-vt-stream/README.md | 19 +++++++++++ example/cpp-vt-stream/build.zig | 42 +++++++++++++++++++++++++ example/cpp-vt-stream/build.zig.zon | 24 ++++++++++++++ example/cpp-vt-stream/src/main.cpp | 49 +++++++++++++++++++++++++++++ include/ghostty/vt/formatter.h | 2 +- include/ghostty/vt/key/encoder.h | 2 +- include/ghostty/vt/key/event.h | 2 +- include/ghostty/vt/mouse/encoder.h | 2 +- include/ghostty/vt/mouse/event.h | 2 +- include/ghostty/vt/osc.h | 4 +-- include/ghostty/vt/render.h | 6 ++-- include/ghostty/vt/sgr.h | 2 +- include/ghostty/vt/terminal.h | 2 +- 13 files changed, 146 insertions(+), 12 deletions(-) create mode 100644 example/cpp-vt-stream/README.md create mode 100644 example/cpp-vt-stream/build.zig create mode 100644 example/cpp-vt-stream/build.zig.zon create mode 100644 example/cpp-vt-stream/src/main.cpp diff --git a/example/cpp-vt-stream/README.md b/example/cpp-vt-stream/README.md new file mode 100644 index 000000000..7dccbe301 --- /dev/null +++ b/example/cpp-vt-stream/README.md @@ -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 +``` diff --git a/example/cpp-vt-stream/build.zig b/example/cpp-vt-stream/build.zig new file mode 100644 index 000000000..c1fd87081 --- /dev/null +++ b/example/cpp-vt-stream/build.zig @@ -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); +} diff --git a/example/cpp-vt-stream/build.zig.zon b/example/cpp-vt-stream/build.zig.zon new file mode 100644 index 000000000..bc6a39a0e --- /dev/null +++ b/example/cpp-vt-stream/build.zig.zon @@ -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", + }, +} diff --git a/example/cpp-vt-stream/src/main.cpp b/example/cpp-vt-stream/src/main.cpp new file mode 100644 index 000000000..a77f98ad5 --- /dev/null +++ b/example/cpp-vt-stream/src/main.cpp @@ -0,0 +1,49 @@ +#include +#include +#include +#include + +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(text), std::strlen(text)); + + text = "\x1b[1;32mGreen Text\x1b[0m\r\n"; + ghostty_terminal_vt_write(terminal, reinterpret_cast(text), std::strlen(text)); + + text = "\x1b[1;1HTop-left corner\r\n"; + ghostty_terminal_vt_write(terminal, reinterpret_cast(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; +} diff --git a/include/ghostty/vt/formatter.h b/include/ghostty/vt/formatter.h index 5a9bb524b..573917596 100644 --- a/include/ghostty/vt/formatter.h +++ b/include/ghostty/vt/formatter.h @@ -111,7 +111,7 @@ typedef struct { * * @ingroup formatter */ -typedef struct GhosttyFormatter* GhosttyFormatter; +typedef struct GhosttyFormatterImpl* GhosttyFormatter; /** * Options for creating a terminal formatter. diff --git a/include/ghostty/vt/key/encoder.h b/include/ghostty/vt/key/encoder.h index 9bfeba98d..7351d3f8f 100644 --- a/include/ghostty/vt/key/encoder.h +++ b/include/ghostty/vt/key/encoder.h @@ -22,7 +22,7 @@ * * @ingroup key */ -typedef struct GhosttyKeyEncoder *GhosttyKeyEncoder; +typedef struct GhosttyKeyEncoderImpl *GhosttyKeyEncoder; /** * Kitty keyboard protocol flags. diff --git a/include/ghostty/vt/key/event.h b/include/ghostty/vt/key/event.h index eba29cecf..18bf4c624 100644 --- a/include/ghostty/vt/key/event.h +++ b/include/ghostty/vt/key/event.h @@ -21,7 +21,7 @@ * * @ingroup key */ -typedef struct GhosttyKeyEvent *GhosttyKeyEvent; +typedef struct GhosttyKeyEventImpl *GhosttyKeyEvent; /** * Keyboard input event types. diff --git a/include/ghostty/vt/mouse/encoder.h b/include/ghostty/vt/mouse/encoder.h index 63fb3e075..0640fd020 100644 --- a/include/ghostty/vt/mouse/encoder.h +++ b/include/ghostty/vt/mouse/encoder.h @@ -23,7 +23,7 @@ * * @ingroup mouse */ -typedef struct GhosttyMouseEncoder *GhosttyMouseEncoder; +typedef struct GhosttyMouseEncoderImpl *GhosttyMouseEncoder; /** * Mouse tracking mode. diff --git a/include/ghostty/vt/mouse/event.h b/include/ghostty/vt/mouse/event.h index 44132097c..65edf591b 100644 --- a/include/ghostty/vt/mouse/event.h +++ b/include/ghostty/vt/mouse/event.h @@ -20,7 +20,7 @@ * * @ingroup mouse */ -typedef struct GhosttyMouseEvent *GhosttyMouseEvent; +typedef struct GhosttyMouseEventImpl *GhosttyMouseEvent; /** * Mouse event action type. diff --git a/include/ghostty/vt/osc.h b/include/ghostty/vt/osc.h index 69f7d1e55..cf0a8a417 100644 --- a/include/ghostty/vt/osc.h +++ b/include/ghostty/vt/osc.h @@ -21,7 +21,7 @@ * * @ingroup osc */ -typedef struct GhosttyOscParser *GhosttyOscParser; +typedef struct GhosttyOscParserImpl *GhosttyOscParser; /** * Opaque handle to a single OSC command. @@ -31,7 +31,7 @@ typedef struct GhosttyOscParser *GhosttyOscParser; * * @ingroup osc */ -typedef struct GhosttyOscCommand *GhosttyOscCommand; +typedef struct GhosttyOscCommandImpl *GhosttyOscCommand; /** @defgroup osc OSC Parser * diff --git a/include/ghostty/vt/render.h b/include/ghostty/vt/render.h index 0a300dde0..1d1e13d25 100644 --- a/include/ghostty/vt/render.h +++ b/include/ghostty/vt/render.h @@ -86,21 +86,21 @@ extern "C" { * * @ingroup render */ -typedef struct GhosttyRenderState* GhosttyRenderState; +typedef struct GhosttyRenderStateImpl* GhosttyRenderState; /** * Opaque handle to a render-state row iterator. * * @ingroup render */ -typedef struct GhosttyRenderStateRowIterator* GhosttyRenderStateRowIterator; +typedef struct GhosttyRenderStateRowIteratorImpl* GhosttyRenderStateRowIterator; /** * Opaque handle to render-state row cells. * * @ingroup render */ -typedef struct GhosttyRenderStateRowCells* GhosttyRenderStateRowCells; +typedef struct GhosttyRenderStateRowCellsImpl* GhosttyRenderStateRowCells; /** * Dirty state of a render state after update. diff --git a/include/ghostty/vt/sgr.h b/include/ghostty/vt/sgr.h index 3b190a6b8..afaa18145 100644 --- a/include/ghostty/vt/sgr.h +++ b/include/ghostty/vt/sgr.h @@ -55,7 +55,7 @@ extern "C" { * * @ingroup sgr */ -typedef struct GhosttySgrParser* GhosttySgrParser; +typedef struct GhosttySgrParserImpl* GhosttySgrParser; /** * SGR attribute tags. diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index 9660576f4..dfbe4e1a0 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -159,7 +159,7 @@ extern "C" { * * @ingroup terminal */ -typedef struct GhosttyTerminal* GhosttyTerminal; +typedef struct GhosttyTerminalImpl* GhosttyTerminal; /** * Terminal initialization options.