libghostty: add initial C API for terminal, formatter (#11506)

This adds an initial C API for terminals and formatting. There is a new
example that shows how to use this.

With these APIs in place, users of the C API can now create a terminal,
pass raw VT streams to it, and dump the terminal viewport to various
formats. As noted in the docs, **the formatter API is not a rendering
API**, it isn't high performance enough for that. But it's a simpler API
to implement than the render state API so I started with that.

Both APIs are purposely fairly minimal, we're just setting the stage for
future functionality.

## Example

```c
#include <ghostty/vt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
  GhosttyTerminal term;
  GhosttyTerminalOptions opts = { .cols = 80, .rows = 24, .max_scrollback = 0 };
  ghostty_terminal_new(NULL, &term, opts);

  const char *input = "Hello, \033[1mBold\033[0m World!\r\nLine 2\r\n";
  ghostty_terminal_vt_write(term, (const uint8_t *)input, strlen(input));

  GhosttyFormatterTerminalOptions fmt = GHOSTTY_INIT_SIZED(GhosttyFormatterTerminalOptions);
  fmt.emit = GHOSTTY_FORMATTER_FORMAT_PLAIN;
  fmt.trim = true;

  GhosttyFormatter fmtr;
  ghostty_formatter_terminal_new(NULL, &fmtr, term, fmt);

  uint8_t *buf;
  size_t len;
  ghostty_formatter_format_alloc(fmtr, NULL, &buf, &len);
  fwrite(buf, 1, len, stdout);

  free(buf);
  ghostty_formatter_free(fmtr);
  ghostty_terminal_free(term);
}
```

## New APIs

| Function | Description |
|----------|-------------|
| `ghostty_terminal_new` | Create a new terminal instance |
| `ghostty_terminal_free` | Free a terminal instance |
| `ghostty_terminal_reset` | Full reset of the terminal (RIS) |
| `ghostty_terminal_resize` | Resize the terminal to given dimensions |
| `ghostty_terminal_vt_write` | Write VT-encoded data to the terminal |
| `ghostty_terminal_scroll_viewport` | Scroll the terminal viewport |
| `ghostty_formatter_terminal_new` | Create a formatter for a terminal's
active screen |
| `ghostty_formatter_format_buf` | Format into a caller-provided buffer
|
| `ghostty_formatter_format_alloc` | Format into an allocated buffer |
| `ghostty_formatter_free` | Free a formatter instance |

## Future

- Obviously need to expose a lot more from the terminal:
  * Read current set modes
  * Read cursor information
  * Read screen information
  * etc...
- Need an optional callback system so that `vt_write` can invoke
callbacks for side effect sequences like clipboards, title setting,
responses, etc.
- `terminal.RenderState` C API so that people can build high performance
renderers on top of libghostty-vt

And so on...
This commit is contained in:
Mitchell Hashimoto
2026-03-14 15:42:23 -07:00
committed by GitHub
26 changed files with 1569 additions and 140 deletions

View File

@@ -1,64 +0,0 @@
#!/usr/bin/env nu
# A command to generate an agent prompt to diagnose and formulate
# a plan for resolving a GitHub issue.
#
# IMPORTANT: This command is prompted to NOT write any code and to ONLY
# produce a plan. You should still be vigilant when running this but that
# is the expected behavior.
#
# The `<issue>` parameter can be either an issue number or a full GitHub
# issue URL.
def main [
issue: any, # Ghostty issue number or URL
--repo: string = "ghostty-org/ghostty" # GitHub repository in the format "owner/repo"
] {
# TODO: This whole script doesn't handle errors very well. I actually
# don't know Nu well enough to know the proper way to handle it all.
let issueData = gh issue view $issue --json author,title,number,body,comments | from json
let comments = $issueData.comments | each { |comment|
$"
### Comment by ($comment.author.login)
($comment.body)
" | str trim
} | str join "\n\n"
$"
Deep-dive on this GitHub issue. Find the problem and generate a plan.
Do not write code. Explain the problem clearly and propose a comprehensive plan
to solve it.
# ($issueData.title) \(($issueData.number)\)
## Description
($issueData.body)
## Comments
($comments)
## Your Tasks
You are an experienced software developer tasked with diagnosing issues.
1. Review the issue context and details.
2. Examine the relevant parts of the codebase. Analyze the code thoroughly
until you have a solid understanding of how it works.
3. Explain the issue in detail, including the problem and its root cause.
4. Create a comprehensive plan to solve the issue. The plan should include:
- Required code changes
- Potential impacts on other parts of the system
- Necessary tests to be written or updated
- Documentation updates
- Performance considerations
- Security implications
- Backwards compatibility \(if applicable\)
- Include the reference link to the source issue and any related discussions
4. Think deeply about all aspects of the task. Consider edge cases, potential
challenges, and best practices for addressing the issue. Review the plan
with the oracle and adjust it based on its feedback.
**ONLY CREATE A PLAN. DO NOT WRITE ANY CODE.** Your task is to create
a thorough, comprehensive strategy for understanding and resolving the issue.
" | str trim
}

View File

@@ -0,0 +1,62 @@
---
name: writing-commit-messages
description: >-
Writes Git commit messages. Activates when the user asks to write
a commit message, draft a commit message, or similar.
---
# Writing Commit Messages
Write commit messages that follow commit style guidelines for the project.
## Format
```
<subsystem>: <summary>
<reference issues/PRs/etc.>
<long form description>
```
## Rules
### Subject line
- **Subsystem prefix**: Use a short, lowercase identifier for the
area of code changed (e.g., `terminal`, `vt`, `lib`, `config`,
`font`). Determine this from the file paths in the diff. If
changes span the macOS app, use `macos`. For GTK, use `gtk`. For
build system, use `build`. Use nested subsystems with `/` when
helpful and exclusive (e.g., `terminal/osc`).
- **Summary**: Lowercase start (not capitalized), imperative mood,
no trailing period. Keep it concise—ideally under 60 characters
total for the whole subject line.
### References
- If the change relates to a GitHub issue, PR, or discussion, list
the relevant numbers on their own lines after the subject, separated
by a blank line. E.g. `#1234`
- If there are no references, omit this section entirely (no blank
line).
### Long form description
- Describe **what changed**, **what the previous behavior was**,
and **how the new behavior works** at a high level.
- Use plain prose, not bullet points. Wrap lines at ~72 characters.
- Focus on the _why_ and _how_ rather than restating the diff.
- Keep the tone direct and technical without no filler phrases.
- Don't exceed a handful of paragraphs; less is more.
## Workflow
- If `.jj` is present, use `jj` instead of `git` for all commands.
- Run a diff to see what changes are present since the last commit.
- Identify the subsystem from the changed file paths.
- Identify any referenced issues/PRs from the diff context or
branch name.
- Draft the commit message following the format above.
- Apply the commit
- Don't push the commit; leave that to the user.

View File

@@ -0,0 +1,18 @@
# Example: `ghostty-vt` Terminal Formatter
This contains a simple example of how to use the `ghostty-vt` terminal and
formatter APIs to create a terminal, write VT-encoded content into it, and
format the screen contents as plain text.
This uses a `build.zig` and `Zig` to build the C program so that we
can reuse a lot of our build logic and depend directly on our source
tree, but Ghostty emits a standard C library that can be used with any
C tooling.
## 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.c"},
});
// 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 = "c_vt_formatter",
.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 = .c_vt_formatter,
.version = "0.0.0",
.fingerprint = 0x9e3758265677a0c4,
.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,63 @@
#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 VT-encoded content into the terminal to exercise various
// cursor movement and styling sequences.
const char *commands[] = {
"Line 1: Hello World!\r\n", // Simple text on row 1
"Line 2: \033[1mBold\033[0m and " // Bold text on row 2
"\033[4mUnderline\033[0m\r\n",
"Line 3: placeholder\r\n", // Will be overwritten below
"\033[3;1H", // CUP: move cursor back to row 3, col 1
"\033[2K", // EL: erase the entire line
"Line 3: Overwritten!\r\n", // Rewrite row 3 with new content
"\033[5;10H", // CUP: jump to row 5, col 10
"Placed at (5,10)", // Write at that position
"\033[1;72H", // CUP: jump to row 1, col 72
"RIGHT->", // Near the right edge of row 1
};
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]));
}
// Create a plain-text formatter for the terminal
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);
// Format into an allocated buffer
uint8_t *buf = NULL;
size_t len = 0;
result = ghostty_formatter_format_alloc(formatter, NULL, &buf, &len);
assert(result == GHOSTTY_SUCCESS);
// Print the formatted output
printf("Formatted output (%zu bytes):\n", len);
fwrite(buf, 1, len, stdout);
printf("\n");
// Clean up
free(buf);
ghostty_formatter_free(formatter);
ghostty_terminal_free(terminal);
return 0;
}

