mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-05-29 16:25:20 +00:00
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:
@@ -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
|
||||
}
|
||||
62
.agents/skills/writing-commit-messages/SKILL.md
Normal file
62
.agents/skills/writing-commit-messages/SKILL.md
Normal 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.
|
||||
18
example/c-vt-formatter/README.md
Normal file
18
example/c-vt-formatter/README.md
Normal 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
|
||||
```
|
||||
42
example/c-vt-formatter/build.zig
Normal file
42
example/c-vt-formatter/build.zig
Normal 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);
|
||||
}
|
||||
24
example/c-vt-formatter/build.zig.zon
Normal file
24
example/c-vt-formatter/build.zig.zon
Normal 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",
|
||||
},
|
||||
}
|
||||
63
example/c-vt-formatter/src/main.c
Normal file
63
example/c-vt-formatter/src/main.c
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
226
include/ghostty/vt/formatter.h
Normal file
226
include/ghostty/vt/formatter.h
Normal 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 */
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 */
|
||||
@@ -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>
|
||||
|
||||
200
include/ghostty/vt/terminal.h
Normal file
200
include/ghostty/vt/terminal.h
Normal 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 */
|
||||
45
include/ghostty/vt/types.h
Normal file
45
include/ghostty/vt/types.h
Normal 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 */
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
22
src/terminal/c/AGENTS.md
Normal 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.
|
||||
417
src/terminal/c/formatter.zig
Normal file
417
src/terminal/c/formatter.zig
Normal 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);
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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
293
src/terminal/c/terminal.zig
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user