build: add CMake support for libghostty-vt (#11700)

Add a top-level CMakeLists.txt that wraps `zig build lib-vt` so that
CMake-based downstream projects can consume libghostty-vt without
needing to interact with the Zig build system directly. A custom command
triggers the zig build during `cmake --build`, and the resulting shared
library is exposed as an IMPORTED target.

Downstream projects can pull in the library via FetchContent, which
fetches the source and builds it as part of their own CMake build, or
via find_package after a manual install step. The package config
template in dist/cmake/ sets up the ghostty-vt::ghostty-vt target with
proper include paths and macOS rpath handling.

A c-vt-cmake example demonstrates the FetchContent workflow, creating a
terminal, writing VT sequences, and formatting the output as plain text.
CI is updated to auto-discover and build CMake-based examples alongside
the existing Zig-based ones.

> [!WARNING]
>
> I am **very much not a CMake expert.** I leaned on LLMs heavily for
this. I did read the docs for what was chosen here and understand what's
going on, but if there is a better or more idiomatic way to do this I'm
all ears!

## Example CMake File

```cmake
cmake_minimum_required(VERSION 3.19)
project(c-vt-cmake LANGUAGES C)

include(FetchContent)
FetchContent_Declare(ghostty
    GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git
    GIT_TAG main
)
FetchContent_MakeAvailable(ghostty)

add_executable(c_vt_cmake src/main.c)
target_link_libraries(c_vt_cmake PRIVATE ghostty-vt)
```
This commit is contained in:
Mitchell Hashimoto
2026-03-20 11:56:36 -07:00
committed by GitHub
10 changed files with 400 additions and 7 deletions

View File

@@ -85,7 +85,8 @@ jobs:
- skip
- build-bench
- build-dist
- build-examples
- build-examples-zig
- build-examples-cmake
- build-flatpak
- build-libghostty-vt
- build-libghostty-vt-android
@@ -172,22 +173,26 @@ jobs:
needs: skip
runs-on: namespace-profile-ghostty-xsm
outputs:
dirs: ${{ steps.list.outputs.dirs }}
zig: ${{ steps.list.outputs.zig }}
cmake: ${{ steps.list.outputs.cmake }}
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- id: list
name: List example directories
run: |
dirs=$(ls example/*/build.zig.zon 2>/dev/null | xargs -n1 dirname | xargs -n1 basename | jq -R -s -c 'split("\n") | map(select(. != ""))')
echo "$dirs" | jq .
echo "dirs=$dirs" >> "$GITHUB_OUTPUT"
zig=$(ls example/*/build.zig.zon 2>/dev/null | xargs -n1 dirname | xargs -n1 basename | jq -R -s -c 'split("\n") | map(select(. != ""))')
echo "$zig" | jq .
echo "zig=$zig" >> "$GITHUB_OUTPUT"
cmake=$(ls example/*/CMakeLists.txt 2>/dev/null | xargs -n1 dirname | xargs -n1 basename | jq -R -s -c 'split("\n") | map(select(. != ""))')
echo "$cmake" | jq .
echo "cmake=$cmake" >> "$GITHUB_OUTPUT"
build-examples:
build-examples-zig:
strategy:
fail-fast: false
matrix:
dir: ${{ fromJSON(needs.list-examples.outputs.dirs) }}
dir: ${{ fromJSON(needs.list-examples.outputs.zig) }}
name: Example ${{ matrix.dir }}
runs-on: namespace-profile-ghostty-xsm
needs: [test, list-examples]
@@ -219,6 +224,43 @@ jobs:
cd example/${{ matrix.dir }}
nix develop -c zig build
build-examples-cmake:
strategy:
fail-fast: false
matrix:
dir: ${{ fromJSON(needs.list-examples.outputs.cmake) }}
name: Example ${{ matrix.dir }}
runs-on: namespace-profile-ghostty-xsm
needs: [test, list-examples]
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@1ca7d21a94afc7c957383a2d217460d980de4934 # v31.10.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Build Example
run: |
cd example/${{ matrix.dir }}
nix develop -c cmake -B build -DFETCHCONTENT_SOURCE_DIR_GHOSTTY=${{ github.workspace }}
nix develop -c cmake --build build
build-flatpak:
strategy:
fail-fast: false

3
.gitignore vendored
View File

@@ -10,6 +10,9 @@
zig-cache/
.zig-cache/
zig-out/
build-cmake/
CMakeCache.txt
CMakeFiles/
/build.zig.zon.bak
/result*
/.nixos-test-history

160
CMakeLists.txt Normal file
View File

@@ -0,0 +1,160 @@
# CMake wrapper for libghostty-vt
#
# This file delegates to `zig build lib-vt` to produce the shared library,
# headers, and pkg-config file. It exists so that CMake-based projects can
# consume libghostty-vt without interacting with the Zig build system
# directly. However, downstream users do still require `zig` on the PATH.
# Please consult the Ghostty docs for the required Zig version:
#
# https://ghostty.org/docs/install/build
#
# Building within the Ghostty repo
# ---------------------------------
#
# cmake -B build
# cmake --build build
# cmake --install build --prefix /usr/local
#
# Pass extra flags to the Zig build with GHOSTTY_ZIG_BUILD_FLAGS:
#
# cmake -B build -DGHOSTTY_ZIG_BUILD_FLAGS="-Demit-macos-app=false"
#
# Integrating into a downstream CMake project
# ---------------------------------------------
#
# Option 1 — FetchContent (recommended, no manual install step):
#
# include(FetchContent)
# FetchContent_Declare(ghostty
# GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git
# GIT_TAG main
# )
# FetchContent_MakeAvailable(ghostty)
#
# target_link_libraries(myapp PRIVATE ghostty-vt)
#
# To use a local checkout instead of fetching:
#
# cmake -B build -DFETCHCONTENT_SOURCE_DIR_GHOSTTY=/path/to/ghostty
#
# Option 2 — find_package (after installing to a prefix):
#
# find_package(ghostty-vt REQUIRED)
# target_link_libraries(myapp PRIVATE ghostty-vt::ghostty-vt)
#
# See dist/cmake/README.md for more details and example/c-vt-cmake/ for a
# complete working example.
cmake_minimum_required(VERSION 3.19)
project(ghostty-vt VERSION 0.1.0 LANGUAGES C)
# --- Options ----------------------------------------------------------------
set(GHOSTTY_ZIG_BUILD_FLAGS "" CACHE STRING "Additional flags to pass to zig build")
# --- Find Zig ----------------------------------------------------------------
find_program(ZIG_EXECUTABLE zig REQUIRED)
message(STATUS "Found zig: ${ZIG_EXECUTABLE}")
# --- Build via zig build -----------------------------------------------------
# The zig build installs into zig-out/ relative to the source tree.
set(ZIG_OUT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/zig-out")
# The library file that zig build produces.
if(APPLE)
set(GHOSTTY_VT_LIBNAME "${CMAKE_SHARED_LIBRARY_PREFIX}ghostty-vt${CMAKE_SHARED_LIBRARY_SUFFIX}")
set(GHOSTTY_VT_SONAME "${CMAKE_SHARED_LIBRARY_PREFIX}ghostty-vt.0${CMAKE_SHARED_LIBRARY_SUFFIX}")
set(GHOSTTY_VT_REALNAME "${CMAKE_SHARED_LIBRARY_PREFIX}ghostty-vt.0.1.0${CMAKE_SHARED_LIBRARY_SUFFIX}")
elseif(WIN32)
set(GHOSTTY_VT_LIBNAME "ghostty-vt.dll")
set(GHOSTTY_VT_REALNAME "ghostty-vt.dll")
else()
set(GHOSTTY_VT_LIBNAME "${CMAKE_SHARED_LIBRARY_PREFIX}ghostty-vt${CMAKE_SHARED_LIBRARY_SUFFIX}")
set(GHOSTTY_VT_SONAME "${CMAKE_SHARED_LIBRARY_PREFIX}ghostty-vt${CMAKE_SHARED_LIBRARY_SUFFIX}.0")
set(GHOSTTY_VT_REALNAME "${CMAKE_SHARED_LIBRARY_PREFIX}ghostty-vt${CMAKE_SHARED_LIBRARY_SUFFIX}.0.1.0")
endif()
set(GHOSTTY_VT_LIBRARY "${ZIG_OUT_DIR}/lib/${GHOSTTY_VT_REALNAME}")
# Ensure the output directories exist so CMake doesn't reject the
# INTERFACE_INCLUDE_DIRECTORIES before the zig build has run.
file(MAKE_DIRECTORY "${ZIG_OUT_DIR}/include")
# Custom command: run zig build lib-vt
add_custom_command(
OUTPUT "${GHOSTTY_VT_LIBRARY}"
COMMAND "${ZIG_EXECUTABLE}" build lib-vt ${GHOSTTY_ZIG_BUILD_FLAGS}
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
COMMENT "Building libghostty-vt via zig build..."
USES_TERMINAL
)
add_custom_target(zig_build_lib_vt ALL DEPENDS "${GHOSTTY_VT_LIBRARY}")
# --- IMPORTED library target --------------------------------------------------
add_library(ghostty-vt SHARED IMPORTED GLOBAL)
set_target_properties(ghostty-vt PROPERTIES
IMPORTED_LOCATION "${GHOSTTY_VT_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${ZIG_OUT_DIR}/include"
)
if(APPLE)
set_target_properties(ghostty-vt PROPERTIES
IMPORTED_SONAME "@rpath/${GHOSTTY_VT_SONAME}"
)
elseif(NOT WIN32)
set_target_properties(ghostty-vt PROPERTIES
IMPORTED_SONAME "${GHOSTTY_VT_SONAME}"
)
endif()
add_dependencies(ghostty-vt zig_build_lib_vt)
# --- Install ------------------------------------------------------------------
include(GNUInstallDirs)
# Install the library
install(FILES "${GHOSTTY_VT_LIBRARY}" TYPE LIB)
if(NOT WIN32)
# Install symlinks
install(CODE "
execute_process(COMMAND \${CMAKE_COMMAND} -E create_symlink
\"${GHOSTTY_VT_REALNAME}\"
\"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/${GHOSTTY_VT_SONAME}\")
execute_process(COMMAND \${CMAKE_COMMAND} -E create_symlink
\"${GHOSTTY_VT_SONAME}\"
\"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/${GHOSTTY_VT_LIBNAME}\")
")
endif()
# Install headers
install(DIRECTORY "${ZIG_OUT_DIR}/include/ghostty" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}")
# --- CMake package config for find_package() ----------------------------------
include(CMakePackageConfigHelpers)
# Generate the config file
configure_package_config_file(
"${CMAKE_CURRENT_SOURCE_DIR}/dist/cmake/ghostty-vt-config.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/ghostty-vt-config.cmake"
INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/ghostty-vt"
)
# Generate the version file
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/ghostty-vt-config-version.cmake"
VERSION "${PROJECT_VERSION}"
COMPATIBILITY SameMajorVersion
)
# Install the config files
install(
FILES
"${CMAKE_CURRENT_BINARY_DIR}/ghostty-vt-config.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/ghostty-vt-config-version.cmake"
DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/ghostty-vt"
)

67
dist/cmake/README.md vendored Normal file
View File

@@ -0,0 +1,67 @@
# CMake Support for libghostty-vt
The top-level `CMakeLists.txt` wraps the Zig build system so that CMake
projects can consume libghostty-vt without invoking `zig build` manually.
Running `cmake --build` triggers `zig build lib-vt` automatically.
This means downstream projects do require a working Zig compiler on
`PATH` to build, but don't need to know any Zig-specific details.
## Using FetchContent (recommended)
Add the following to your project's `CMakeLists.txt`:
```cmake
include(FetchContent)
FetchContent_Declare(ghostty
GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git
GIT_TAG main
)
FetchContent_MakeAvailable(ghostty)
add_executable(myapp main.c)
target_link_libraries(myapp PRIVATE ghostty-vt)
```
This fetches the Ghostty source, builds libghostty-vt via Zig during your
CMake build, and links it into your target. Headers are added to the
include path automatically.
### Using a local checkout
If you already have the Ghostty source checked out, skip the download by
pointing CMake at it:
```shell-session
cmake -B build -DFETCHCONTENT_SOURCE_DIR_GHOSTTY=/path/to/ghostty
cmake --build build
```
## Using find_package (install-based)
Build and install libghostty-vt first:
```shell-session
cd /path/to/ghostty
cmake -B build
cmake --build build
cmake --install build --prefix /usr/local
```
Then in your project:
```cmake
find_package(ghostty-vt REQUIRED)
add_executable(myapp main.c)
target_link_libraries(myapp PRIVATE ghostty-vt::ghostty-vt)
```
## Files
- `ghostty-vt-config.cmake.in` — template for the CMake package config
file installed alongside the library, enabling `find_package()` support.
## Example
See `example/c-vt-cmake/` for a complete working example.

33
dist/cmake/ghostty-vt-config.cmake.in vendored Normal file
View File

@@ -0,0 +1,33 @@
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
if(NOT TARGET ghostty-vt::ghostty-vt)
add_library(ghostty-vt::ghostty-vt SHARED IMPORTED)
set(_ghostty_vt_libdir "${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_LIBDIR@")
set_target_properties(ghostty-vt::ghostty-vt PROPERTIES
IMPORTED_LOCATION "${_ghostty_vt_libdir}/@GHOSTTY_VT_REALNAME@"
INTERFACE_INCLUDE_DIRECTORIES "${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_INCLUDEDIR@"
)
if(APPLE)
set_target_properties(ghostty-vt::ghostty-vt PROPERTIES
IMPORTED_SONAME "@rpath/@GHOSTTY_VT_SONAME@"
INTERFACE_LINK_DIRECTORIES "${_ghostty_vt_libdir}"
)
# Ensure consumers can find the @rpath dylib at runtime
set_property(TARGET ghostty-vt::ghostty-vt APPEND PROPERTY
INTERFACE_LINK_OPTIONS "LINKER:-rpath,${_ghostty_vt_libdir}"
)
elseif(NOT WIN32)
set_target_properties(ghostty-vt::ghostty-vt PROPERTIES
IMPORTED_SONAME "@GHOSTTY_VT_SONAME@"
)
endif()
unset(_ghostty_vt_libdir)
endif()
check_required_components(ghostty-vt)

1
example/.gitignore vendored
View File

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

View File

@@ -0,0 +1,12 @@
cmake_minimum_required(VERSION 3.19)
project(c-vt-cmake LANGUAGES C)
include(FetchContent)
FetchContent_Declare(ghostty
GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git
GIT_TAG main
)
FetchContent_MakeAvailable(ghostty)
add_executable(c_vt_cmake src/main.c)
target_link_libraries(c_vt_cmake PRIVATE ghostty-vt)

View File

@@ -0,0 +1,21 @@
# c-vt-cmake
Demonstrates consuming libghostty-vt from a CMake project using
`FetchContent`. Creates a terminal, writes VT sequences into it, and
formats the screen contents as plain text.
## Building
```shell-session
cd example/c-vt-cmake
cmake -B build
cmake --build build
./build/c_vt_cmake
```
To build against a local checkout instead of fetching from GitHub:
```shell-session
cmake -B build -DFETCHCONTENT_SOURCE_DIR_GHOSTTY=../..
cmake --build build
```

View File

@@ -0,0 +1,52 @@
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ghostty/vt.h>
int main() {
// Create a terminal with a small grid
GhosttyTerminal terminal;
GhosttyTerminalOptions opts = {
.cols = 80,
.rows = 24,
.max_scrollback = 0,
};
GhosttyResult result = ghostty_terminal_new(NULL, &terminal, opts);
assert(result == GHOSTTY_SUCCESS);
// Write some VT-encoded content into the terminal
const char *commands[] = {
"Hello from a \033[1mCMake\033[0m-built program!\r\n",
"Line 2: \033[4munderlined\033[0m text\r\n",
"Line 3: \033[31mred\033[0m \033[32mgreen\033[0m \033[34mblue\033[0m\r\n",
};
for (size_t i = 0; i < sizeof(commands) / sizeof(commands[0]); i++) {
ghostty_terminal_vt_write(terminal, (const uint8_t *)commands[i],
strlen(commands[i]));
}
// Format the terminal contents as plain text
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(NULL, &formatter, terminal, fmt_opts);
assert(result == GHOSTTY_SUCCESS);
uint8_t *buf = NULL;
size_t len = 0;
result = ghostty_formatter_format_alloc(formatter, NULL, &buf, &len);
assert(result == GHOSTTY_SUCCESS);
printf("Plain text (%zu bytes):\n", len);
fwrite(buf, 1, len, stdout);
printf("\n");
free(buf);
ghostty_formatter_free(formatter);
ghostty_terminal_free(terminal);
return 0;
}

View File

@@ -8,6 +8,7 @@
appstream,
flatpak-builder,
gdb,
cmake,
#, glxinfo # unused
ncurses,
nodejs,
@@ -91,6 +92,7 @@ in
packages =
[
# For builds
cmake
doxygen
jq
llvmPackages_latest.llvm