View File

@@ -28,6 +28,8 @@
* @section groups_sec API Reference
*
* The API is organized into the following groups:
* - @ref terminal "Terminal" - Complete terminal emulator state and rendering
* - @ref formatter "Formatter" - Format terminal content as plain text, VT sequences, or HTML
* - @ref key "Key Encoding" - Encode key events into terminal sequences
* - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences
* - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences
@@ -42,6 +44,7 @@
* - @ref c-vt-key-encode/src/main.c - Key encoding example
* - @ref c-vt-paste/src/main.c - Paste safety check example
* - @ref c-vt-sgr/src/main.c - SGR parser example
* - @ref c-vt-formatter/src/main.c - Terminal formatter example
*
*/
@@ -65,6 +68,12 @@
* styling sequences and extract text attributes like colors and underline styles.
*/
/** @example c-vt-formatter/src/main.c
* This example demonstrates how to use the terminal and formatter APIs to
* create a terminal, write VT-encoded content into it, and format the screen
* contents as plain text.
*/
#ifndef GHOSTTY_VT_H
#define GHOSTTY_VT_H
@@ -72,8 +81,10 @@
extern "C" {
#endif
#include <ghostty/vt/result.h>
#include <ghostty/vt/types.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/formatter.h>
#include <ghostty/vt/terminal.h>
#include <ghostty/vt/osc.h>
#include <ghostty/vt/sgr.h>
#include <ghostty/vt/key.h>

View File

@@ -0,0 +1,226 @@
/**
* @file formatter.h
*
* Format terminal content as plain text, VT sequences, or HTML.
*/
#ifndef GHOSTTY_VT_FORMATTER_H
#define GHOSTTY_VT_FORMATTER_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/types.h>
#include <ghostty/vt/terminal.h>
#ifdef __cplusplus
extern "C" {
#endif
/** @defgroup formatter Formatter
*
* Format terminal content as plain text, VT sequences, or HTML.
*
* A formatter captures a reference to a terminal and formatting options.
* It can be used repeatedly to produce output that reflects the current
* terminal state at the time of each format call.
*
* The terminal must outlive the formatter.
*
* @{
*/
/**
* Output format.
*
* @ingroup formatter
*/
typedef enum {
/** Plain text (no escape sequences). */
GHOSTTY_FORMATTER_FORMAT_PLAIN,
/** VT sequences preserving colors, styles, URLs, etc. */
GHOSTTY_FORMATTER_FORMAT_VT,
/** HTML with inline styles. */
GHOSTTY_FORMATTER_FORMAT_HTML,
} GhosttyFormatterFormat;
/**
* Extra screen state to include in styled output.
*
* @ingroup formatter
*/
typedef struct {
/** Size of this struct in bytes. Must be set to sizeof(GhosttyFormatterScreenExtra). */
size_t size;
/** Emit cursor position using CUP (CSI H). */
bool cursor;
/** Emit current SGR style state based on the cursor's active style_id. */
bool style;
/** Emit current hyperlink state using OSC 8 sequences. */
bool hyperlink;
/** Emit character protection mode using DECSCA. */
bool protection;
/** Emit Kitty keyboard protocol state using CSI > u and CSI = sequences. */
bool kitty_keyboard;
/** Emit character set designations and invocations. */
bool charsets;
} GhosttyFormatterScreenExtra;
/**
* Extra terminal state to include in styled output.
*
* @ingroup formatter
*/
typedef struct {
/** Size of this struct in bytes. Must be set to sizeof(GhosttyFormatterTerminalExtra). */
size_t size;
/** Emit the palette using OSC 4 sequences. */
bool palette;
/** Emit terminal modes that differ from their defaults using CSI h/l. */
bool modes;
/** Emit scrolling region state using DECSTBM and DECSLRM sequences. */
bool scrolling_region;
/** Emit tabstop positions by clearing all tabs and setting each one. */
bool tabstops;
/** Emit the present working directory using OSC 7. */
bool pwd;
/** Emit keyboard modes such as ModifyOtherKeys. */
bool keyboard;
/** Screen-level extras. */
GhosttyFormatterScreenExtra screen;
} GhosttyFormatterTerminalExtra;
/**
* Opaque handle to a formatter instance.
*
* @ingroup formatter
*/
typedef struct GhosttyFormatter* GhosttyFormatter;
/**
* Options for creating a terminal formatter.
*
* @ingroup formatter
*/
typedef struct {
/** Size of this struct in bytes. Must be set to sizeof(GhosttyFormatterTerminalOptions). */
size_t size;
/** Output format to emit. */
GhosttyFormatterFormat emit;
/** Whether to unwrap soft-wrapped lines. */
bool unwrap;
/** Whether to trim trailing whitespace on non-blank lines. */
bool trim;
/** Extra terminal state to include in styled output. */
GhosttyFormatterTerminalExtra extra;
} GhosttyFormatterTerminalOptions;
/**
* Create a formatter for a terminal's active screen.
*
* The terminal must outlive the formatter. The formatter stores a borrowed
* reference to the terminal and reads its current state on each format call.
*
* @param allocator Pointer to allocator, or NULL to use the default allocator
* @param formatter Pointer to store the created formatter handle
* @param terminal The terminal to format (must not be NULL)
* @param options Formatting options
* @return GHOSTTY_SUCCESS on success, or an error code on failure
*
* @ingroup formatter
*/
GhosttyResult ghostty_formatter_terminal_new(
const GhosttyAllocator* allocator,
GhosttyFormatter* formatter,
GhosttyTerminal terminal,
GhosttyFormatterTerminalOptions options);
/**
* Run the formatter and produce output into the caller-provided buffer.
*
* Each call formats the current terminal state. Pass NULL for buf to
* query the required buffer size without writing any output; in that case
* out_written receives the required size and the return value is
* GHOSTTY_OUT_OF_SPACE.
*
* If the buffer is too small, returns GHOSTTY_OUT_OF_SPACE and sets
* out_written to the required size. The caller can then retry with a
* larger buffer.
*
* @param formatter The formatter handle (must not be NULL)
* @param buf Pointer to the output buffer, or NULL to query size
* @param buf_len Length of the output buffer in bytes
* @param out_written Pointer to receive the number of bytes written,
* or the required size on failure
* @return GHOSTTY_SUCCESS on success, or an error code on failure
*
* @ingroup formatter
*/
GhosttyResult ghostty_formatter_format_buf(GhosttyFormatter formatter,
uint8_t* buf,
size_t buf_len,
size_t* out_written);
/**
* Run the formatter and return an allocated buffer with the output.
*
* Each call formats the current terminal state. The buffer is allocated
* using the provided allocator (or the default allocator if NULL).
* The caller is responsible for freeing the returned buffer. When using
* the default allocator (NULL), the buffer can be freed with `free()`.
* When using a custom allocator, the buffer must be freed using the
* same allocator.
*
* @param formatter The formatter handle (must not be NULL)
* @param allocator Pointer to allocator, or NULL to use the default allocator
* @param out_ptr Pointer to receive the allocated buffer
* @param out_len Pointer to receive the length of the output in bytes
* @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation
* failure
*
* @ingroup formatter
*/
GhosttyResult ghostty_formatter_format_alloc(GhosttyFormatter formatter,
const GhosttyAllocator* allocator,
uint8_t** out_ptr,
size_t* out_len);
/**
* Free a formatter instance.
*
* Releases all resources associated with the formatter. After this call,
* the formatter handle becomes invalid.
*
* @param formatter The formatter handle to free (may be NULL)
*
* @ingroup formatter
*/
void ghostty_formatter_free(GhosttyFormatter formatter);
/** @} */
#ifdef __cplusplus
}
#endif
#endif /* GHOSTTY_VT_FORMATTER_H */

View File

@@ -9,7 +9,7 @@
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/result.h>
#include <ghostty/vt/types.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/key/event.h>

View File

@@ -10,7 +10,7 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/result.h>
#include <ghostty/vt/types.h>
#include <ghostty/vt/allocator.h>
/**

View File

@@ -10,7 +10,7 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/result.h>
#include <ghostty/vt/types.h>
#include <ghostty/vt/allocator.h>
/**

View File

@@ -1,22 +0,0 @@
/**
* @file result.h
*
* Result codes for libghostty-vt operations.
*/
#ifndef GHOSTTY_VT_RESULT_H
#define GHOSTTY_VT_RESULT_H
/**
* Result codes for libghostty-vt operations.
*/
typedef enum {
/** Operation completed successfully */
GHOSTTY_SUCCESS = 0,
/** Operation failed due to failed allocation */
GHOSTTY_OUT_OF_MEMORY = -1,
/** Operation failed due to invalid value */
GHOSTTY_INVALID_VALUE = -2,
} GhosttyResult;
#endif /* GHOSTTY_VT_RESULT_H */

View File

@@ -73,7 +73,7 @@
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/color.h>
#include <ghostty/vt/result.h>
#include <ghostty/vt/types.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

View File

@@ -0,0 +1,200 @@
/**
* @file terminal.h
*
* Complete terminal emulator state and rendering.
*/
#ifndef GHOSTTY_VT_TERMINAL_H
#define GHOSTTY_VT_TERMINAL_H
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/types.h>
#include <ghostty/vt/allocator.h>
#ifdef __cplusplus
extern "C" {
#endif
/** @defgroup terminal Terminal
*
* Complete terminal emulator state and rendering.
*
* A terminal instance manages the full emulator state including the screen,
* scrollback, cursor, styles, modes, and VT stream processing.
*
* @{
*/
/**
* Opaque handle to a terminal instance.
*
* @ingroup terminal
*/
typedef struct GhosttyTerminal* GhosttyTerminal;
/**
* Terminal initialization options.
*
* @ingroup terminal
*/
typedef struct {
/** Terminal width in cells. Must be greater than zero. */
uint16_t cols;
/** Terminal height in cells. Must be greater than zero. */
uint16_t rows;
/** Maximum number of lines to keep in scrollback history. */
size_t max_scrollback;
// TODO: Consider ABI compatibility implications of this struct.
// We may want to artificially pad it significantly to support
// future options.
} GhosttyTerminalOptions;
/**
* Scroll viewport behavior tag.
*
* @ingroup terminal
*/
typedef enum {
/** Scroll to the top of the scrollback. */
GHOSTTY_SCROLL_VIEWPORT_TOP,
/** Scroll to the bottom (active area). */
GHOSTTY_SCROLL_VIEWPORT_BOTTOM,
/** Scroll by a delta amount (up is negative). */
GHOSTTY_SCROLL_VIEWPORT_DELTA,
} GhosttyTerminalScrollViewportTag;
/**
* Scroll viewport value.
*
* @ingroup terminal
*/
typedef union {
/** Scroll delta (only used with GHOSTTY_SCROLL_VIEWPORT_DELTA). Up is negative. */
intptr_t delta;
/** Padding for ABI compatibility. Do not use. */
uint64_t _padding[2];
} GhosttyTerminalScrollViewportValue;
/**
* Tagged union for scroll viewport behavior.
*
* @ingroup terminal
*/
typedef struct {
GhosttyTerminalScrollViewportTag tag;
GhosttyTerminalScrollViewportValue value;
} GhosttyTerminalScrollViewport;
/**
* Create a new terminal instance.
*
* @param allocator Pointer to allocator, or NULL to use the default allocator
* @param terminal Pointer to store the created terminal handle
* @param options Terminal initialization options
* @return GHOSTTY_SUCCESS on success, or an error code on failure
*
* @ingroup terminal
*/
GhosttyResult ghostty_terminal_new(const GhosttyAllocator* allocator,
GhosttyTerminal* terminal,
GhosttyTerminalOptions options);
/**
* Free a terminal instance.
*
* Releases all resources associated with the terminal. After this call,
* the terminal handle becomes invalid and must not be used.
*
* @param terminal The terminal handle to free (may be NULL)
*
* @ingroup terminal
*/
void ghostty_terminal_free(GhosttyTerminal terminal);
/**
* Perform a full reset of the terminal (RIS).
*
* Resets all terminal state back to its initial configuration, including
* modes, scrollback, scrolling region, and screen contents. The terminal
* dimensions are preserved.
*
* @param terminal The terminal handle (may be NULL, in which case this is a no-op)
*
* @ingroup terminal
*/
void ghostty_terminal_reset(GhosttyTerminal terminal);
/**
* Resize the terminal to the given dimensions.
*
* Changes the number of columns and rows in the terminal. The primary
* screen will reflow content if wraparound mode is enabled; the alternate
* screen does not reflow. If the dimensions are unchanged, this is a no-op.
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param cols New width in cells (must be greater than zero)
* @param rows New height in cells (must be greater than zero)
* @return GHOSTTY_SUCCESS on success, or an error code on failure
*
* @ingroup terminal
*/
GhosttyResult ghostty_terminal_resize(GhosttyTerminal terminal,
uint16_t cols,
uint16_t rows);
/**
* Write VT-encoded data to the terminal for processing.
*
* Feeds raw bytes through the terminal's VT stream parser, updating
* terminal state accordingly. Only read-only sequences are processed;
* sequences that require output (queries) are ignored.
*
* In the future, a callback-based API will be added to allow handling
* of output or side effect sequences.
*
* This never fails. Any erroneous input or errors in processing the
* input are logged internally but do not cause this function to fail
* because this input is assumed to be untrusted and from an external
* source; so the primary goal is to keep the terminal state consistent and
* not allow malformed input to corrupt or crash.
*
* @param terminal The terminal handle
* @param data Pointer to the data to write
* @param len Length of the data in bytes
*
* @ingroup terminal
*/
void ghostty_terminal_vt_write(GhosttyTerminal terminal,
const uint8_t* data,
size_t len);
/**
* Scroll the terminal viewport.
*
* Scrolls the terminal's viewport according to the given behavior.
* When using GHOSTTY_SCROLL_VIEWPORT_DELTA, set the delta field in
* the value union to specify the number of rows to scroll (negative
* for up, positive for down). For other behaviors, the value is ignored.
*
* @param terminal The terminal handle (may be NULL, in which case this is a no-op)
* @param behavior The scroll behavior as a tagged union
*
* @ingroup terminal
*/
void ghostty_terminal_scroll_viewport(GhosttyTerminal terminal,
GhosttyTerminalScrollViewport behavior);
/** @} */
#ifdef __cplusplus
}
#endif
#endif /* GHOSTTY_VT_TERMINAL_H */

View File

@@ -0,0 +1,45 @@
/**
* @file types.h
*
* Common types, macros, and utilities for libghostty-vt.
*/
#ifndef GHOSTTY_VT_TYPES_H
#define GHOSTTY_VT_TYPES_H
/**
* Result codes for libghostty-vt operations.
*/
typedef enum {
/** Operation completed successfully */
GHOSTTY_SUCCESS = 0,
/** Operation failed due to failed allocation */
GHOSTTY_OUT_OF_MEMORY = -1,
/** Operation failed due to invalid value */
GHOSTTY_INVALID_VALUE = -2,
/** Operation failed because the provided buffer was too small */
GHOSTTY_OUT_OF_SPACE = -3,
} GhosttyResult;
/**
* Initialize a sized struct to zero and set its size field.
*
* Sized structs use a `size` field as the first member for ABI
* compatibility. This macro zero-initializes the struct and sets the
* size field to `sizeof(type)`, which allows the library to detect
* which version of the struct the caller was compiled against.
*
* @param type The struct type to initialize
* @return A zero-initialized struct with the size field set
*
* Example:
* @code
* GhosttyFormatterTerminalOptions opts = GHOSTTY_INIT_SIZED(GhosttyFormatterTerminalOptions);
* opts.emit = GHOSTTY_FORMATTER_FORMAT_PLAIN;
* opts.trim = true;
* @endcode
*/
#define GHOSTTY_INIT_SIZED(type) \
((type){ .size = sizeof(type) })
#endif /* GHOSTTY_VT_TYPES_H */

View File

@@ -1,6 +1,17 @@
const std = @import("std");
const testing = std.testing;
const Target = @import("target.zig").Target;
/// Create a struct type that is C ABI compatible from a Zig struct type.
///
/// When the target is `.zig`, the original struct type is returned as-is.
/// When the target is `.c`, the struct is recreated with an `extern` layout,
/// ensuring a stable, C-compatible memory layout.
///
/// This handles packed structs by resolving zero alignments to the natural
/// alignment of each field's type, since extern structs require explicit
/// alignment. This means packed struct fields like `bool` will take up
/// their full size (1 byte) rather than being bit-packed.
pub fn Struct(
comptime target: Target,
comptime Zig: type,
@@ -16,7 +27,7 @@ pub fn Struct(
.type = field.type,
.default_value_ptr = field.default_value_ptr,
.is_comptime = field.is_comptime,
.alignment = field.alignment,
.alignment = if (field.alignment > 0) field.alignment else @alignOf(field.type),
};
}
@@ -29,3 +40,20 @@ pub fn Struct(
},
};
}
test "packed struct converts to extern with full-size bools" {
const Packed = packed struct {
flag1: bool,
flag2: bool,
value: u8,
};
const C = Struct(.c, Packed);
const info = @typeInfo(C).@"struct";
try testing.expectEqual(.@"extern", info.layout);
try testing.expectEqual(@as(usize, 1), @sizeOf(@FieldType(C, "flag1")));
try testing.expectEqual(@as(usize, 1), @sizeOf(@FieldType(C, "flag2")));
try testing.expectEqual(@as(usize, 1), @sizeOf(@FieldType(C, "value")));
try testing.expectEqual(@as(usize, 3), @sizeOf(C));
}

