Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/build-android-libs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ on:
ghostty_commit:
description: ghostty commit to build (C ABI is not yet stable — always pin)
required: true
default: b0947378349eff70f7030dda0e6d022fae1e6fbd
default: 3c1ef5b32fc5ea6b93d28493fabf193f595139cf

permissions:
contents: write

env:
# ghostty's requireZig hard-pins the minor version.
ZIG_VERSION: 0.15.2
ZIG_VERSION: 0.16.0
NDK_VERSION: 27.1.12297006
# Symbols-only Nerd Font bundled as an AAR asset for PUA glyph cells.
NERD_FONTS_VERSION: v3.4.0
NERD_FONTS_SHA256: 8e617904b980fe3648a4b116808788fe50c99d2d495376cb7c0badbd8a564c47
NERD_FONTS_VERSION: v3.5.1
NERD_FONTS_SHA256: fdca3682534f6f65e1ccb2345b0362ccf67d9b8eca7c8025330946e93e2473bc

jobs:
build:
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,16 @@ jobs:
with:
node-version: 26

- uses: actions/setup-java@v5
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 17

- uses: gradle/actions/setup-gradle@v5
- uses: gradle/actions/setup-gradle@v6
with:
# v6's cache is a separate proprietary component. Keep CI on the
# MIT-licensed action and skip that cache.
cache-disabled: true

# postinstall fetches the pinned libghostty-vt vendor tarball the
# CMake build links against.
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@

### 💡 Others

- Dependency bump: libghostty-spm 1.3.1 → 1.5.1 (XCFramework
`upstream.1.3.1-2`, Ghostty v1.3.1), MSDisplayLink 2.1.0 → 2.2.0,
libghostty-vt to ghostty `3c1ef5b` (Zig 0.16.0, Nerd Fonts v3.5.1).
Android JNI follows the new vt C API (`ghostty_terminal_new` cols/rows,
mode query via `GHOSTTY_TERMINAL_DATA_MODE`, colors via
`GHOSTTY_RENDER_STATE_DATA_COLORS`). Expo SDK 57.0.19 / React Native
0.86.3, ESLint 10.9.1 and typescript-eslint 8.69.0. `tsc` is TypeScript
7.0.2 (`@typescript/native`); the `typescript` package is
`@typescript/typescript6` so ESLint still has a compiler API (TypeScript
7 has none until 7.1).
- iOS: `clipboard-write = ask` so OSC 52 cannot silently replace the
system pasteboard. User paste still proceeds; program clipboard
read/write is denied until a host supplies confirmation UI.
- First automated test suites: vitest covers the TerminalView imperative
queue (the 0.8.1 mount-race contract), and JUnit covers the Android
snapshot wire format, cell color resolution, and the sticky-modifier state
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ dependencies this layer disappears in favor of the upstream package.

