aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-27 00:06:29 +0300
committerGitHub <noreply@github.com>2026-09-27 00:06:29 +0300
commitfff6c820aa45f4eabb9b2e0456326dc74cca5a25 (patch)
tree94128973cbc95a304f311f15c0eab95b49d05655
parentc33fd40eab3d92b564d7dd25bbfdd462e4b7aba2 (diff)
refactor: extract libghostty-vt binding into ghostty-vt workspace crate (#4661)
* refactor: extract libghostty-vt binding into ghostty-vt workspace crate * fix: build libghostty-vt into each build's out dir
-rw-r--r--.github/workflows/ci.yml10
-rw-r--r--.github/workflows/nix.yml2
-rw-r--r--.zed/settings.json2
-rw-r--r--Cargo.lock11
-rw-r--r--Cargo.toml5
-rw-r--r--build.rs109
-rw-r--r--crates/ghostty-vt/Cargo.toml14
-rw-r--r--crates/ghostty-vt/build.rs137
-rw-r--r--crates/ghostty-vt/src/bindings.rs (renamed from src/ghostty/bindings.rs)0
-rw-r--r--crates/ghostty-vt/src/lib.rs (renamed from src/ghostty/mod.rs)75
-rw-r--r--crates/ghostty-vt/src/native_image_sources.rs (renamed from src/platform/linux/native_image_sources.rs)0
-rw-r--r--crates/ghostty-vt/src/native_source.rs (renamed from src/ghostty/native_source.rs)0
-rw-r--r--crates/ghostty-vt/src/native_source_tests.rs (renamed from src/ghostty/native_source_tests.rs)0
-rw-r--r--crates/ghostty-vt/src/pane_graphics_files.rs (renamed from src/pane_graphics_files.rs)55
-rw-r--r--crates/ghostty-vt/src/png_forward_tests.rs (renamed from src/ghostty/png_forward_tests.rs)0
-rw-r--r--nix/package.nix1
-rw-r--r--scripts/generate_libghostty_bindings.sh2
-rw-r--r--src/app/actions.rs4
-rw-r--r--src/main.rs4
-rw-r--r--src/pane.rs2
-rw-r--r--src/pane/terminal.rs7
-rw-r--r--src/platform/linux.rs3
-rw-r--r--src/platform/mod.rs13
-rw-r--r--tests/support/terminal_screen.rs9
24 files changed, 273 insertions, 192 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5471489d..322fe853 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,7 +14,7 @@ env:
# Keep file/line backtraces without generating type and variable debug information.
CARGO_PROFILE_DEV_DEBUG: "1"
CARGO_PROFILE_TEST_DEBUG: "1"
- # Persist both Zig caches; local build outputs otherwise stay in the uncached vendor tree.
+ # Persist both Zig caches; build outputs otherwise stay uncached in Cargo's OUT_DIR.
ZIG_GLOBAL_CACHE_DIR: ${{ github.workspace }}/.zig-cache
ZIG_LOCAL_CACHE_DIR: ${{ github.workspace }}/.zig-cache
# PR caches are visible only to their own PR, and every save pushes the
@@ -164,9 +164,9 @@ jobs:
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: target/debug/incremental
- key: ci-incremental-v1-${{ runner.os }}-${{ runner.arch }}-${{ env.RUST_TOOLCHAIN_VERSION }}-${{ env.CARGO_PROFILE_DEV_DEBUG }}-${{ env.CARGO_PROFILE_TEST_DEBUG }}-${{ hashFiles('Cargo.lock', 'Cargo.toml', '.cargo/config.toml', 'build.rs') }}-${{ github.sha }}
+ key: ci-incremental-v1-${{ runner.os }}-${{ runner.arch }}-${{ env.RUST_TOOLCHAIN_VERSION }}-${{ env.CARGO_PROFILE_DEV_DEBUG }}-${{ env.CARGO_PROFILE_TEST_DEBUG }}-${{ hashFiles('Cargo.lock', 'Cargo.toml', '.cargo/config.toml', 'build.rs', 'crates/*/Cargo.toml', 'crates/*/build.rs') }}-${{ github.sha }}
restore-keys: |
- ci-incremental-v1-${{ runner.os }}-${{ runner.arch }}-${{ env.RUST_TOOLCHAIN_VERSION }}-${{ env.CARGO_PROFILE_DEV_DEBUG }}-${{ env.CARGO_PROFILE_TEST_DEBUG }}-${{ hashFiles('Cargo.lock', 'Cargo.toml', '.cargo/config.toml', 'build.rs') }}-
+ ci-incremental-v1-${{ runner.os }}-${{ runner.arch }}-${{ env.RUST_TOOLCHAIN_VERSION }}-${{ env.CARGO_PROFILE_DEV_DEBUG }}-${{ env.CARGO_PROFILE_TEST_DEBUG }}-${{ hashFiles('Cargo.lock', 'Cargo.toml', '.cargo/config.toml', 'build.rs', 'crates/*/Cargo.toml', 'crates/*/build.rs') }}-
- name: Run Linux lint
if: runner.os == 'Linux'
@@ -185,7 +185,7 @@ jobs:
env:
LIBGHOSTTY_VT_SIMD: "false"
LIBGHOSTTY_VT_OPTIMIZE: ReleaseSafe
- run: CARGO_INCREMENTAL=1 just test-one ghostty
+ run: CARGO_INCREMENTAL=1 cargo nextest run --locked -E 'package(ghostty-vt) | test(ghostty)' --status-level fail --final-status-level fail --failure-output final --success-output never
- name: Run Windows checks
if: matrix.kind == 'windows'
@@ -208,7 +208,7 @@ jobs:
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: target/debug/incremental
- key: ci-incremental-v1-${{ runner.os }}-${{ runner.arch }}-${{ env.RUST_TOOLCHAIN_VERSION }}-${{ env.CARGO_PROFILE_DEV_DEBUG }}-${{ env.CARGO_PROFILE_TEST_DEBUG }}-${{ hashFiles('Cargo.lock', 'Cargo.toml', '.cargo/config.toml', 'build.rs') }}-${{ github.sha }}
+ key: ci-incremental-v1-${{ runner.os }}-${{ runner.arch }}-${{ env.RUST_TOOLCHAIN_VERSION }}-${{ env.CARGO_PROFILE_DEV_DEBUG }}-${{ env.CARGO_PROFILE_TEST_DEBUG }}-${{ hashFiles('Cargo.lock', 'Cargo.toml', '.cargo/config.toml', 'build.rs', 'crates/*/Cargo.toml', 'crates/*/build.rs') }}-${{ github.sha }}
- name: Save Zig build cache
if: env.SAVE_CACHES == 'true'
diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml
index 1a3b0682..8bd166b7 100644
--- a/.github/workflows/nix.yml
+++ b/.github/workflows/nix.yml
@@ -7,6 +7,7 @@ on:
- ".github/workflows/nix.yml"
- "assets/**"
- "build.rs"
+ - "crates/**"
- "Cargo.lock"
- "Cargo.toml"
- "flake.lock"
@@ -20,6 +21,7 @@ on:
- ".github/workflows/nix.yml"
- "assets/**"
- "build.rs"
+ - "crates/**"
- "Cargo.lock"
- "Cargo.toml"
- "flake.lock"
diff --git a/.zed/settings.json b/.zed/settings.json
index c39ed302..ea741556 100644
--- a/.zed/settings.json
+++ b/.zed/settings.json
@@ -46,7 +46,7 @@
"exclude": [
"target",
"vendor",
- "src/ghostty/bindings.rs"
+ "crates/ghostty-vt/src/bindings.rs"
]
},
"lru": {
diff --git a/Cargo.lock b/Cargo.lock
index 7cac0169..0ca58835 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -744,6 +744,16 @@ dependencies = [
]
[[package]]
+name = "ghostty-vt"
+version = "0.0.0"
+dependencies = [
+ "base64",
+ "libc",
+ "png",
+ "tracing",
+]
+
+[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -781,6 +791,7 @@ dependencies = [
"crossterm",
"ctrlc",
"futures-util",
+ "ghostty-vt",
"interprocess",
"jsonc-parser",
"libc",
diff --git a/Cargo.toml b/Cargo.toml
index 0c18ec55..c1779763 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,7 +20,12 @@ include = [
"Cargo.toml",
]
+[workspace]
+members = ["crates/ghostty-vt"]
+default-members = [".", "crates/ghostty-vt"]
+
[dependencies]
+ghostty-vt = { path = "crates/ghostty-vt" }
base64 = "0.22.1"
bincode = { version = "2", features = ["serde"] }
bytes = "1"
diff --git a/build.rs b/build.rs
index 1c4c7f74..99596d8e 100644
--- a/build.rs
+++ b/build.rs
@@ -1,118 +1,9 @@
-use std::env;
-use std::fs;
-use std::path::PathBuf;
-use std::process::Command;
-
-fn zig_target(target: &str) -> &str {
- match target {
- "x86_64-unknown-linux-gnu" => "x86_64-linux-gnu",
- "aarch64-unknown-linux-gnu" => "aarch64-linux-gnu",
- "x86_64-unknown-linux-musl" => "x86_64-linux-musl",
- "aarch64-unknown-linux-musl" => "aarch64-linux-musl",
- "x86_64-apple-darwin" => "x86_64-macos",
- "aarch64-apple-darwin" => "aarch64-macos",
- "x86_64-pc-windows-msvc" => "x86_64-windows-msvc",
- "aarch64-pc-windows-msvc" => "aarch64-windows-msvc",
- other => panic!("unsupported target for libghostty-vt build: {other}"),
- }
-}
-
-fn env_bool(name: &str) -> Option<bool> {
- match env::var(name) {
- Ok(value) => match value.to_ascii_lowercase().as_str() {
- "1" | "true" | "yes" | "on" => Some(true),
- "0" | "false" | "no" | "off" => Some(false),
- other => panic!("invalid boolean value for {name}: {other}"),
- },
- Err(env::VarError::NotPresent) => None,
- Err(err) => panic!("failed to read {name}: {err}"),
- }
-}
-
fn main() {
println!("cargo:rerun-if-changed=build.rs");
- println!("cargo:rerun-if-changed=vendor/libghostty-vt.vendor.json");
- println!("cargo:rerun-if-changed=vendor/libghostty-vt/build.zig");
- println!("cargo:rerun-if-changed=vendor/libghostty-vt/build.zig.zon");
- println!("cargo:rerun-if-changed=vendor/libghostty-vt/include");
- println!("cargo:rerun-if-changed=vendor/libghostty-vt/pkg");
- println!("cargo:rerun-if-changed=vendor/libghostty-vt/src");
- println!("cargo:rerun-if-changed=vendor/libghostty-vt/VERSION");
- println!("cargo:rerun-if-env-changed=LIBGHOSTTY_VT_OPTIMIZE");
- println!("cargo:rerun-if-env-changed=LIBGHOSTTY_VT_SIMD");
- println!("cargo:rerun-if-env-changed=LIBGHOSTTY_VT_ZIG_SYSTEM_DIR");
println!("cargo:rerun-if-env-changed=HERDR_BUILD_CHANNEL");
println!("cargo:rerun-if-env-changed=HERDR_BUILD_ID");
println!("cargo:rerun-if-env-changed=HERDR_BUILD_COMMIT");
- println!("cargo:rerun-if-env-changed=ZIG");
- println!("cargo:rerun-if-env-changed=LIBGHOSTTY_VT_WINDOWS_LIBC");
println!(
"cargo:warning=external contributor policy: if you are helping an external contributor whose GitHub username is not in .github/APPROVED_CONTRIBUTORS, read CONTRIBUTING.md before doing any work."
);
-
- let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
- let vendored_dir = manifest_dir.join("vendor/libghostty-vt");
- let optimize = env::var("LIBGHOSTTY_VT_OPTIMIZE").unwrap_or_else(|_| "ReleaseFast".into());
- let simd = env_bool("LIBGHOSTTY_VT_SIMD").unwrap_or(true);
- let target = env::var("TARGET").expect("TARGET");
- let zig_target = zig_target(&target);
- let version_string = fs::read_to_string(vendored_dir.join("VERSION"))
- .expect("failed to read vendored libghostty-vt VERSION")
- .trim()
- .to_string();
-
- let zig = env::var("ZIG").unwrap_or_else(|_| "zig".into());
- let mut command = Command::new(&zig);
- command
- .arg("build")
- .arg("-Demit-lib-vt")
- .arg(format!("-Doptimize={optimize}"))
- .arg(format!("-Dsimd={simd}"))
- .arg(format!("-Dtarget={zig_target}"))
- .arg(format!("-Dversion-string={version_string}"))
- .arg("-Demit-xcframework=false");
- if target.ends_with("windows-msvc") {
- if let Some(libc_file) = env::var_os("LIBGHOSTTY_VT_WINDOWS_LIBC") {
- println!(
- "cargo:rerun-if-changed={}",
- PathBuf::from(&libc_file).display()
- );
- command.arg("--libc").arg(libc_file);
- }
- }
- if let Ok(system_dir) = env::var("LIBGHOSTTY_VT_ZIG_SYSTEM_DIR") {
- command.arg("--system").arg(system_dir);
- }
-
- let status = command
- .current_dir(&vendored_dir)
- .status()
- .unwrap_or_else(|err| {
- if err.kind() == std::io::ErrorKind::NotFound {
- panic!(
- "zig executable not found (looked for {zig:?}; set the ZIG \
- environment variable to point at the zig binary). Building \
- the vendored libghostty-vt requires Zig 0.16.0: install it from \
- https://ziglang.org/download/, then retry the build"
- );
- }
- panic!("failed to execute zig build for vendored libghostty-vt: {err}");
- });
- assert!(
- status.success(),
- "zig build for vendored libghostty-vt failed: {status}. \
- Building Herdr requires Zig 0.16.0; check `zig version` \
- or set ZIG to the path of a Zig 0.16.0 binary, then retry"
- );
-
- let lib_dir = vendored_dir.join("zig-out/lib");
- println!("cargo:rustc-link-search=native={}", lib_dir.display());
- if target.contains("apple-darwin") {
- let static_lib = lib_dir.join("libghostty-vt.a");
- println!("cargo:rustc-link-arg={}", static_lib.display());
- } else if target.contains("windows-msvc") {
- println!("cargo:rustc-link-lib=static=ghostty-vt-static");
- } else {
- println!("cargo:rustc-link-lib=static=ghostty-vt");
- }
}
diff --git a/crates/ghostty-vt/Cargo.toml b/crates/ghostty-vt/Cargo.toml
new file mode 100644
index 00000000..53c44fda
--- /dev/null
+++ b/crates/ghostty-vt/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "ghostty-vt"
+version = "0.0.0"
+edition = "2021"
+description = "Herdr's binding to its vendored libghostty-vt"
+license = "Apache-2.0"
+repository = "https://github.com/herdrdev/herdr"
+publish = false
+
+[dependencies]
+base64 = "0.22.1"
+libc = "0.2"
+png = "0.17"
+tracing = "0.1.44"
diff --git a/crates/ghostty-vt/build.rs b/crates/ghostty-vt/build.rs
new file mode 100644
index 00000000..af02e0a7
--- /dev/null
+++ b/crates/ghostty-vt/build.rs
@@ -0,0 +1,137 @@
+use std::env;
+use std::fs;
+use std::path::PathBuf;
+use std::process::Command;
+
+fn zig_target(target: &str) -> &str {
+ match target {
+ "x86_64-unknown-linux-gnu" => "x86_64-linux-gnu",
+ "aarch64-unknown-linux-gnu" => "aarch64-linux-gnu",
+ "x86_64-unknown-linux-musl" => "x86_64-linux-musl",
+ "aarch64-unknown-linux-musl" => "aarch64-linux-musl",
+ "x86_64-apple-darwin" => "x86_64-macos",
+ "aarch64-apple-darwin" => "aarch64-macos",
+ "aarch64-apple-ios" => "aarch64-ios",
+ "aarch64-apple-ios-sim" => "aarch64-ios-simulator",
+ "x86_64-pc-windows-msvc" => "x86_64-windows-msvc",
+ "aarch64-pc-windows-msvc" => "aarch64-windows-msvc",
+ other => panic!("unsupported target for libghostty-vt build: {other}"),
+ }
+}
+
+fn env_bool(name: &str) -> Option<bool> {
+ match env::var(name) {
+ Ok(value) => match value.to_ascii_lowercase().as_str() {
+ "1" | "true" | "yes" | "on" => Some(true),
+ "0" | "false" | "no" | "off" => Some(false),
+ other => panic!("invalid boolean value for {name}: {other}"),
+ },
+ Err(env::VarError::NotPresent) => None,
+ Err(err) => panic!("failed to read {name}: {err}"),
+ }
+}
+
+fn main() {
+ // The vendored source stays at the repository root, shared with its maintenance scripts.
+ let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
+ let vendor_dir = manifest_dir.join("../../vendor");
+ let vendored_dir = vendor_dir.join("libghostty-vt");
+
+ println!("cargo:rerun-if-changed=build.rs");
+ for path in [
+ vendor_dir.join("libghostty-vt.vendor.json"),
+ vendored_dir.join("build.zig"),
+ vendored_dir.join("build.zig.zon"),
+ vendored_dir.join("include"),
+ vendored_dir.join("pkg"),
+ vendored_dir.join("src"),
+ vendored_dir.join("VERSION"),
+ ] {
+ println!("cargo:rerun-if-changed={}", path.display());
+ }
+ println!("cargo:rerun-if-env-changed=LIBGHOSTTY_VT_OPTIMIZE");
+ println!("cargo:rerun-if-env-changed=LIBGHOSTTY_VT_SIMD");
+ println!("cargo:rerun-if-env-changed=LIBGHOSTTY_VT_ZIG_SYSTEM_DIR");
+ println!("cargo:rerun-if-env-changed=ZIG");
+ println!("cargo:rerun-if-env-changed=LIBGHOSTTY_VT_WINDOWS_LIBC");
+
+ let optimize = env::var("LIBGHOSTTY_VT_OPTIMIZE").unwrap_or_else(|_| "ReleaseFast".into());
+ let simd = env_bool("LIBGHOSTTY_VT_SIMD").unwrap_or(true);
+ let target = env::var("TARGET").expect("TARGET");
+ let zig_target = zig_target(&target);
+ let version_string = fs::read_to_string(vendored_dir.join("VERSION"))
+ .expect("failed to read vendored libghostty-vt VERSION")
+ .trim()
+ .to_string();
+
+ // Install into this build's OUT_DIR so concurrent builds for different targets
+ // from one checkout never link each other's archive.
+ let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
+ let zig_prefix = out_dir.join("zig-out");
+
+ let zig = env::var("ZIG").unwrap_or_else(|_| "zig".into());
+ let mut command = Command::new(&zig);
+ command
+ .arg("build")
+ .arg("--prefix")
+ .arg(&zig_prefix)
+ .arg("-Demit-lib-vt")
+ .arg(format!("-Doptimize={optimize}"))
+ .arg(format!("-Dsimd={simd}"))
+ .arg(format!("-Dtarget={zig_target}"))
+ .arg(format!("-Dversion-string={version_string}"))
+ .arg("-Demit-xcframework=false");
+ if target.ends_with("windows-msvc") {
+ if let Some(libc_file) = env::var_os("LIBGHOSTTY_VT_WINDOWS_LIBC") {
+ println!(
+ "cargo:rerun-if-changed={}",
+ PathBuf::from(&libc_file).display()
+ );
+ command.arg("--libc").arg(libc_file);
+ }
+ }
+ if let Ok(system_dir) = env::var("LIBGHOSTTY_VT_ZIG_SYSTEM_DIR") {
+ command.arg("--system").arg(system_dir);
+ }
+
+ let status = command
+ .current_dir(&vendored_dir)
+ .status()
+ .unwrap_or_else(|err| {
+ if err.kind() == std::io::ErrorKind::NotFound {
+ panic!(
+ "zig executable not found (looked for {zig:?}; set the ZIG \
+ environment variable to point at the zig binary). Building \
+ the vendored libghostty-vt requires Zig 0.16.0: install it from \
+ https://ziglang.org/download/, then retry the build"
+ );
+ }
+ panic!("failed to execute zig build for vendored libghostty-vt: {err}");
+ });
+ assert!(
+ status.success(),
+ "zig build for vendored libghostty-vt failed: {status}. \
+ Building Herdr requires Zig 0.16.0; check `zig version` \
+ or set ZIG to the path of a Zig 0.16.0 binary, then retry"
+ );
+
+ let mut lib_dir = zig_prefix.join("lib");
+ if target.contains("-apple-") {
+ // Apple's linker prefers the sibling dylib for `-l`, so search a directory
+ // holding only the static archive.
+ let static_dir = out_dir.join("lib");
+ fs::create_dir_all(&static_dir).expect("failed to create libghostty-vt link directory");
+ fs::copy(
+ lib_dir.join("libghostty-vt.a"),
+ static_dir.join("libghostty-vt.a"),
+ )
+ .expect("failed to copy libghostty-vt static archive");
+ lib_dir = static_dir;
+ }
+ println!("cargo:rustc-link-search=native={}", lib_dir.display());
+ if target.contains("windows-msvc") {
+ println!("cargo:rustc-link-lib=static=ghostty-vt-static");
+ } else {
+ println!("cargo:rustc-link-lib=static=ghostty-vt");
+ }
+}
diff --git a/src/ghostty/bindings.rs b/crates/ghostty-vt/src/bindings.rs
index 33443aca..33443aca 100644
--- a/src/ghostty/bindings.rs
+++ b/crates/ghostty-vt/src/bindings.rs
diff --git a/src/ghostty/mod.rs b/crates/ghostty-vt/src/lib.rs
index 43a70fb1..ef62ef1e 100644
--- a/src/ghostty/mod.rs
+++ b/crates/ghostty-vt/src/lib.rs
@@ -25,7 +25,13 @@ use std::slice;
use std::sync::{Arc, Mutex, Once, OnceLock};
use crate::pane_graphics_files::OwnedExport;
+#[cfg(target_os = "linux")]
+mod native_image_sources;
mod native_source;
+pub mod pane_graphics_files;
+
+/// Terminfo entry the terminal emulates; child processes should see it as TERM.
+pub const TERM: &str = "xterm-256color";
pub use bindings as ffi;
@@ -77,7 +83,7 @@ pub enum Dirty {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub(crate) enum TerminalCompressionResult {
+pub enum TerminalCompressionResult {
Unsupported,
Pending,
Complete,
@@ -199,7 +205,7 @@ const TERMINAL_DATA_COLOR_CURSOR: ffi::GhosttyTerminalData = 20;
const KITTY_IMAGE_STORAGE_LIMIT_BYTES: u64 = 64 * 1024 * 1024;
const APC_MAX_BYTES: usize = 16 * 1024 * 1024;
const APC_MAX_BYTES_KITTY: usize = 16 * 1024 * 1024;
-pub(crate) const KITTY_UNICODE_PLACEHOLDER: u32 = 0x10EEEE;
+pub const KITTY_UNICODE_PLACEHOLDER: u32 = 0x10EEEE;
// The vendored C headers expose these placement fields, but the checked-in
// generated bindings predate the names. Keep the explicit values aligned with
// vendor/libghostty-vt/include/ghostty/vt/kitty_graphics.h.
@@ -230,7 +236,7 @@ pub struct KittyImagePlacement {
pub data_len: usize,
pub data_fingerprint: u64,
pub data: Vec<u8>,
- pub(crate) source_file: Option<Arc<OwnedExport>>,
+ pub source_file: Option<Arc<OwnedExport>>,
pub render: KittyPlacementRenderInfo,
}
@@ -463,13 +469,13 @@ pub enum CellWide {
}
#[derive(Debug, Clone, PartialEq, Eq)]
-pub(crate) struct ScreenTextCell {
+pub struct ScreenTextCell {
pub wide: CellWide,
pub graphemes: Vec<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
-pub(crate) struct ScreenTextRow {
+pub struct ScreenTextRow {
pub cells: Vec<ScreenTextCell>,
pub soft_wrapped: bool,
pub wrap_continuation: bool,
@@ -826,11 +832,19 @@ pub fn encode_focus(event: FocusEvent) -> Result<Vec<u8>, Error> {
}
#[derive(Debug, PartialEq, Eq)]
-pub(crate) enum LinkTarget {
+pub enum LinkTarget {
Uri(String),
Text { text: String, clicked_byte: usize },
}
+/// Inclusive display-cell columns on the current viewport.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct LinkRegion {
+ pub row: u16,
+ pub start_col: u16,
+ pub end_col: u16,
+}
+
pub struct Terminal {
raw: ffi::GhosttyTerminal,
max_scrollback: usize,
@@ -870,8 +884,8 @@ impl Terminal {
let userdata = (&mut *terminal.callback_state as *mut TerminalCallbackState).cast();
let glyph_protocol = false;
let terminfo_name = ffi::GhosttyString {
- ptr: crate::pane::PANE_TERM.as_ptr().cast(),
- len: crate::pane::PANE_TERM.len(),
+ ptr: TERM.as_ptr().cast(),
+ len: TERM.len(),
};
let grapheme_default = ffi::GhosttyTerminalModeConfig {
mode: MODE_GRAPHEME_CLUSTER,
@@ -951,7 +965,7 @@ impl Terminal {
}
}
- pub(crate) fn compression_activity(&self) -> Result<u64, Error> {
+ pub fn compression_activity(&self) -> Result<u64, Error> {
let mut activity = 0;
// SAFETY: self.raw is a live terminal handle and activity is a valid out pointer.
unsafe {
@@ -960,7 +974,7 @@ impl Terminal {
Ok(activity)
}
- pub(crate) fn compress_incremental(&mut self) -> Result<TerminalCompressionResult, Error> {
+ pub fn compress_incremental(&mut self) -> Result<TerminalCompressionResult, Error> {
let mut result =
ffi::GhosttyTerminalCompressionResult_GHOSTTY_TERMINAL_COMPRESSION_RESULT_UNSUPPORTED;
// SAFETY: self.raw is a live terminal handle and result is a valid out pointer.
@@ -1240,7 +1254,7 @@ impl Terminal {
}
#[cfg(windows)]
- pub(crate) fn track_row(&mut self, y: u32) -> Option<usize> {
+ pub fn track_row(&mut self, y: u32) -> Option<usize> {
let mut point = ffi::GhosttyPointCoordinate::default();
let tag = ffi::GhosttyPointTag_GHOSTTY_POINT_TAG_SCREEN;
let result =
@@ -1262,11 +1276,11 @@ impl Terminal {
Ok((wide, graphemes))
}
- pub(crate) fn screen_text_rows(&self) -> Result<Vec<ScreenTextRow>, Error> {
+ pub fn screen_text_rows(&self) -> Result<Vec<ScreenTextRow>, Error> {
self.screen_text_rows_range(0, usize::MAX)
}
- pub(crate) fn screen_text_rows_range(
+ pub fn screen_text_rows_range(
&self,
start_row: usize,
end_row_exclusive: usize,
@@ -1317,7 +1331,7 @@ impl Terminal {
grid_ref_hyperlink_uri(&grid_ref)
}
- pub(crate) fn viewport_link_target(&self, x: u16, y: u32) -> Result<Option<LinkTarget>, Error> {
+ pub fn viewport_link_target(&self, x: u16, y: u32) -> Result<Option<LinkTarget>, Error> {
Ok(self
.viewport_link_selection(x, y)?
.map(|(target, _)| target))
@@ -1390,12 +1404,12 @@ impl Terminal {
/// Resolve only the bounded plain-text token. OSC 8 regions are resolved by
/// clients from frame hyperlink IDs; their full URI activation path is unchanged.
- pub(crate) fn viewport_link_regions(
+ pub fn viewport_link_regions(
&self,
x: u16,
y: u32,
resolve: fn(&str, usize) -> Option<std::ops::Range<usize>>,
- ) -> Result<Vec<crate::api::schema::PaneLinkRegion>, Error> {
+ ) -> Result<Vec<LinkRegion>, Error> {
let cols = self.cols()?;
let rows = self.rows()?;
if x >= cols || y >= u32::from(rows) {
@@ -1461,7 +1475,7 @@ impl Terminal {
false,
)?;
let mut byte = prefix.len().saturating_sub(cell_len(&first)?);
- let mut regions: Vec<crate::api::schema::PaneLinkRegion> = Vec::new();
+ let mut regions: Vec<LinkRegion> = Vec::new();
for row in start_row..=end_row {
let mut cell = self.grid_ref(ghostty_viewport_point(0, row))?;
let left = if row == start_row { start_col } else { 0 };
@@ -1485,7 +1499,7 @@ impl Terminal {
{
last.end_col = end;
} else {
- regions.push(crate::api::schema::PaneLinkRegion {
+ regions.push(LinkRegion {
row: row as u16,
start_col: col,
end_col: end,
@@ -1716,11 +1730,11 @@ impl Terminal {
self.get_optional_rgb_color(TERMINAL_DATA_COLOR_CURSOR)
}
- pub(crate) fn width_px(&self) -> Result<u32, Error> {
+ pub fn width_px(&self) -> Result<u32, Error> {
self.get_u32(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_WIDTH_PX)
}
- pub(crate) fn height_px(&self) -> Result<u32, Error> {
+ pub fn height_px(&self) -> Result<u32, Error> {
self.get_u32(ffi::GhosttyTerminalData_GHOSTTY_TERMINAL_DATA_HEIGHT_PX)
}
@@ -1805,7 +1819,7 @@ impl Terminal {
)
}
- pub(crate) fn kitty_graphics_may_have_placements(&self) -> Result<bool, Error> {
+ pub fn kitty_graphics_may_have_placements(&self) -> Result<bool, Error> {
let generation = self.kitty_graphics_generation()?;
Ok(generation != 0 && self.kitty_empty_generation.get() != Some(generation))
}
@@ -2460,7 +2474,7 @@ fn kitty_placeholder_diacritic_index(codepoint: u32) -> Option<u32> {
// Reuse Ghostty's vendored table so Herdr decodes the same placeholder
// row/column diacritics that libghostty accepts.
let source =
- include_str!("../../vendor/libghostty-vt/src/terminal/kitty/graphics_unicode.zig");
+ include_str!("../../../vendor/libghostty-vt/src/terminal/kitty/graphics_unicode.zig");
let mut map = HashMap::new();
let mut in_table = false;
for line in source.lines() {
@@ -3159,6 +3173,8 @@ pub struct RowIter<'a> {
}
impl<'a> RowIter<'a> {
+ // Advances a native cursor; rows are read in place, so this is not an Iterator.
+ #[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> bool {
// SAFETY: iterator handle is valid while self is alive.
unsafe { ffi::ghostty_render_state_row_iterator_next(self.iterator.raw) }
@@ -3327,6 +3343,7 @@ impl Default for CellBasicData {
}
impl<'a> RowCellIter<'a> {
+ #[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> bool {
// SAFETY: cells handle is valid while self is alive.
unsafe { ffi::ghostty_render_state_row_cells_next(self.cells.raw) }
@@ -3989,6 +4006,8 @@ mod tests {
#[cfg(unix)]
#[test]
fn kitty_graphics_file_upload_can_be_placed_later() {
+ use base64::Engine as _;
+
let dir = std::env::temp_dir().join(format!(
"herdr-kitty-file-upload-test-{}",
std::process::id()
@@ -4000,14 +4019,10 @@ mod tests {
let mut terminal = Terminal::new(10, 5, 0).unwrap();
terminal.enable_kitty_graphics().unwrap();
terminal.resize(10, 5, 8, 16).unwrap();
- let mut upload = Vec::new();
- crate::kitty_graphics::encode_kitty_regular_file(
- &mut upload,
- &[],
- "a=t,f=32,s=1,v=1,i=10,q=0",
- path.to_str().unwrap(),
- );
- terminal.write(&upload);
+ let payload =
+ base64::engine::general_purpose::STANDARD.encode(path.to_str().unwrap().as_bytes());
+ let upload = format!("\x1b7\x1b_Ga=t,f=32,s=1,v=1,i=10,q=0,t=f;{payload}\x1b\\\x1b8");
+ terminal.write(upload.as_bytes());
assert!(terminal.kitty_image_placements().unwrap().is_empty());
terminal.write(b"\x1b_Ga=p,i=10,p=5,c=10,r=5,C=1,q=2\x1b\\");
diff --git a/src/platform/linux/native_image_sources.rs b/crates/ghostty-vt/src/native_image_sources.rs
index b4d3b073..b4d3b073 100644
--- a/src/platform/linux/native_image_sources.rs
+++ b/crates/ghostty-vt/src/native_image_sources.rs
diff --git a/src/ghostty/native_source.rs b/crates/ghostty-vt/src/native_source.rs
index 3e4e8a3f..3e4e8a3f 100644
--- a/src/ghostty/native_source.rs
+++ b/crates/ghostty-vt/src/native_source.rs
diff --git a/src/ghostty/native_source_tests.rs b/crates/ghostty-vt/src/native_source_tests.rs
index bb4ded46..bb4ded46 100644
--- a/src/ghostty/native_source_tests.rs
+++ b/crates/ghostty-vt/src/native_source_tests.rs
diff --git a/src/pane_graphics_files.rs b/crates/ghostty-vt/src/pane_graphics_files.rs
index 5c272bbe..c5f844d2 100644
--- a/src/pane_graphics_files.rs
+++ b/crates/ghostty-vt/src/pane_graphics_files.rs
@@ -9,13 +9,32 @@ use std::sync::{Arc, Mutex, OnceLock};
#[cfg(unix)]
use std::os::unix::fs::{DirBuilderExt, FileExt, MetadataExt, OpenOptionsExt, PermissionsExt};
+#[cfg(target_os = "linux")]
+use crate::native_image_sources::clone_native_image_source;
+
+/// Kernel CoW snapshots are deliberately unsupported outside Linux.
+#[cfg(not(target_os = "linux"))]
+fn clone_native_image_source(
+ _source_fd: i64,
+ _destination: &File,
+ _expected_len: usize,
+) -> io::Result<()> {
+ Err(io::Error::new(
+ io::ErrorKind::Unsupported,
+ "native source cloning requires Linux",
+ ))
+}
+
+// Herdr's default log filter is `herdr=info`; keep these warnings under its target.
+const LOG_TARGET: &str = "herdr::pane_graphics_files";
+
#[cfg(unix)]
const DIRECTORY_MODE: u32 = 0o700;
#[cfg(unix)]
const FILE_MODE: u32 = 0o600;
#[derive(Debug)]
-pub(crate) struct FileStore {
+pub struct FileStore {
base: PathBuf,
generation: OnceLock<Arc<Generation>>,
next_fingerprint: AtomicU64,
@@ -30,7 +49,7 @@ struct Generation {
/// Keep this owned snapshot alive until the terminal has consumed its path.
#[derive(Debug)]
-pub(crate) struct OwnedExport {
+pub struct OwnedExport {
lease: Lease,
_file: ExportFile,
_reservation: Option<Reservation>,
@@ -106,7 +125,7 @@ impl FileStore {
}
}
- pub(crate) fn native_sources() -> Self {
+ pub fn native_sources() -> Self {
let mut store = Self::new(native_base());
store.native_budget = Some(Arc::new(Mutex::new(NativeBudget::default())));
store
@@ -114,7 +133,7 @@ impl FileStore {
/// Kernel-only bounded snapshot; any unsupported clone returns an error.
/// The caller keeps the borrowed descriptor alive throughout this call.
- pub(crate) fn snapshot(&self, source_fd: i64, expected_len: usize) -> io::Result<OwnedExport> {
+ pub fn snapshot(&self, source_fd: i64, expected_len: usize) -> io::Result<OwnedExport> {
if expected_len == 0 || expected_len > MAX_EXPORT_BYTES || !expected_len.is_multiple_of(4) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
@@ -133,7 +152,7 @@ impl FileStore {
path,
_generation: generation,
};
- crate::platform::clone_native_image_source(source_fd, &destination, expected_len)?;
+ clone_native_image_source(source_fd, &destination, expected_len)?;
drop(destination);
let lease = self.lease(&file.path, expected_len)?;
Ok(OwnedExport {
@@ -143,8 +162,9 @@ impl FileStore {
})
}
- #[cfg(all(test, unix))]
- pub(crate) fn source_directory(&self) -> io::Result<PathBuf> {
+ // Not test-gated: herdr's own tests use it, and a dependency's cfg(test) is off.
+ #[cfg(unix)]
+ pub fn source_directory(&self) -> io::Result<PathBuf> {
Ok(self.generation()?.source.clone())
}
@@ -168,7 +188,7 @@ impl FileStore {
/// Snapshot decoded bytes into a private, uniquely named file. Aggregate
/// outstanding-export limits are the caller's responsibility.
- pub(crate) fn export(&self, data: &[u8]) -> io::Result<OwnedExport> {
+ pub fn export(&self, data: &[u8]) -> io::Result<OwnedExport> {
if data.len() > MAX_EXPORT_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
@@ -230,7 +250,7 @@ impl PartialEq for OwnedExport {
impl Eq for OwnedExport {}
impl OwnedExport {
- pub(crate) fn copy_rgba(&self) -> io::Result<Vec<u8>> {
+ pub fn copy_rgba(&self) -> io::Result<Vec<u8>> {
self.lease.copy_rgba()
}
@@ -238,15 +258,16 @@ impl OwnedExport {
self.lease.read_into(data)
}
- pub(crate) fn path(&self) -> &Path {
+ pub fn path(&self) -> &Path {
self.lease.path()
}
- pub(crate) fn len(&self) -> usize {
+ #[allow(clippy::len_without_is_empty)]
+ pub fn len(&self) -> usize {
self.lease.len()
}
- pub(crate) fn fingerprint(&self) -> u64 {
+ pub fn fingerprint(&self) -> u64 {
self.lease.fingerprint()
}
}
@@ -255,7 +276,7 @@ impl Drop for ExportFile {
fn drop(&mut self) {
if let Err(err) = fs::remove_file(&self.path) {
if err.kind() != io::ErrorKind::NotFound {
- tracing::warn!(path = %self.path.display(), err = %err, "failed to remove decoded graphics export");
+ tracing::warn!(target: LOG_TARGET, path = %self.path.display(), err = %err, "failed to remove decoded graphics export");
}
}
}
@@ -306,14 +327,14 @@ impl Drop for Generation {
fn drop(&mut self) {
if let Err(err) = fs::remove_dir_all(&self.root) {
if err.kind() != io::ErrorKind::NotFound {
- tracing::warn!(path = %self.root.display(), err = %err, "failed to remove pane graphics directory");
+ tracing::warn!(target: LOG_TARGET, path = %self.root.display(), err = %err, "failed to remove pane graphics directory");
}
}
}
}
#[cfg(unix)]
-pub(crate) fn validate_direct_source(path: &Path, expected_len: usize) -> io::Result<()> {
+pub fn validate_direct_source(path: &Path, expected_len: usize) -> io::Result<()> {
validate_source_under(path, expected_len, runtime_base())
}
@@ -343,7 +364,7 @@ fn validate_source_under(path: &Path, expected_len: usize, base: PathBuf) -> io:
/// Unlike the API validator, accepts only the dedicated native hierarchy.
#[cfg(unix)]
-pub(crate) fn validate_native_source(path: &Path, expected_len: usize) -> io::Result<()> {
+pub fn validate_native_source(path: &Path, expected_len: usize) -> io::Result<()> {
if expected_len == 0 || expected_len > MAX_EXPORT_BYTES || !expected_len.is_multiple_of(4) {
return Err(invalid_path());
}
@@ -463,7 +484,7 @@ fn remove_stale_generations(base: &Path) {
continue;
}
if let Err(err) = fs::remove_dir_all(entry.path()) {
- tracing::warn!(path = %entry.path().display(), err = %err, "failed to remove stale pane graphics directory");
+ tracing::warn!(target: LOG_TARGET, path = %entry.path().display(), err = %err, "failed to remove stale pane graphics directory");
}
}
}
diff --git a/src/ghostty/png_forward_tests.rs b/crates/ghostty-vt/src/png_forward_tests.rs
index 6d3904b2..6d3904b2 100644
--- a/src/ghostty/png_forward_tests.rs
+++ b/crates/ghostty-vt/src/png_forward_tests.rs
diff --git a/nix/package.nix b/nix/package.nix
index 2a506c3a..06ea6055 100644
--- a/nix/package.nix
+++ b/nix/package.nix
@@ -40,6 +40,7 @@ rustPlatform.buildRustPackage {
fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ./..)) (
lib.fileset.unions [
../assets
+ ../crates
../distribution/install.ps1
../docs/next/api/herdr-api.schema.json
../src
diff --git a/scripts/generate_libghostty_bindings.sh b/scripts/generate_libghostty_bindings.sh
index c87efca7..0868b73e 100644
--- a/scripts/generate_libghostty_bindings.sh
+++ b/scripts/generate_libghostty_bindings.sh
@@ -8,5 +8,5 @@ cd "$(dirname "$0")/.."
--allowlist-function 'ghostty_.*' \
--allowlist-var 'GHOSTTY_.*' \
--with-derive-default \
- --output src/ghostty/bindings.rs \
+ --output crates/ghostty-vt/src/bindings.rs \
-- -Ivendor/libghostty-vt/include "$@"
diff --git a/src/app/actions.rs b/src/app/actions.rs
index 26bdf0e4..6a6e99d2 100644
--- a/src/app/actions.rs
+++ b/src/app/actions.rs
@@ -2364,7 +2364,7 @@ mod tests {
fn link_resolution_regions_keep_grapheme_byte_offsets() {
let mut terminal = crate::ghostty::Terminal::new(40, 3, 1024).unwrap();
terminal.write("e\u{301}(https://example.com/路e\u{301}),".as_bytes());
- let expected = vec![crate::api::schema::PaneLinkRegion {
+ let expected = vec![crate::ghostty::LinkRegion {
row: 0,
start_col: 2,
end_col: 24,
@@ -2392,7 +2392,7 @@ mod tests {
.unwrap();
assert_eq!(
regions,
- vec![crate::api::schema::PaneLinkRegion {
+ vec![crate::ghostty::LinkRegion {
row: 0,
start_col: 0,
end_col: 3
diff --git a/src/main.rs b/src/main.rs
index d5275529..45a64095 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -23,7 +23,7 @@ mod config;
mod copy_mode;
mod detect;
mod events;
-mod ghostty;
+use ghostty_vt as ghostty;
mod handoff_runtime;
mod input;
mod integration;
@@ -34,7 +34,7 @@ mod logging;
mod metadata_tokens;
mod noninteractive_process;
mod pane;
-mod pane_graphics_files;
+use ghostty_vt::pane_graphics_files;
mod persist;
mod platform;
mod plugin_command;
diff --git a/src/pane.rs b/src/pane.rs
index 7d42e9c0..35534135 100644
--- a/src/pane.rs
+++ b/src/pane.rs
@@ -65,7 +65,7 @@ pub(crate) struct TerminalDirtyPatchSnapshot {
const RELEASE_REACQUIRE_SUPPRESSION: std::time::Duration = std::time::Duration::from_secs(1);
const TERMINAL_COMPRESSION_IDLE: std::time::Duration = std::time::Duration::from_millis(250);
const TERMINAL_COMPRESSION_STEP: std::time::Duration = std::time::Duration::from_millis(1);
-pub(crate) const PANE_TERM: &str = "xterm-256color";
+pub(crate) const PANE_TERM: &str = crate::ghostty::TERM;
const PANE_COLORTERM: &str = "truecolor";
fn terminal_compression_permits() -> Arc<tokio::sync::Semaphore> {
diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs
index d0b9d3ca..5eaddb21 100644
--- a/src/pane/terminal.rs
+++ b/src/pane/terminal.rs
@@ -2289,6 +2289,13 @@ impl GhosttyPaneTerminal {
.ok()
})
.unwrap_or_default()
+ .into_iter()
+ .map(|region| crate::api::schema::PaneLinkRegion {
+ row: region.row,
+ start_col: region.start_col,
+ end_col: region.end_col,
+ })
+ .collect()
}
pub(crate) fn link_target_at(&self, col: u16, row: u16) -> Option<crate::ghostty::LinkTarget> {
diff --git a/src/platform/linux.rs b/src/platform/linux.rs
index 9be40488..c73584e5 100644
--- a/src/platform/linux.rs
+++ b/src/platform/linux.rs
@@ -1,6 +1,3 @@
-mod native_image_sources;
-pub(crate) use native_image_sources::clone_native_image_source;
-
use std::{
collections::{HashSet, VecDeque},
io::{Read, Write},
diff --git a/src/platform/mod.rs b/src/platform/mod.rs
index 65717228..927cd32b 100644
--- a/src/platform/mod.rs
+++ b/src/platform/mod.rs
@@ -727,16 +727,3 @@ pub(crate) fn shared_ssh_control_path(
"interactive SSH recovery requires Unix OpenSSH multiplexing",
))
}
-
-/// Kernel CoW snapshots are deliberately unsupported outside Linux.
-#[cfg(not(target_os = "linux"))]
-pub(crate) fn clone_native_image_source(
- _source_fd: i64,
- _destination: &std::fs::File,
- _expected_len: usize,
-) -> std::io::Result<()> {
- Err(std::io::Error::new(
- std::io::ErrorKind::Unsupported,
- "native source cloning requires Linux",
- ))
-}
diff --git a/tests/support/terminal_screen.rs b/tests/support/terminal_screen.rs
index 45d7731b..b9d51759 100644
--- a/tests/support/terminal_screen.rs
+++ b/tests/support/terminal_screen.rs
@@ -1,14 +1,7 @@
use std::{mem::size_of, ptr};
// Reuse the generated C API; this test observer needs only terminal parsing and formatting.
-#[allow(
- dead_code,
- non_camel_case_types,
- non_snake_case,
- non_upper_case_globals
-)]
-#[path = "../../src/ghostty/bindings.rs"]
-mod ffi;
+use ghostty_vt::ffi;
struct Screen {
terminal: ffi::GhosttyTerminal,