View File

@@ -143,6 +143,16 @@ comptime {
@export(&c.sgr_unknown_partial, .{ .name = "ghostty_sgr_unknown_partial" });
@export(&c.sgr_attribute_tag, .{ .name = "ghostty_sgr_attribute_tag" });
@export(&c.sgr_attribute_value, .{ .name = "ghostty_sgr_attribute_value" });
@export(&c.formatter_terminal_new, .{ .name = "ghostty_formatter_terminal_new" });
@export(&c.formatter_format_buf, .{ .name = "ghostty_formatter_format_buf" });
@export(&c.formatter_format_alloc, .{ .name = "ghostty_formatter_format_alloc" });
@export(&c.formatter_free, .{ .name = "ghostty_formatter_free" });
@export(&c.terminal_new, .{ .name = "ghostty_terminal_new" });
@export(&c.terminal_free, .{ .name = "ghostty_terminal_free" });
@export(&c.terminal_reset, .{ .name = "ghostty_terminal_reset" });
@export(&c.terminal_resize, .{ .name = "ghostty_terminal_resize" });
@export(&c.terminal_vt_write, .{ .name = "ghostty_terminal_vt_write" });
@export(&c.terminal_scroll_viewport, .{ .name = "ghostty_terminal_scroll_viewport" });
// On Wasm we need to export our allocator convenience functions.
if (builtin.target.cpu.arch.isWasm()) {

View File

@@ -5,6 +5,7 @@ const Terminal = @This();
const std = @import("std");
const build_options = @import("terminal_options");
const lib = @import("../lib/main.zig");
const assert = @import("../quirks.zig").inlineAssert;
const testing = std.testing;
const Allocator = std.mem.Allocator;
@@ -35,6 +36,8 @@ const Page = pagepkg.Page;
const Cell = pagepkg.Cell;
const Row = pagepkg.Row;
const lib_target: lib.Target = if (build_options.c_abi) .c else .zig;
const log = std.log.scoped(.terminal);
/// Default tabstop interval
@@ -271,7 +274,7 @@ pub fn vtHandler(self: *Terminal) ReadonlyHandler {
}
/// The general allocator we should use for this terminal.
fn gpa(self: *Terminal) Allocator {
pub fn gpa(self: *Terminal) Allocator {
return self.screens.active.alloc;
}
@@ -1704,7 +1707,7 @@ pub fn scrollUp(self: *Terminal, count: usize) !void {
}
/// Options for scrolling the viewport of the terminal grid.
pub const ScrollViewport = union(enum) {
pub const ScrollViewport = union(Tag) {
/// Scroll to the top of the scrollback
top,
@@ -1713,6 +1716,23 @@ pub const ScrollViewport = union(enum) {
/// Scroll by some delta amount, up is negative.
delta: isize,
pub const Tag = lib.Enum(lib_target, &.{
"top",
"bottom",
"delta",
});
const c_union = lib.TaggedUnion(
lib_target,
@This(),
// Padding: largest variant is isize (8 bytes on 64-bit).
// Use [2]u64 (16 bytes) for future expansion.
[2]u64,
);
pub const C = c_union.C;
pub const CValue = c_union.CValue;
pub const cval = c_union.cval;
};
/// Scroll the viewport of the terminal grid.

22
src/terminal/c/AGENTS.md Normal file
View File

@@ -0,0 +1,22 @@
# libghostty-vt C API
- C API must be designed with ABI compatibility in mind
- Zig tagged unions must be converted to C ABI compatible unions
via `lib.TaggedUnion`.
- Any functions must be updated all the way through from here to
`src/terminal/c/main.zig` to `src/lib_vt.zig` and the headers
in `include/ghostty/vt.h`.
- In `include/ghostty/vt.h`, always sort the header contents by:
(1) macros, (2) forward declarations, (3) types, (4) functions
## ABI Compatibility
- Prefer opaque pointers for long-lived objects, such as
`GhosttyTerminal`.
- Structs:
- May contain padding bytes if we're confident we'll never grow
beyond a certain size.
- May use the "sized struct" pattern: an `extern struct` with
`size: usize = @sizeOf(Self)` as the first field. In the C header,
callers use `GHOSTTY_INIT_SIZED` from `types.h` to zero-initialize and
set the size.

View File

@@ -0,0 +1,417 @@
const std = @import("std");
const testing = std.testing;
const lib_alloc = @import("../../lib/allocator.zig");
const CAllocator = lib_alloc.Allocator;
const terminal_c = @import("terminal.zig");
const ZigTerminal = @import("../Terminal.zig");
const formatterpkg = @import("../formatter.zig");
const Result = @import("result.zig").Result;
/// Wrapper around formatter that tracks the allocator for C API usage.
const FormatterWrapper = struct {
kind: Kind,
alloc: std.mem.Allocator,
const Kind = union(enum) {
terminal: formatterpkg.TerminalFormatter,
};
};
/// C: GhosttyFormatter
pub const Formatter = ?*FormatterWrapper;
/// C: GhosttyFormatterFormat
pub const Format = formatterpkg.Format;
/// C: GhosttyFormatterScreenOptions
pub const ScreenOptions = extern struct {
/// C: GhosttyFormatterScreenExtra
pub const Extra = extern struct {
size: usize = @sizeOf(Extra),
cursor: bool,
style: bool,
hyperlink: bool,
protection: bool,
kitty_keyboard: bool,
charsets: bool,
comptime {
for (std.meta.fieldNames(formatterpkg.ScreenFormatter.Extra)) |name| {
if (!@hasField(Extra, name))
@compileError("ScreenOptions.Extra missing field: " ++ name);
}
}
fn toZig(self: Extra) formatterpkg.ScreenFormatter.Extra {
return .{
.cursor = self.cursor,
.style = self.style,
.hyperlink = self.hyperlink,
.protection = self.protection,
.kitty_keyboard = self.kitty_keyboard,
.charsets = self.charsets,
};
}
};
};
/// C: GhosttyFormatterTerminalOptions
pub const TerminalOptions = extern struct {
size: usize = @sizeOf(TerminalOptions),
emit: Format,
unwrap: bool,
trim: bool,
extra: Extra,
/// C: GhosttyFormatterTerminalExtra
pub const Extra = extern struct {
size: usize = @sizeOf(Extra),
palette: bool,
modes: bool,
scrolling_region: bool,
tabstops: bool,
pwd: bool,
keyboard: bool,
screen: ScreenOptions.Extra,
comptime {
for (std.meta.fieldNames(formatterpkg.TerminalFormatter.Extra)) |name| {
if (!@hasField(Extra, name))
@compileError("TerminalOptions.Extra missing field: " ++ name);
}
}
fn toZig(self: Extra) formatterpkg.TerminalFormatter.Extra {
return .{
.palette = self.palette,
.modes = self.modes,
.scrolling_region = self.scrolling_region,
.tabstops = self.tabstops,
.pwd = self.pwd,
.keyboard = self.keyboard,
.screen = self.screen.toZig(),
};
}
};
};
pub fn terminal_new(
alloc_: ?*const CAllocator,
result: *Formatter,
terminal_: terminal_c.Terminal,
opts: TerminalOptions,
) callconv(.c) Result {
result.* = terminal_new_(
alloc_,
terminal_,
opts,
) catch |err| {
result.* = null;
return switch (err) {
error.InvalidValue => .invalid_value,
error.OutOfMemory => .out_of_memory,
};
};
return .success;
}
fn terminal_new_(
alloc_: ?*const CAllocator,
terminal_: terminal_c.Terminal,
opts: TerminalOptions,
) error{
InvalidValue,
OutOfMemory,
}!*FormatterWrapper {
const t = terminal_ orelse return error.InvalidValue;
const alloc = lib_alloc.default(alloc_);
const ptr = alloc.create(FormatterWrapper) catch
return error.OutOfMemory;
errdefer alloc.destroy(ptr);
var formatter: formatterpkg.TerminalFormatter = .init(t, .{
.emit = opts.emit,
.unwrap = opts.unwrap,
.trim = opts.trim,
});
formatter.extra = opts.extra.toZig();
ptr.* = .{
.kind = .{ .terminal = formatter },
.alloc = alloc,
};
return ptr;
}
pub fn format_buf(
formatter_: Formatter,
out_: ?[*]u8,
out_len: usize,
out_written: *usize,
) callconv(.c) Result {
const wrapper = formatter_ orelse return .invalid_value;
var writer: std.Io.Writer = .fixed(if (out_) |out|
out[0..out_len]
else
&.{});
switch (wrapper.kind) {
.terminal => |*t| t.format(&writer) catch |err| switch (err) {
error.WriteFailed => {
// On write failed we always report how much
// space we actually needed.
var discarding: std.Io.Writer.Discarding = .init(&.{});
t.format(&discarding.writer) catch unreachable;
out_written.* = @intCast(discarding.count);
return .out_of_space;
},
},
}
out_written.* = writer.end;
return .success;
}
pub fn format_alloc(
formatter_: Formatter,
alloc_: ?*const CAllocator,
out_ptr: *?[*]u8,
out_len: *usize,
) callconv(.c) Result {
const wrapper = formatter_ orelse return .invalid_value;
const alloc = lib_alloc.default(alloc_);
var aw: std.Io.Writer.Allocating = .init(alloc);
defer aw.deinit();
switch (wrapper.kind) {
.terminal => |*t| t.format(&aw.writer) catch return .out_of_memory,
}
const buf = aw.toOwnedSlice() catch return .out_of_memory;
out_ptr.* = buf.ptr;
out_len.* = buf.len;
return .success;
}
pub fn free(formatter_: Formatter) callconv(.c) void {
const wrapper = formatter_ orelse return;
const alloc = wrapper.alloc;
alloc.destroy(wrapper);
}
test "terminal_new/free" {
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer terminal_c.free(t);
var f: Formatter = null;
try testing.expectEqual(Result.success, terminal_new(
&lib_alloc.test_allocator,
&f,
t,
.{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
));
try testing.expect(f != null);
free(f);
}
test "terminal_new invalid_value on null terminal" {
var f: Formatter = null;
try testing.expectEqual(Result.invalid_value, terminal_new(
&lib_alloc.test_allocator,
&f,
null,
.{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
));
try testing.expect(f == null);
}
test "free null" {
free(null);
}
test "format plain" {
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer terminal_c.free(t);
terminal_c.vt_write(t, "Hello", 5);
var f: Formatter = null;
try testing.expectEqual(Result.success, terminal_new(
&lib_alloc.test_allocator,
&f,
t,
.{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
));
defer free(f);
var buf: [1024]u8 = undefined;
var written: usize = 0;
try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
try testing.expectEqualStrings("Hello", buf[0..written]);
}
test "format reflects terminal changes" {
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer terminal_c.free(t);
terminal_c.vt_write(t, "Hello", 5);
var f: Formatter = null;
try testing.expectEqual(Result.success, terminal_new(
&lib_alloc.test_allocator,
&f,
t,
.{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
));
defer free(f);
var buf: [1024]u8 = undefined;
var written: usize = 0;
try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
try testing.expectEqualStrings("Hello", buf[0..written]);
// Write more data and re-format
terminal_c.vt_write(t, "\r\nWorld", 7);
try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
try testing.expectEqualStrings("Hello\nWorld", buf[0..written]);
}
test "format null returns required size" {
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer terminal_c.free(t);
terminal_c.vt_write(t, "Hello", 5);
var f: Formatter = null;
try testing.expectEqual(Result.success, terminal_new(
&lib_alloc.test_allocator,
&f,
t,
.{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
));
defer free(f);
// Pass null buffer to query required size
var required: usize = 0;
try testing.expectEqual(Result.out_of_space, format_buf(f, null, 0, &required));
try testing.expect(required > 0);
// Now allocate and format
var buf: [1024]u8 = undefined;
var written: usize = 0;
try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
try testing.expectEqual(required, written);
}
test "format buffer too small" {
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer terminal_c.free(t);
terminal_c.vt_write(t, "Hello", 5);
var f: Formatter = null;
try testing.expectEqual(Result.success, terminal_new(
&lib_alloc.test_allocator,
&f,
t,
.{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
));
defer free(f);
// Buffer too small
var buf: [2]u8 = undefined;
var written: usize = 0;
try testing.expectEqual(Result.out_of_space, format_buf(f, &buf, buf.len, &written));
// written contains the required size
try testing.expectEqual(@as(usize, 5), written);
}
test "format null formatter" {
var written: usize = 0;
try testing.expectEqual(Result.invalid_value, format_buf(null, null, 0, &written));
}
test "format vt" {
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer terminal_c.free(t);
terminal_c.vt_write(t, "Test", 4);
var f: Formatter = null;
try testing.expectEqual(Result.success, terminal_new(
&lib_alloc.test_allocator,
&f,
t,
.{ .emit = .vt, .unwrap = false, .trim = true, .extra = .{ .palette = true, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = true, .hyperlink = true, .protection = false, .kitty_keyboard = false, .charsets = false } } },
));
defer free(f);
var buf: [65536]u8 = undefined;
var written: usize = 0;
try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
try testing.expect(written > 0);
try testing.expect(std.mem.indexOf(u8, buf[0..written], "Test") != null);
}
test "format html" {
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer terminal_c.free(t);
terminal_c.vt_write(t, "Html", 4);
var f: Formatter = null;
try testing.expectEqual(Result.success, terminal_new(
&lib_alloc.test_allocator,
&f,
t,
.{ .emit = .html, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
));
defer free(f);
var buf: [65536]u8 = undefined;
var written: usize = 0;
try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
try testing.expect(written > 0);
try testing.expect(std.mem.indexOf(u8, buf[0..written], "Html") != null);
}

View File

@@ -1,9 +1,11 @@
pub const color = @import("color.zig");
pub const formatter = @import("formatter.zig");
pub const osc = @import("osc.zig");
pub const key_event = @import("key_event.zig");
pub const key_encode = @import("key_encode.zig");
pub const paste = @import("paste.zig");
pub const sgr = @import("sgr.zig");
pub const terminal = @import("terminal.zig");
// The full C API, unexported.
pub const osc_new = osc.new;
@@ -16,6 +18,11 @@ pub const osc_command_data = osc.commandData;
pub const color_rgb_get = color.rgb_get;
pub const formatter_terminal_new = formatter.terminal_new;
pub const formatter_format_buf = formatter.format_buf;
pub const formatter_format_alloc = formatter.format_alloc;
pub const formatter_free = formatter.free;
pub const sgr_new = sgr.new;
pub const sgr_free = sgr.free;
pub const sgr_reset = sgr.reset;
@@ -52,13 +59,22 @@ pub const key_encoder_encode = key_encode.encode;
pub const paste_is_safe = paste.is_safe;
pub const terminal_new = terminal.new;
pub const terminal_free = terminal.free;
pub const terminal_reset = terminal.reset;
pub const terminal_resize = terminal.resize;
pub const terminal_vt_write = terminal.vt_write;
pub const terminal_scroll_viewport = terminal.scroll_viewport;
test {
_ = color;
_ = formatter;
_ = osc;
_ = key_event;
_ = key_encode;
_ = paste;
_ = sgr;
_ = terminal;
// We want to make sure we run the tests for the C allocator interface.
_ = @import("../../lib/allocator.zig");

View File

@@ -3,4 +3,5 @@ pub const Result = enum(c_int) {
success = 0,
out_of_memory = -1,
invalid_value = -2,
out_of_space = -3,
};

293
src/terminal/c/terminal.zig Normal file
View File

@@ -0,0 +1,293 @@
const std = @import("std");
const testing = std.testing;
const lib_alloc = @import("../../lib/allocator.zig");
const CAllocator = lib_alloc.Allocator;
const ZigTerminal = @import("../Terminal.zig");
const size = @import("../size.zig");
const Result = @import("result.zig").Result;
/// C: GhosttyTerminal
pub const Terminal = ?*ZigTerminal;
/// C: GhosttyTerminalOptions
pub const Options = extern struct {
cols: size.CellCountInt,
rows: size.CellCountInt,
max_scrollback: usize,
};
const NewError = error{
InvalidValue,
OutOfMemory,
};
pub fn new(
alloc_: ?*const CAllocator,
result: *Terminal,
opts: Options,
) callconv(.c) Result {
result.* = new_(alloc_, opts) catch |err| {
result.* = null;
return switch (err) {
error.InvalidValue => .invalid_value,
error.OutOfMemory => .out_of_memory,
};
};
return .success;
}
fn new_(
alloc_: ?*const CAllocator,
opts: Options,
) NewError!*ZigTerminal {
if (opts.cols == 0 or opts.rows == 0) return error.InvalidValue;
const alloc = lib_alloc.default(alloc_);
const ptr = alloc.create(ZigTerminal) catch
return error.OutOfMemory;
errdefer alloc.destroy(ptr);
ptr.* = try .init(alloc, .{
.cols = opts.cols,
.rows = opts.rows,
.max_scrollback = opts.max_scrollback,
});
return ptr;
}
pub fn vt_write(
terminal_: Terminal,
ptr: [*]const u8,
len: usize,
) callconv(.c) void {
const t = terminal_ orelse return;
var stream = t.vtStream();
stream.nextSlice(ptr[0..len]);
}
/// C: GhosttyTerminalScrollViewport
pub const ScrollViewport = ZigTerminal.ScrollViewport.C;
pub fn scroll_viewport(
terminal_: Terminal,
behavior: ScrollViewport,
) callconv(.c) void {
const t = terminal_ orelse return;
t.scrollViewport(switch (behavior.tag) {
.top => .top,
.bottom => .bottom,
.delta => .{ .delta = behavior.value.delta },
});
}
pub fn resize(
terminal_: Terminal,
cols: size.CellCountInt,
rows: size.CellCountInt,
) callconv(.c) Result {
const t = terminal_ orelse return .invalid_value;
if (cols == 0 or rows == 0) return .invalid_value;
t.resize(t.gpa(), cols, rows) catch return .out_of_memory;
return .success;
}
pub fn reset(terminal_: Terminal) callconv(.c) void {
const t = terminal_ orelse return;
t.fullReset();
}
pub fn free(terminal_: Terminal) callconv(.c) void {
const t = terminal_ orelse return;
const alloc = t.gpa();
t.deinit(alloc);
alloc.destroy(t);
}
test "new/free" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 10_000,
},
));
try testing.expect(t != null);
free(t);
}
test "new invalid value" {
var t: Terminal = null;
try testing.expectEqual(Result.invalid_value, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 0,
.rows = 24,
.max_scrollback = 10_000,
},
));
try testing.expect(t == null);
try testing.expectEqual(Result.invalid_value, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 0,
.max_scrollback = 10_000,
},
));
try testing.expect(t == null);
}
test "free null" {
free(null);
}
test "scroll_viewport" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 5,
.rows = 2,
.max_scrollback = 10_000,
},
));
defer free(t);
const zt = t.?;
// Write "hello" on the first line
vt_write(t, "hello", 5);
// Push "hello" into scrollback with 3 newlines (index = ESC D)
vt_write(t, "\x1bD\x1bD\x1bD", 6);
{
// Viewport should be empty now since hello scrolled off
const str = try zt.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("", str);
}
// Scroll to top: "hello" should be visible again
scroll_viewport(t, .{ .tag = .top, .value = undefined });
{
const str = try zt.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("hello", str);
}
// Scroll to bottom: viewport should be empty again
scroll_viewport(t, .{ .tag = .bottom, .value = undefined });
{
const str = try zt.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("", str);
}
// Scroll up by delta to bring "hello" back into view
scroll_viewport(t, .{ .tag = .delta, .value = .{ .delta = -3 } });
{
const str = try zt.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("hello", str);
}
}
test "scroll_viewport null" {
scroll_viewport(null, .{ .tag = .top, .value = undefined });
}
test "reset" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 10_000,
},
));
defer free(t);
vt_write(t, "Hello", 5);
reset(t);
const str = try t.?.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("", str);
}
test "reset null" {
reset(null);
}
test "resize" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 10_000,
},
));
defer free(t);
try testing.expectEqual(Result.success, resize(t, 40, 12));
try testing.expectEqual(40, t.?.cols);
try testing.expectEqual(12, t.?.rows);
}
test "resize null" {
try testing.expectEqual(Result.invalid_value, resize(null, 80, 24));
}
test "resize invalid value" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 10_000,
},
));
defer free(t);
try testing.expectEqual(Result.invalid_value, resize(t, 0, 24));
try testing.expectEqual(Result.invalid_value, resize(t, 80, 0));
}
test "vt_write" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 10_000,
},
));
defer free(t);
vt_write(t, "Hello", 5);
const str = try t.?.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("Hello", str);
}