On Android, `android/vendor/` holds per-ABI `libghostty-vt.a` static
libraries plus the matching C headers, cross-compiled from a pinned ghostty
commit (Zig 0.15.2 + NDK r27); `vendor-manifest.json` pins the tarball
commit (Zig 0.16.0 + NDK r27); `vendor-manifest.json` pins the tarball
checksum. A thin JNI shim (`android/src/main/cpp/ghostty_jni.cpp`) exposes
the terminal + render-state loop to Kotlin, which paints the grid with
Canvas/Skia (`GhosttyTerminalView.kt`).
Expand Down
21 changes: 11 additions & 10 deletions android/src/main/cpp/ghostty_jni.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,9 @@ JNIEXPORT jlong JNICALL
Java_expo_modules_libghostty_GhosttyVt_nativeCreate(
JNIEnv*, jobject, jint cols, jint rows, jlong maxScrollback) {
auto* session = new Session();
GhosttyTerminalOptions opts{};
opts.cols = static_cast<uint16_t>(cols);
opts.rows = static_cast<uint16_t>(rows);
opts.max_scrollback = static_cast<size_t>(maxScrollback);

if (ghostty_terminal_new(nullptr, &session->term, opts) != GHOSTTY_SUCCESS ||
if (ghostty_terminal_new(nullptr, &session->term, static_cast<uint16_t>(cols),
static_cast<uint16_t>(rows)) != GHOSTTY_SUCCESS ||
ghostty_render_state_new(nullptr, &session->renderState) != GHOSTTY_SUCCESS ||
ghostty_render_state_row_iterator_new(nullptr, &session->rowIter) != GHOSTTY_SUCCESS ||
ghostty_render_state_row_cells_new(nullptr, &session->cells) != GHOSTTY_SUCCESS ||
Expand All @@ -284,6 +281,9 @@ Java_expo_modules_libghostty_GhosttyVt_nativeCreate(
return 0;
}

const size_t maxLines = static_cast<size_t>(maxScrollback);
ghostty_terminal_set(session->term, GHOSTTY_TERMINAL_OPT_SCROLLBACK_MAX_LINES, &maxLines);

ghostty_terminal_set(session->term, GHOSTTY_TERMINAL_OPT_USERDATA, session);
ghostty_terminal_set(session->term, GHOSTTY_TERMINAL_OPT_WRITE_PTY,
reinterpret_cast<const void*>(&writePtyCallback));
Expand Down Expand Up @@ -487,9 +487,8 @@ Java_expo_modules_libghostty_GhosttyVt_nativeSnapshot(
ghostty_render_state_get(session->renderState, GHOSTTY_RENDER_STATE_DATA_COLS, &cols);
ghostty_render_state_get(session->renderState, GHOSTTY_RENDER_STATE_DATA_ROWS, &rows);

GhosttyRenderStateColors colors{};
colors.size = sizeof(colors);
ghostty_render_state_colors_get(session->renderState, &colors);
GhosttyRenderStateColors colors = GHOSTTY_INIT_SIZED(GhosttyRenderStateColors);
ghostty_render_state_get(session->renderState, GHOSTTY_RENDER_STATE_DATA_COLORS, &colors);

bool cursorVisible = false;
bool cursorBlinking = false;
Expand Down Expand Up @@ -814,8 +813,10 @@ Java_expo_modules_libghostty_GhosttyVt_nativeEncodePaste(
env->GetByteArrayRegion(data, 0, len, reinterpret_cast<jbyte*>(input.data()));
}

bool bracketed = false;
ghostty_terminal_mode_get(session->term, GHOSTTY_MODE_BRACKETED_PASTE, &bracketed);
GhosttyTerminalModeConfig modeConfig{};
modeConfig.mode = GHOSTTY_MODE_BRACKETED_PASTE;
ghostty_terminal_get(session->term, GHOSTTY_TERMINAL_DATA_MODE, &modeConfig);
const bool bracketed = modeConfig.value;

// Bracketed wrapping adds 12 bytes; stripping/CR replacement is 1:1.
std::vector<char> out(input.size() + 16);
Expand Down
9 changes: 8 additions & 1 deletion eslint.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,11 @@ const { defineConfig } = require('eslint/config');
const universe = require('eslint-config-universe/flat/native');
const universeWeb = require('eslint-config-universe/flat/web');

module.exports = defineConfig([{ ignores: ['build'] }, ...universe, ...universeWeb]);
module.exports = defineConfig([
{ ignores: ['build'] },
...universe,
...universeWeb,
// eslint-plugin-react 7.37.5 is not ESLint 10-ready. Pinning the React
// version skips detectReactVersion(), which still calls context.getFilename().
{ settings: { react: { version: '19.2' } } },
]);
12 changes: 6 additions & 6 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
"version": "1.0.0",
"main": "index.ts",
"dependencies": {
"expo": "~57.0.6",
"react": "19.2.3",
"react-native": "0.86.0"
"expo": "~57.0.19",
"react": "19.2.8",
"react-native": "0.86.3"
},
"devDependencies": {
"@types/react": "~19.2.2",
"babel-preset-expo": "^57.0.3",
"typescript": "~6.0.3"
"@types/react": "~19.2.18",
"babel-preset-expo": "^57.0.10",
"typescript": "~7.0.2"
},
"scripts": {
"start": "expo start",
Expand Down
1 change: 1 addition & 0 deletions ios/ExpoLibghostty.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ Pod::Spec.new do |s|

# Module sources only — the vendored pods own everything under vendor/.
s.source_files = '*.{h,m,mm,swift,hpp,cpp}'
s.exclude_files = 'GhosttyTerminalBundle+CocoaPods.swift'
s.resource_bundles = { 'ExpoLibghostty_privacy' => 'privacy/ExpoLibghostty/PrivacyInfo.xcprivacy' }
end
14 changes: 13 additions & 1 deletion ios/ExpoLibghosttyView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ class ExpoLibghosttyView: ExpoView {
self.session = session
// Without a controller the coordinator never builds a surface
// ("surface rebuild skipped: missing controller").
// OSC 52 writes must ask; the default `clipboard-write = allow`
// would let PTY output silently replace the system pasteboard.
_ = TerminalController.shared.setTerminalConfiguration(
TerminalConfiguration().custom("clipboard-write", "ask")
)
terminalView.controller = TerminalController.shared
terminalView.configuration = TerminalSurfaceOptions(backend: .inMemory(session))
terminalView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
Expand Down Expand Up @@ -96,7 +101,8 @@ class ExpoLibghosttyView: ExpoView {

// Terminal effects (OSC escapes) surfaced as component events.
extension ExpoLibghosttyView: TerminalSurfaceBellDelegate, TerminalSurfaceTitleDelegate,
TerminalSurfacePwdDelegate {
TerminalSurfacePwdDelegate, TerminalSurfaceClipboardConfirmationDelegate
{
func terminalDidRingBell() {
onBell([:])
}
Expand All @@ -108,4 +114,10 @@ extension ExpoLibghosttyView: TerminalSurfaceBellDelegate, TerminalSurfaceTitleD
func terminalDidChangeWorkingDirectory(_ path: String) {
onDirectoryChange(["path": path])
}

func terminalDidRequestClipboardConfirmation(_ request: TerminalClipboardConfirmationRequest) {
// User-started paste can proceed. A program's OSC 52 read/write is
// denied until a host wires its own confirmation UI.
request.respond(allow: request.kind == .paste)
}
}
2 changes: 1 addition & 1 deletion ios/GhosttyKit.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# scripts/download-xcframework.mjs fetches (checksum-pinned) at install time.
Pod::Spec.new do |s|
s.name = 'GhosttyKit'
s.version = '1.3.1'
s.version = '1.5.1'
s.summary = "Ghostty's libghostty C API for Apple platforms (vendored by expo-libghostty)."
s.author = { 'Lakr233' => 'https://github.com/Lakr233' }
s.homepage = 'https://github.com/Lakr233/libghostty-spm'
Expand Down
14 changes: 11 additions & 3 deletions ios/GhosttyTerminal.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Swift wrapper layer: native terminal views, input/IME handling, display link.
Pod::Spec.new do |s|
s.name = 'GhosttyTerminal'
s.version = '1.3.1'
s.version = '1.5.1'
s.summary = 'Ghostty-powered native terminal view (vendored by expo-libghostty).'
s.author = { 'Lakr233' => 'https://github.com/Lakr233' }
s.homepage = 'https://github.com/Lakr233/libghostty-spm'
Expand All @@ -16,6 +16,14 @@ Pod::Spec.new do |s|
s.dependency 'GhosttyKit'
s.dependency 'MSDisplayLink'

s.source_files = 'vendor/GhosttyTerminal/**/*.swift'
s.resource_bundles = { 'GhosttyTerminal_privacy' => 'privacy/GhosttyTerminal/PrivacyInfo.xcprivacy' }
s.source_files = 'vendor/GhosttyTerminal/**/*.swift', 'GhosttyTerminalBundle+CocoaPods.swift'
# SPM generates Bundle.module for Resources/{Ghostty,terminfo}; CocoaPods
# needs an explicit resource bundle plus the Bundle.module shim.
s.resource_bundles = {
'GhosttyTerminal_privacy' => 'privacy/GhosttyTerminal/PrivacyInfo.xcprivacy',
'GhosttyTerminal' => [
'vendor/GhosttyTerminal/Resources/Ghostty',
'vendor/GhosttyTerminal/Resources/terminfo',
],
}
end
19 changes: 19 additions & 0 deletions ios/GhosttyTerminalBundle+CocoaPods.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation

#if !SWIFT_PACKAGE
// SPM synthesizes Bundle.module from Package.swift resources. CocoaPods
// does not, so the vendored GhosttyRuntimeResources lookups need this
// shim. The GhosttyTerminal resource bundle holds Resources/Ghostty and
// Resources/terminfo (shell integration + compiled terminfo).
extension Bundle {
static var module: Bundle {
let host = Bundle(for: TerminalController.self)
if let url = host.url(forResource: "GhosttyTerminal", withExtension: "bundle"),
let bundle = Bundle(url: url)
{
return bundle
}
return host
}
}
#endif
2 changes: 1 addition & 1 deletion ios/MSDisplayLink.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Upstream ships SPM only; this pod exists because CocoaPods cannot consume SPM packages.
Pod::Spec.new do |s|
s.name = 'MSDisplayLink'
s.version = '2.1.0'
s.version = '2.2.0'
s.summary = 'Cross-platform DisplayLink (vendored by expo-libghostty).'
s.author = { 'Lakr233' => 'https://github.com/Lakr233' }
s.homepage = 'https://github.com/Lakr233/MSDisplayLink'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Darwin
import Foundation

/// Runtime assets required by Ghostty's exec backend.
///
/// The package owns these assets and always points libghostty at this immutable
/// bundle location before the C runtime initializes. User-level Ghostty
/// resources and configuration are never consulted.
public enum GhosttyRuntimeResources {
/// The package-bundled Ghostty resource directory.
///
/// Ghostty expects shell integration below this directory and its compiled
/// terminfo database in a sibling `terminfo` directory.
public static var directoryURL: URL? {
Bundle.module.url(forResource: "Ghostty", withExtension: nil)
}

/// The compiled terminfo database exported to child shells by Ghostty.
public static var terminfoDirectoryURL: URL? {
Bundle.module.url(forResource: "terminfo", withExtension: nil)
}

static func configureEnvironment() {
guard let path = directoryURL?.path else { return }
setenv("GHOSTTY_RESOURCES_DIR", path, 1)
}
}
Loading