View File

@@ -1,5 +1,8 @@
const std = @import("std");
const build_options = @import("terminal_options");
const assert = @import("../quirks.zig").inlineAssert;
const lib = @import("../lib/main.zig");
const lib_target: lib.Target = if (build_options.c_abi) .c else .zig;
const Allocator = std.mem.Allocator;
const color = @import("color.zig");
const size = @import("size.zig");
@@ -19,46 +22,47 @@ const Selection = @import("Selection.zig");
const Style = @import("style.zig").Style;
/// Formats available.
pub const Format = enum {
/// Plain text.
plain,
pub const Format = lib.Enum(lib_target, &.{
// Plain text.
"plain",
/// Include VT sequences to preserve colors, styles, URLs, etc.
/// This is predominantly SGR sequences but may contain others as needed.
///
/// Note that for reference colors, like palette indices, this will
/// vary based on the formatter and you should see the docs. For example,
/// PageFormatter with VT will emit SGR sequences with palette indices,
/// not the color itself.
///
/// For VT, newlines will be emitted as `\r\n` so that the cursor properly
/// moves back to the beginning prior emitting follow-up lines.
vt,
// Include VT sequences to preserve colors, styles, URLs, etc.
// This is predominantly SGR sequences but may contain others as needed.
//
// Note that for reference colors, like palette indices, this will
// vary based on the formatter and you should see the docs. For example,
// PageFormatter with VT will emit SGR sequences with palette indices,
// not the color itself.
//
// For VT, newlines will be emitted as `\r\n` so that the cursor properly
// moves back to the beginning prior emitting follow-up lines.
"vt",
/// HTML output.
///
/// This will emit inline styles for as much styling as possible,
/// in the interest of simplicity and ease of editing. This isn't meant
/// to build the most beautiful or efficient HTML, but rather to be
/// stylistically correct.
///
/// For colors, RGB values are emitted as inline CSS (#RRGGBB) while palette
/// indices use CSS variables (var(--vt-palette-N)). The palette colors are
/// emitted by TerminalFormatter.Extra.palette as a <style> block if you
/// want to also include that. But if you only format a screen or lower,
/// the formatter doesn't have access to the current palette to render it.
///
/// Newlines are emitted as actual '\n' characters. Consumers should use
/// CSS white-space: pre or pre-wrap to preserve spacing and alignment.
html,
// HTML output.
//
// This will emit inline styles for as much styling as possible,
// in the interest of simplicity and ease of editing. This isn't meant
// to build the most beautiful or efficient HTML, but rather to be
// stylistically correct.
//
// For colors, RGB values are emitted as inline CSS (#RRGGBB) while palette
// indices use CSS variables (var(--vt-palette-N)). The palette colors are
// emitted by TerminalFormatter.Extra.palette as a <style> block if you
// want to also include that. But if you only format a screen or lower,
// the formatter doesn't have access to the current palette to render it.
//
// Newlines are emitted as actual '\n' characters. Consumers should use
// CSS white-space: pre or pre-wrap to preserve spacing and alignment.
"html",
});
pub fn styled(self: Format) bool {
return switch (self) {
.plain => false,
.html, .vt => true,
};
}
};
/// Returns true if the format emits styled output (not plaintext).
pub fn formatStyled(fmt: Format) bool {
return switch (fmt) {
.plain => false,
.html, .vt => true,
};
}
pub const CodepointMap = struct {
/// Unicode codepoint range to replace.
@@ -289,7 +293,7 @@ pub const TerminalFormatter = struct {
m.map.appendNTimes(
m.alloc,
self.terminal.screens.active.pages.getTopLeft(.screen),
discarding.count,
std.math.cast(usize, discarding.count) orelse return error.WriteFailed,
) catch return error.WriteFailed;
}
}
@@ -327,7 +331,7 @@ pub const TerminalFormatter = struct {
m.map.appendNTimes(
m.alloc,
self.terminal.screens.active.pages.getTopLeft(.screen),
discarding.count,
std.math.cast(usize, discarding.count) orelse return error.WriteFailed,
) catch return error.WriteFailed;
}
}
@@ -411,7 +415,7 @@ pub const TerminalFormatter = struct {
.y = last.y,
};
} else self.terminal.screens.active.pages.getTopLeft(.screen),
discarding.count,
std.math.cast(usize, discarding.count) orelse return error.WriteFailed,
) catch return error.WriteFailed;
}
}
@@ -684,7 +688,7 @@ pub const ScreenFormatter = struct {
.y = last.y,
};
} else self.screen.pages.getTopLeft(.screen),
discarding.count,
std.math.cast(usize, discarding.count) orelse return error.WriteFailed,
) catch return error.WriteFailed;
}
}
@@ -1130,7 +1134,7 @@ pub const PageFormatter = struct {
// If we're emitting styled output (not plaintext) and
// the cell has some kind of styling or is not empty
// then this isn't blank.
if (self.opts.emit.styled() and
if (formatStyled(self.opts.emit) and
(!cell.isEmpty() or cell.hasStyling())) break :blank;
// Cells with no text are blank
@@ -1186,7 +1190,7 @@ pub const PageFormatter = struct {
style: {
// If we aren't emitting styled output then we don't
// have to worry about styles.
if (!self.opts.emit.styled()) break :style;
if (!formatStyled(self.opts.emit)) break :style;
// Get our cell style.
const cell_style = self.cellStyle(cell);
@@ -1230,7 +1234,10 @@ pub const PageFormatter = struct {
&discarding.writer,
&style,
);
for (0..discarding.count) |_| map.map.append(map.alloc, .{
for (0..std.math.cast(
usize,
discarding.count,
) orelse return error.WriteFailed) |_| map.map.append(map.alloc, .{
.x = x,
.y = y,
}) catch return error.WriteFailed;
@@ -1287,7 +1294,10 @@ pub const PageFormatter = struct {
&discarding.writer,
uri,
);
for (0..discarding.count) |_| map.map.append(map.alloc, .{
for (0..std.math.cast(
usize,
discarding.count,
) orelse return error.WriteFailed) |_| map.map.append(map.alloc, .{
.x = x,
.y = y,
}) catch return error.WriteFailed;
@@ -1305,7 +1315,10 @@ pub const PageFormatter = struct {
if (self.point_map) |*map| {
var discarding: std.Io.Writer.Discarding = .init(&.{});
try self.writeCell(tag, &discarding.writer, cell);
for (0..discarding.count) |_| map.map.append(map.alloc, .{
for (0..std.math.cast(
usize,
discarding.count,
) orelse return error.WriteFailed) |_| map.map.append(map.alloc, .{
.x = x,
.y = y,
}) catch return error.WriteFailed;

View File

@@ -16,6 +16,8 @@ const max_context_id_len = 64;
/// A single OSC 3008 context signal command.
pub const Command = struct {
pub const C = void;
action: Action,
/// The context identifier. Must be 1-64 characters in the 32..126 byte range.
id: []const u8,

View File

@@ -14,6 +14,8 @@ const log = std.log.scoped(.osc_semantic_prompt);
/// all except one do and the spec does also say to ignore unknown
/// options. So, I think this is a fair interpretation.
pub const Command = struct {
pub const C = void;
action: Action,
options_unvalidated: []const u8,