mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-09-16 10:51:59 +00:00
Merge branch 'ghostty-org:main' into main
This commit is contained in:
4
.github/VOUCHED.td
vendored
4
.github/VOUCHED.td
vendored
@@ -245,6 +245,7 @@ nolinmcfarland
|
||||
nouritsu
|
||||
nwehg
|
||||
ocean6954
|
||||
ollioddi
|
||||
oshdubh
|
||||
otomn
|
||||
paaloeye
|
||||
@@ -277,6 +278,7 @@ rgehan
|
||||
rhodes-b
|
||||
rightaditya
|
||||
rjwittams
|
||||
rkoten
|
||||
rmengelbrecht
|
||||
rmunn
|
||||
rockorager
|
||||
@@ -313,6 +315,7 @@ tothedarktowercame
|
||||
trag1c
|
||||
tristan957
|
||||
tsacha
|
||||
tuananh
|
||||
turbolent
|
||||
tweedbeetle
|
||||
uhojin
|
||||
@@ -320,6 +323,7 @@ unphased
|
||||
unsaltedscholar
|
||||
uzaaft
|
||||
vancluever
|
||||
vasilmytsyk
|
||||
vaughanandrews
|
||||
-vectorpeak Stupid bot cosplaying as a maintainer
|
||||
vegerot
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
# Example: `ghostty-vt` Paste Utilities
|
||||
# Example: `ghostty-vt` Paste
|
||||
|
||||
This contains a simple example of how to use the `ghostty-vt` paste
|
||||
utilities to check if paste data is safe and encode it for terminal input.
|
||||
This contains a simple example of how to paste into a `ghostty-vt`
|
||||
terminal with `ghostty_terminal_paste`: plain and bracketed (mode 2004)
|
||||
text pastes, the unsafe-paste confirmation flow, and Kitty clipboard
|
||||
protocol paste events (mode 5522) including the program's follow-up
|
||||
clipboard read. The clipboard's data is produced on demand through a
|
||||
read callback, so only what is actually pasted is ever read, and the
|
||||
result streams to the pty in chunks. It also shows the terminal-free
|
||||
building blocks for checking paste safety and encoding paste data.
|
||||
|
||||
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
|
||||
|
||||
@@ -1,7 +1,160 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <ghostty/vt.h>
|
||||
|
||||
#define GS(s) ((GhosttyString){.ptr = (const uint8_t*)(s), .len = sizeof(s) - 1})
|
||||
|
||||
// Print bytes destined for the pty with control characters made visible.
|
||||
static void print_escaped(const uint8_t* data, size_t len) {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
switch (data[i]) {
|
||||
case 0x1b: printf("ESC"); break;
|
||||
case '\r': printf("\\r"); break;
|
||||
case '\n': printf("\\n"); break;
|
||||
default: putchar(data[i]); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The base64 password of the last paste event, captured from the OK
|
||||
// packet so the example can play the program's side of the protocol.
|
||||
static char event_pw[128];
|
||||
|
||||
// Everything the terminal writes to the running program: the pasted
|
||||
// text, or the paste event packets when mode 5522 is enabled.
|
||||
static void on_write_pty(GhosttyTerminal terminal,
|
||||
void* userdata,
|
||||
const uint8_t* data,
|
||||
size_t len) {
|
||||
(void)terminal;
|
||||
(void)userdata;
|
||||
printf(" -> pty (%zu bytes): ", len);
|
||||
print_escaped(data, len);
|
||||
printf("\n");
|
||||
|
||||
// A paste event's OK packet: OSC 5522 ; type=read:status=OK:pw=<b64> ST
|
||||
const char* prefix = "\x1b]5522;type=read:status=OK:pw=";
|
||||
size_t prefix_len = strlen(prefix);
|
||||
if (len > prefix_len && memcmp(data, prefix, prefix_len) == 0) {
|
||||
size_t end = prefix_len;
|
||||
while (end < len && data[end] != 0x1b) end++;
|
||||
size_t pw_len = end - prefix_len;
|
||||
if (pw_len < sizeof(event_pw)) {
|
||||
memcpy(event_pw, data + prefix_len, pw_len);
|
||||
event_pw[pw_len] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Serves clipboard reads. After a paste event the program's read arrives
|
||||
// with `granted` set, because it carries the event's one-time password,
|
||||
// so the embedder skips its permission prompt.
|
||||
static void on_clipboard_read(GhosttyTerminal terminal,
|
||||
void* userdata,
|
||||
const GhosttyClipboardRead* read) {
|
||||
(void)terminal;
|
||||
(void)userdata;
|
||||
printf(" clipboard read: name=\"");
|
||||
fwrite(read->name.ptr, 1, read->name.len, stdout);
|
||||
printf("\" granted=%s\n", read->granted ? "yes (no prompt needed)" : "no");
|
||||
|
||||
const char* text = "hello from the clipboard";
|
||||
GhosttyClipboardContent content = {
|
||||
.mime = GS("text/plain"),
|
||||
.data = {.ptr = (const uint8_t*)text, .len = strlen(text)},
|
||||
};
|
||||
GhosttyClipboardReadReply reply = {
|
||||
.size = sizeof(reply),
|
||||
.result = GHOSTTY_CLIPBOARD_READ_RESULT_SUCCESS,
|
||||
.contents = &content,
|
||||
.contents_len = 1,
|
||||
.available = NULL,
|
||||
.available_len = 0,
|
||||
.remember = false,
|
||||
};
|
||||
read->reply(read, &reply);
|
||||
}
|
||||
|
||||
// A real embedder would show a dialog here.
|
||||
static bool confirm_with_user(void) {
|
||||
printf(" paste could inject commands; user confirmed\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
//! [terminal-paste]
|
||||
// What the clipboard holds. A real embedder would keep a handle to the
|
||||
// pasteboard or its items here; the data is only produced on demand.
|
||||
typedef struct {
|
||||
const char* text;
|
||||
} clipboard_t;
|
||||
|
||||
// Produces the data of one representation when the terminal needs it.
|
||||
// Only the text is ever read: the image is listed on a paste event
|
||||
// but never requested, so a large image costs nothing to paste.
|
||||
// Nothing written to the writer is retained, so the data can be
|
||||
// streamed from anywhere in pieces of any size.
|
||||
static bool read_clipboard(void* userdata, GhosttyString mime, GhosttyWriter writer) {
|
||||
clipboard_t* clipboard = userdata;
|
||||
if (mime.len == strlen("text/plain") &&
|
||||
memcmp(mime.ptr, "text/plain", mime.len) == 0) {
|
||||
// Stream the text in small pieces just to show that it works.
|
||||
const uint8_t* data = (const uint8_t*)clipboard->text;
|
||||
size_t len = strlen(clipboard->text);
|
||||
for (size_t offset = 0; offset < len; offset += 4) {
|
||||
size_t n = len - offset < 4 ? len - offset : 4;
|
||||
if (!writer.write(writer.userdata, data + offset, n)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
printf(" image read requested, which never happens\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Paste whatever the clipboard holds. The terminal applies its own
|
||||
// state: bracketed paste framing (mode 2004) or a Kitty paste event
|
||||
// (mode 5522) instead of the text.
|
||||
static void paste_clipboard(GhosttyTerminal terminal, const char* text) {
|
||||
clipboard_t clipboard = {.text = text};
|
||||
GhosttyString mimes[] = {
|
||||
// The first text representation is what a text paste writes.
|
||||
GS("text/plain"),
|
||||
// Listed on a paste event, never read.
|
||||
GS("image/png"),
|
||||
};
|
||||
GhosttyPaste paste = {
|
||||
.size = sizeof(paste),
|
||||
.location = GHOSTTY_CLIPBOARD_LOCATION_STANDARD,
|
||||
.source = GHOSTTY_PASTE_SOURCE_CLIPBOARD,
|
||||
.mimes = mimes,
|
||||
.mimes_len = sizeof(mimes) / sizeof(mimes[0]),
|
||||
.reader = {.read = read_clipboard, .userdata = &clipboard},
|
||||
.allow_unsafe = false,
|
||||
};
|
||||
|
||||
bool written = false;
|
||||
GhosttyResult result = ghostty_terminal_paste(terminal, &paste, &written);
|
||||
if (result == GHOSTTY_REJECTED) {
|
||||
// The text could inject commands (e.g. a newline outside of a
|
||||
// bracketed paste). Nothing was written; ask, then retry.
|
||||
if (!confirm_with_user()) return;
|
||||
paste.allow_unsafe = true;
|
||||
result = ghostty_terminal_paste(terminal, &paste, &written);
|
||||
}
|
||||
if (result != GHOSTTY_SUCCESS) {
|
||||
fprintf(stderr, "paste failed: %d\n", (int)result);
|
||||
return;
|
||||
}
|
||||
|
||||
// Whether the pty got the text or a paste event depends on the
|
||||
// terminal's modes; either way it went through write_pty above, in
|
||||
// chunks as the text was read.
|
||||
printf(" %s\n", written ? "written" : "nothing to paste");
|
||||
}
|
||||
//! [terminal-paste]
|
||||
|
||||
//! [paste-safety]
|
||||
void safety_example() {
|
||||
const char* safe_data = "hello world";
|
||||
@@ -29,27 +182,87 @@ void encode_example() {
|
||||
|
||||
if (result == GHOSTTY_SUCCESS) {
|
||||
printf("Encoded %zu bytes: ", written);
|
||||
fwrite(buf, 1, written, stdout);
|
||||
print_escaped((const uint8_t*)buf, written);
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
//! [paste-encode]
|
||||
|
||||
static void vt_write(GhosttyTerminal terminal, const char* seq) {
|
||||
ghostty_terminal_vt_write(terminal, (const uint8_t*)seq, strlen(seq));
|
||||
}
|
||||
|
||||
int main() {
|
||||
GhosttyTerminal terminal = NULL;
|
||||
if (ghostty_terminal_new(NULL, &terminal, 80, 24) != GHOSTTY_SUCCESS) {
|
||||
fprintf(stderr, "Failed to create terminal\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Pasted bytes and paste events go to write_pty. Serving clipboard
|
||||
// reads is what lets the terminal send paste events at all: without
|
||||
// this callback the program could never read the clipboard, so pastes
|
||||
// stay text even when mode 5522 is enabled.
|
||||
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_WRITE_PTY,
|
||||
(const void*)on_write_pty);
|
||||
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_CLIPBOARD_READ,
|
||||
(const void*)on_clipboard_read);
|
||||
|
||||
printf("Plain paste:\n");
|
||||
paste_clipboard(terminal, "hello world");
|
||||
|
||||
printf("Paste with a newline (refused, then confirmed):\n");
|
||||
paste_clipboard(terminal, "echo hi\n");
|
||||
|
||||
// The program enables bracketed paste: newlines are safe inside the
|
||||
// frame and are preserved.
|
||||
printf("Bracketed paste (mode 2004):\n");
|
||||
vt_write(terminal, "\x1b[?2004h");
|
||||
paste_clipboard(terminal, "line one\nline two");
|
||||
|
||||
// The program enables paste events: the clipboard's MIME types are
|
||||
// listed with a one-time password instead of writing the data.
|
||||
printf("Paste event (mode 5522):\n");
|
||||
vt_write(terminal, "\x1b[?5522h");
|
||||
paste_clipboard(terminal, "hello world");
|
||||
|
||||
// Play the program's side: read the clipboard with the password from
|
||||
// the event. The read arrives granted and the data is served through
|
||||
// write_pty as base64 without any permission prompt.
|
||||
if (event_pw[0] != 0) {
|
||||
printf("Program reads with the event password:\n");
|
||||
char read_seq[256];
|
||||
snprintf(read_seq, sizeof(read_seq),
|
||||
"\x1b]5522;type=read:pw=%s:name=UGFzdGUgZXZlbnQ=;dGV4dC9wbGFpbg==\x1b\\",
|
||||
event_pw);
|
||||
vt_write(terminal, read_seq);
|
||||
}
|
||||
|
||||
// Text inserted by other means (IME, drag and drop) is never an event.
|
||||
printf("IME text with mode 5522 enabled:\n");
|
||||
{
|
||||
clipboard_t clipboard = {.text = "committed"};
|
||||
GhosttyString mime = GS("text/plain");
|
||||
GhosttyPaste paste = {
|
||||
.size = sizeof(paste),
|
||||
.location = GHOSTTY_CLIPBOARD_LOCATION_STANDARD,
|
||||
.source = GHOSTTY_PASTE_SOURCE_TEXT,
|
||||
.mimes = &mime,
|
||||
.mimes_len = 1,
|
||||
.reader = {.read = read_clipboard, .userdata = &clipboard},
|
||||
.allow_unsafe = true,
|
||||
};
|
||||
bool written = false;
|
||||
if (ghostty_terminal_paste(terminal, &paste, &written) == GHOSTTY_SUCCESS &&
|
||||
written) {
|
||||
printf(" written\n");
|
||||
}
|
||||
}
|
||||
|
||||
ghostty_terminal_free(terminal);
|
||||
|
||||
printf("\nTerminal-free building blocks:\n");
|
||||
safety_example();
|
||||
|
||||
// Test unsafe paste data with bracketed paste end sequence
|
||||
const char *unsafe_escape = "evil\x1b[201~code";
|
||||
if (!ghostty_paste_is_safe(unsafe_escape, strlen(unsafe_escape))) {
|
||||
printf("Data with escape sequence is UNSAFE\n");
|
||||
}
|
||||
|
||||
// Test empty data
|
||||
const char *empty_data = "";
|
||||
if (ghostty_paste_is_safe(empty_data, 0)) {
|
||||
printf("Empty data is safe\n");
|
||||
}
|
||||
|
||||
encode_example();
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
* - @ref snapshot "Terminal Snapshot" - Encode and incrementally restore terminal state
|
||||
* - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences
|
||||
* - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences
|
||||
* - @ref paste "Paste Utilities" - Validate paste data safety
|
||||
* - @ref paste "Paste" - Paste into a terminal, validate and encode paste data
|
||||
* - @ref unicode "Unicode Utilities" - Codepoint properties for text layout
|
||||
* - @ref build_info "Build Info" - Query compile-time build configuration
|
||||
* - @ref allocator "Memory Management" - Memory management and custom allocators
|
||||
@@ -53,7 +53,7 @@
|
||||
* - @ref c-vt/src/main.c - OSC parser example
|
||||
* - @ref c-vt-encode-key/src/main.c - Key encoding example
|
||||
* - @ref c-vt-encode-mouse/src/main.c - Mouse encoding example
|
||||
* - @ref c-vt-paste/src/main.c - Paste safety check example
|
||||
* - @ref c-vt-paste/src/main.c - Paste example
|
||||
* - @ref c-vt-sgr/src/main.c - SGR parser example
|
||||
* - @ref c-vt-formatter/src/main.c - Terminal formatter example
|
||||
* - @ref c-vt-grid-traverse/src/main.c - Grid traversal example using grid refs
|
||||
@@ -83,8 +83,10 @@
|
||||
*/
|
||||
|
||||
/** @example c-vt-paste/src/main.c
|
||||
* This example demonstrates how to use the paste utilities to check if
|
||||
* paste data is safe before sending it to the terminal.
|
||||
* This example demonstrates how to paste into a terminal, including the
|
||||
* unsafe-paste confirmation flow and Kitty clipboard protocol paste events
|
||||
* (mode 5522), as well as the terminal-free paste safety and encoding
|
||||
* utilities.
|
||||
*/
|
||||
|
||||
/** @example c-vt-sgr/src/main.c
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <ghostty/vt/types.h>
|
||||
|
||||
/** @defgroup io I/O
|
||||
*
|
||||
@@ -98,6 +99,49 @@ typedef struct {
|
||||
void* userdata;
|
||||
} GhosttyWriter;
|
||||
|
||||
/**
|
||||
* Read one MIME-typed representation of some content, streaming its
|
||||
* bytes to a writer.
|
||||
*
|
||||
* The library calls this with the MIME type of the representation it
|
||||
* needs. The callback writes all of that representation's data to
|
||||
* @p writer, in as many calls to `writer.write(writer.userdata, data,
|
||||
* len)` as is convenient (one call with everything or many small
|
||||
* pieces both work), and returns true. Nothing written is retained
|
||||
* beyond each write call, so the data may be borrowed from anywhere:
|
||||
* a pasteboard item, a file being read, a stream.
|
||||
*
|
||||
* Returning false reports that the data could not be read. If the
|
||||
* writer refuses a write (returns false), stop and return false
|
||||
* without writing more.
|
||||
*
|
||||
* All pointer arguments, the mime, and the writer are borrowed and
|
||||
* valid only for the duration of the callback. The callback is
|
||||
* invoked synchronously on the calling thread. The API receiving the
|
||||
* GhosttyMimeReader defines which MIME types are requested, how many
|
||||
* times, and any consistency requirements across repeated reads.
|
||||
*
|
||||
* @param userdata Opaque userdata from GhosttyMimeReader
|
||||
* @param mime The MIME type of the representation to read
|
||||
* @param writer Where to write the data; valid only during this call
|
||||
* @return true once all the data was written, false if it could not
|
||||
* be read or the writer refused a write
|
||||
*/
|
||||
typedef bool (*GhosttyMimeReaderFn)(
|
||||
void* userdata,
|
||||
GhosttyString mime,
|
||||
GhosttyWriter writer);
|
||||
|
||||
/**
|
||||
* A MIME-typed content source callback and its opaque context.
|
||||
*
|
||||
* The struct is passed by value. @p read must be non-NULL.
|
||||
*/
|
||||
typedef struct {
|
||||
GhosttyMimeReaderFn read;
|
||||
void* userdata;
|
||||
} GhosttyMimeReader;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,26 +1,67 @@
|
||||
/**
|
||||
* @file paste.h
|
||||
*
|
||||
* Paste utilities - validate and encode paste data for terminal input.
|
||||
* Paste - paste into a terminal, and validate and encode paste data.
|
||||
*/
|
||||
|
||||
#ifndef GHOSTTY_VT_PASTE_H
|
||||
#define GHOSTTY_VT_PASTE_H
|
||||
|
||||
/** @defgroup paste Paste Utilities
|
||||
/** @defgroup paste Paste
|
||||
*
|
||||
* Utilities for validating and encoding paste data for terminal input.
|
||||
* Pasting into a terminal, plus the terminal-free utilities for
|
||||
* validating and encoding paste data.
|
||||
*
|
||||
* ## Basic Usage
|
||||
* ## Pasting into a Terminal
|
||||
*
|
||||
* Use ghostty_paste_is_safe() to check if paste data contains potentially
|
||||
* dangerous sequences before sending it to the terminal.
|
||||
* What a paste writes to the pty depends on the terminal's state, so
|
||||
* the recommended way to paste is ghostty_terminal_paste(). The embedder
|
||||
* hands over the MIME types the clipboard holds (just `text/plain` for
|
||||
* an ordinary paste), a GhosttyMimeReader that produces the data of
|
||||
* any one of them, and where the paste came from, and the terminal
|
||||
* decides how its current modes apply:
|
||||
*
|
||||
* Use ghostty_paste_encode() to encode paste data for writing to the pty,
|
||||
* - If Kitty clipboard protocol paste events (mode 5522,
|
||||
* GHOSTTY_MODE_PASTE_EVENTS) are enabled, the paste was user-initiated
|
||||
* (GHOSTTY_PASTE_SOURCE_CLIPBOARD), and a clipboard_read callback is
|
||||
* installed, the terminal sends the program a paste event listing the
|
||||
* clipboard's MIME types with a one-time password instead of the data.
|
||||
* The program then reads what it wants through the clipboard_read
|
||||
* callback, which arrives with `granted` set so no permission prompt
|
||||
* is needed. No data is read for the event.
|
||||
* - Otherwise the first text representation is written: unsafe control
|
||||
* bytes are replaced with spaces, and it is wrapped in bracketed paste
|
||||
* sequences if mode 2004 (GHOSTTY_MODE_BRACKETED_PASTE) is enabled, or
|
||||
* has its newlines converted to carriage returns if not.
|
||||
*
|
||||
* The data is pulled through GhosttyPaste::reader only when a
|
||||
* representation is actually pasted (so a clipboard holding a large
|
||||
* image next to some text costs nothing), and the encoded bytes
|
||||
* stream to the write_pty callback (GHOSTTY_TERMINAL_OPT_WRITE_PTY)
|
||||
* in chunks as they are produced, never in one piece. The callback
|
||||
* may be invoked several times for a single paste; the pieces must be
|
||||
* written to the pty in order.
|
||||
*
|
||||
* Text that could inject commands (a newline when unbracketed, or the
|
||||
* bracketed paste terminator when bracketed) is refused with
|
||||
* GHOSTTY_REJECTED and nothing written unless GhosttyPaste::allow_unsafe
|
||||
* is set. The usual flow is to call once, confirm with the user on
|
||||
* GHOSTTY_REJECTED, and call again with `allow_unsafe` set. Each call
|
||||
* reads the text at most once and buffers it whole while the rule is
|
||||
* applied, so the source needs no stability across reads (the
|
||||
* confirmed retry simply pastes whatever the source holds then) and a
|
||||
* refused or failed paste writes nothing at all.
|
||||
*
|
||||
* @snippet c-vt-paste/src/main.c terminal-paste
|
||||
*
|
||||
* ## Building Blocks
|
||||
*
|
||||
* For embedders that encode without a terminal, ghostty_paste_is_safe()
|
||||
* checks if paste data contains potentially dangerous sequences
|
||||
* (conservatively, regardless of terminal state) and
|
||||
* ghostty_paste_encode() encodes paste data for writing to the pty,
|
||||
* including bracketed paste wrapping and unsafe byte stripping.
|
||||
*
|
||||
* ## Examples
|
||||
*
|
||||
* ### Safety Check
|
||||
*
|
||||
* @snippet c-vt-paste/src/main.c paste-safety
|
||||
@@ -35,11 +76,119 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <ghostty/vt/types.h>
|
||||
#include <ghostty/vt/io.h>
|
||||
#include <ghostty/vt/terminal.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Why a paste happened.
|
||||
*/
|
||||
typedef enum GHOSTTY_ENUM_TYPED {
|
||||
/** The user pasted from a clipboard: keybind, menu, middle click. */
|
||||
GHOSTTY_PASTE_SOURCE_CLIPBOARD = 0,
|
||||
|
||||
/**
|
||||
* Text inserted some other way: IME commit, drag and drop, scripted
|
||||
* input. Always written as text, never as a paste event, matching
|
||||
* kitty. This is not a way to opt out of paste events; an embedder
|
||||
* that doesn't want them doesn't install a clipboard_read callback.
|
||||
*/
|
||||
GHOSTTY_PASTE_SOURCE_TEXT = 1,
|
||||
GHOSTTY_PASTE_SOURCE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyPasteSource;
|
||||
|
||||
/**
|
||||
* A paste of clipboard contents into the terminal.
|
||||
*
|
||||
* This is a sized struct; set `size` to `sizeof(GhosttyPaste)`. The
|
||||
* MIME type array and the strings it points to are borrowed only for
|
||||
* the duration of the ghostty_terminal_paste() call, as is everything
|
||||
* the reader produces.
|
||||
*/
|
||||
typedef struct {
|
||||
/** Size of this struct in bytes. */
|
||||
size_t size;
|
||||
|
||||
/**
|
||||
* The clipboard the contents came from. Reported to the program on a
|
||||
* paste event (the selection and primary locations are both reported
|
||||
* as the primary selection, the protocol knows only two); no effect
|
||||
* on a text paste.
|
||||
*/
|
||||
GhosttyClipboardLocation location;
|
||||
|
||||
/** Why this paste happened. */
|
||||
GhosttyPasteSource source;
|
||||
|
||||
/**
|
||||
* Borrowed array of the MIME types of the representations available,
|
||||
* in preferred order. A text paste reads and writes the first entry
|
||||
* with a text MIME type such as "text/plain" and ignores the rest. A
|
||||
* paste event lists every entry and reads none. May be NULL when
|
||||
* mimes_len is zero, which is nothing to paste.
|
||||
*/
|
||||
const GhosttyString* mimes;
|
||||
|
||||
/** Number of entries in mimes. */
|
||||
size_t mimes_len;
|
||||
|
||||
/**
|
||||
* Produces the data of a representation on demand. Required when
|
||||
* mimes_len is nonzero.
|
||||
*
|
||||
* Called at most once per ghostty_terminal_paste() call: for the
|
||||
* text representation being pasted, never for anything else and
|
||||
* never for a paste event. The MIME type requested is always an
|
||||
* entry of `mimes`, passed through exactly as given there (the same
|
||||
* pointer and length), so the callback may identify the
|
||||
* representation by pointer or by content. A false return fails the
|
||||
* paste with GHOSTTY_IO_ERROR.
|
||||
*/
|
||||
GhosttyMimeReader reader;
|
||||
|
||||
/**
|
||||
* Write text that could inject commands. Call with false, confirm
|
||||
* with the user on GHOSTTY_REJECTED, and call again with true.
|
||||
*/
|
||||
bool allow_unsafe;
|
||||
} GhosttyPaste;
|
||||
|
||||
/**
|
||||
* Paste into the terminal according to its current state: a Kitty
|
||||
* clipboard protocol paste event if mode 5522 is enabled and a
|
||||
* clipboard_read callback is installed, otherwise the text framed per
|
||||
* mode 2004. See the group documentation for the full behavior. Output
|
||||
* streams through the write_pty callback in chunks. The viewport is not
|
||||
* scrolled; that is up to the embedder, as for key input.
|
||||
*
|
||||
* A paste event records a session grant for its one-time password only
|
||||
* once the event is written; a failed call never leaves a grant for an
|
||||
* event that was never sent.
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param paste The paste request, borrowed for the duration of the call
|
||||
* @param[out] out_written On success, whether anything was written to
|
||||
* the pty (the encoded text or a paste event). False means
|
||||
* there was nothing to paste: no non-empty text
|
||||
* representation. May be NULL.
|
||||
* @return GHOSTTY_SUCCESS on success (see @p out_written);
|
||||
* GHOSTTY_REJECTED if the text could inject commands and
|
||||
* GhosttyPaste::allow_unsafe is false (nothing was written);
|
||||
* GHOSTTY_INVALID_VALUE for a NULL terminal or paste, MIME
|
||||
* types without a reader, or when no write_pty callback is
|
||||
* installed; GHOSTTY_OUT_OF_MEMORY; GHOSTTY_IO_ERROR if the
|
||||
* reader failed or there is no secure entropy source to mint a
|
||||
* paste event password (wasm32-freestanding without
|
||||
* GHOSTTY_SYS_OPT_RANDOM_SECURE set). Errors write nothing.
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_terminal_paste(
|
||||
GhosttyTerminal terminal,
|
||||
const GhosttyPaste* paste,
|
||||
bool* out_written);
|
||||
|
||||
/**
|
||||
* Check if paste data is safe to paste into the terminal.
|
||||
*
|
||||
@@ -49,7 +198,9 @@ extern "C" {
|
||||
* to exit bracketed paste mode and inject commands
|
||||
*
|
||||
* This check is conservative and considers data unsafe regardless of
|
||||
* current terminal state.
|
||||
* current terminal state. ghostty_terminal_paste() applies the
|
||||
* terminal-state-aware rule itself (newlines are safe inside a
|
||||
* bracketed paste); use this to apply the stricter rule on top.
|
||||
*
|
||||
* @param data The paste data to check (must not be NULL)
|
||||
* @param len The length of the data in bytes
|
||||
@@ -74,6 +225,9 @@ GHOSTTY_API bool ghostty_paste_is_safe(const char* data, size_t len);
|
||||
* GHOSTTY_OUT_OF_SPACE and sets the required size in @p out_written.
|
||||
* The caller can then retry with a sufficiently sized buffer.
|
||||
*
|
||||
* This is the encoder ghostty_terminal_paste() uses for a text paste;
|
||||
* use it directly when there is no terminal to paste into.
|
||||
*
|
||||
* @param data The paste data to encode (modified in place, may be NULL)
|
||||
* @param data_len The length of the input data in bytes
|
||||
* @param bracketed Whether bracketed paste mode is active
|
||||
|
||||
@@ -124,6 +124,24 @@ typedef bool (*GhosttySysDecodePngFn)(
|
||||
size_t data_len,
|
||||
GhosttySysImage* out);
|
||||
|
||||
/**
|
||||
* Callback type for secure random bytes.
|
||||
*
|
||||
* Fills @p buf with @p len cryptographically secure random bytes. The
|
||||
* library uses this for secrets, so it must be a real CSPRNG (getrandom,
|
||||
* arc4random_buf, BCryptGenRandom, crypto.getRandomValues, ...); a
|
||||
* predictable source is a security hole.
|
||||
*
|
||||
* @param userdata The userdata pointer set via GHOSTTY_SYS_OPT_USERDATA
|
||||
* @param buf Buffer to fill
|
||||
* @param len Number of bytes to fill
|
||||
* @return true if the buffer was filled, false if no entropy is available
|
||||
*/
|
||||
typedef bool (*GhosttySysRandomSecureFn)(
|
||||
void* userdata,
|
||||
uint8_t* buf,
|
||||
size_t len);
|
||||
|
||||
/**
|
||||
* System option identifiers for ghostty_sys_set().
|
||||
*/
|
||||
@@ -165,6 +183,21 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
* Input type: GhosttySysLogFn (function pointer, or NULL)
|
||||
*/
|
||||
GHOSTTY_SYS_OPT_LOG = 2,
|
||||
|
||||
/**
|
||||
* Override the secure random source.
|
||||
*
|
||||
* By default the library draws secure random bytes from the
|
||||
* platform (getrandom or arc4random_buf on POSIX, CNG on Windows).
|
||||
* Targets without one, such as wasm32-freestanding, have no default
|
||||
* and operations that need entropy fail with GHOSTTY_IO_ERROR until
|
||||
* this is set. When set,
|
||||
* it is used instead of the platform source on every target. When
|
||||
* cleared (NULL value), the platform default is restored.
|
||||
*
|
||||
* Input type: GhosttySysRandomSecureFn (function pointer, or NULL)
|
||||
*/
|
||||
GHOSTTY_SYS_OPT_RANDOM_SECURE = 3,
|
||||
GHOSTTY_SYS_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttySysOption;
|
||||
|
||||
|
||||
@@ -724,6 +724,11 @@ struct GhosttyClipboardRead {
|
||||
* serves a request for only the targets listing (`list` with no `mimes`)
|
||||
* without prompting.
|
||||
*
|
||||
* Installing this callback also enables Kitty paste events (mode 5522):
|
||||
* ghostty_terminal_paste() sends the program an event instead of the text,
|
||||
* and the program's follow-up read arrives here with `granted` set since
|
||||
* the user already pasted. See ghostty_terminal_paste().
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
|
||||
* @param read Borrowed clipboard read request
|
||||
|
||||
@@ -100,6 +100,12 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
GHOSTTY_IO_ERROR = -5,
|
||||
/** Operation failed because encoded input exceeded a configured limit */
|
||||
GHOSTTY_LIMIT_EXCEEDED = -6,
|
||||
/**
|
||||
* Operation was rejected by a safety check (e.g. pasted text that could
|
||||
* inject commands). Nothing was done. Confirm with the user and retry
|
||||
* with the operation's allow flag set.
|
||||
*/
|
||||
GHOSTTY_REJECTED = -7,
|
||||
GHOSTTY_RESULT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyResult;
|
||||
|
||||
|
||||
362
po/bg.po
362
po/bg.po
@@ -9,7 +9,7 @@ msgstr ""
|
||||
"Project-Id-Version: com.mitchellh.ghostty\n"
|
||||
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
|
||||
"POT-Creation-Date: 2026-08-10 10:06-0500\n"
|
||||
"PO-Revision-Date: 2026-02-09 22:07+0200\n"
|
||||
"PO-Revision-Date: 2026-08-13 13:40+0300\n"
|
||||
"Last-Translator: reo101 <pavel.atanasov2001@gmail.com>\n"
|
||||
"Language-Team: Bulgarian <dict@ludost.net>\n"
|
||||
"Language: bg\n"
|
||||
@@ -175,7 +175,7 @@ msgstr "Раздели надясно"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:377
|
||||
msgid "Close Split"
|
||||
msgstr ""
|
||||
msgstr "Затвори разделянето"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:383
|
||||
msgid "Tab"
|
||||
@@ -184,7 +184,7 @@ msgstr "Раздел"
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:386 src/apprt/gtk/ui/1.5/window.blp:232
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:339 src/input/command.zig:464
|
||||
msgid "Change Tab Title…"
|
||||
msgstr "Смени името на таба…"
|
||||
msgstr "Промени заглавието на раздела…"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:391 src/apprt/gtk/ui/1.5/window.blp:60
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:110 src/apprt/gtk/ui/1.5/window.blp:237
|
||||
@@ -203,7 +203,7 @@ msgstr "Прозорец"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:406 src/apprt/gtk/ui/1.5/window.blp:215
|
||||
msgid "Change Window Title…"
|
||||
msgstr ""
|
||||
msgstr "Промени заглавието на прозореца…"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:411 src/apprt/gtk/ui/1.5/window.blp:220
|
||||
#: src/input/command.zig:421
|
||||
@@ -221,11 +221,11 @@ msgstr "Конфигурация"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:427 src/apprt/gtk/ui/1.5/window.blp:306
|
||||
msgid "Open Configuration in OS Editor"
|
||||
msgstr ""
|
||||
msgstr "Отвори конфигурацията в редактора на операционната система"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:433 src/apprt/gtk/ui/1.5/window.blp:312
|
||||
msgid "Open Configuration in New Window"
|
||||
msgstr ""
|
||||
msgstr "Отвори конфигурацията в нов прозорец"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/title-dialog.blp:5
|
||||
msgid "Leave blank to restore the default title."
|
||||
@@ -269,27 +269,27 @@ msgstr "Изпълни команда…"
|
||||
|
||||
#: src/apprt/gtk/class/application.zig:1767
|
||||
msgid "The keybind was revoked by the system."
|
||||
msgstr ""
|
||||
msgstr "Клавишната комбинация е отменена от системата."
|
||||
|
||||
#: src/apprt/gtk/class/application.zig:1769
|
||||
msgid "The keybind was denied by the system."
|
||||
msgstr ""
|
||||
msgstr "Клавишната комбинация е отказана от системата."
|
||||
|
||||
#: src/apprt/gtk/class/application.zig:1780
|
||||
msgid "Global keybind unavailable"
|
||||
msgstr ""
|
||||
msgstr "Глобалната клавишна комбинация не е достъпна"
|
||||
|
||||
#: src/apprt/gtk/class/application.zig:2259
|
||||
msgid "Export Terminal IO Events"
|
||||
msgstr ""
|
||||
msgstr "Експортиране на входно-изходните събития на терминала"
|
||||
|
||||
#: src/apprt/gtk/class/application.zig:2262
|
||||
msgid "Export"
|
||||
msgstr ""
|
||||
msgstr "Експортирай"
|
||||
|
||||
#: src/apprt/gtk/class/application.zig:2783
|
||||
msgid "Editing configuration file"
|
||||
msgstr ""
|
||||
msgstr "Редактиране на конфигурационния файл"
|
||||
|
||||
#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198
|
||||
msgid ""
|
||||
@@ -371,11 +371,11 @@ msgstr "Промяна на заглавието на терминала"
|
||||
|
||||
#: src/apprt/gtk/class/title_dialog.zig:228
|
||||
msgid "Change Tab Title"
|
||||
msgstr "Смени името на таба"
|
||||
msgstr "Промяна на заглавието на раздела"
|
||||
|
||||
#: src/apprt/gtk/class/title_dialog.zig:229
|
||||
msgid "Change Window Title"
|
||||
msgstr ""
|
||||
msgstr "Промяна на заглавието на прозореца"
|
||||
|
||||
#: src/apprt/gtk/class/window.zig:1153
|
||||
msgid "Reloaded the configuration"
|
||||
@@ -395,710 +395,768 @@ msgstr "Разработчици на Ghostty"
|
||||
|
||||
#: src/input/command.zig:149
|
||||
msgid "Reset Terminal"
|
||||
msgstr ""
|
||||
msgstr "Нулиране на терминала"
|
||||
|
||||
#: src/input/command.zig:150
|
||||
msgid "Reset the terminal to a clean state."
|
||||
msgstr ""
|
||||
msgstr "Връщане на терминала в начално състояние."
|
||||
|
||||
#: src/input/command.zig:155
|
||||
msgid "Copy to Clipboard"
|
||||
msgstr ""
|
||||
msgstr "Копиране в клипборда"
|
||||
|
||||
#: src/input/command.zig:156
|
||||
msgid ""
|
||||
"Copy the selected text to the clipboard in both plain and styled formats."
|
||||
msgstr ""
|
||||
"Копиране на избрания текст в клипборда както в обикновен, така и във "
|
||||
"форматиран вид."
|
||||
|
||||
#: src/input/command.zig:159
|
||||
msgid "Copy Selection as Plain Text to Clipboard"
|
||||
msgstr ""
|
||||
msgstr "Копиране на избраното като обикновен текст в клипборда"
|
||||
|
||||
#: src/input/command.zig:160
|
||||
msgid "Copy the selected text as plain text to the clipboard."
|
||||
msgstr ""
|
||||
msgstr "Копиране на избрания текст като обикновен текст в клипборда."
|
||||
|
||||
#: src/input/command.zig:163
|
||||
msgid "Copy Selection as ANSI Sequences to Clipboard"
|
||||
msgstr ""
|
||||
msgstr "Копиране на избраното с ANSI форматиране в клипборда"
|
||||
|
||||
#: src/input/command.zig:164
|
||||
msgid "Copy the selected text as ANSI escape sequences to the clipboard."
|
||||
msgstr ""
|
||||
msgstr "Копиране на избрания текст с ANSI форматиране в клипборда."
|
||||
|
||||
#: src/input/command.zig:167
|
||||
msgid "Copy Selection as HTML to Clipboard"
|
||||
msgstr ""
|
||||
msgstr "Копиране на избраното като HTML в клипборда"
|
||||
|
||||
#: src/input/command.zig:168
|
||||
msgid "Copy the selected text as HTML to the clipboard."
|
||||
msgstr ""
|
||||
msgstr "Копиране на избрания текст като HTML в клипборда."
|
||||
|
||||
#: src/input/command.zig:173
|
||||
msgid "Copy URL to Clipboard"
|
||||
msgstr ""
|
||||
msgstr "Копиране на URL адреса в клипборда"
|
||||
|
||||
#: src/input/command.zig:174
|
||||
msgid "Copy the URL under the cursor to the clipboard."
|
||||
msgstr ""
|
||||
msgstr "Копиране на URL адреса под курсора в клипборда."
|
||||
|
||||
#: src/input/command.zig:179
|
||||
msgid "Copy Terminal Title to Clipboard"
|
||||
msgstr ""
|
||||
msgstr "Копиране на заглавието на терминала в клипборда"
|
||||
|
||||
#: src/input/command.zig:180
|
||||
msgid ""
|
||||
"Copy the terminal title to the clipboard. If the terminal title is not set "
|
||||
"this has no effect."
|
||||
msgstr ""
|
||||
"Копиране на заглавието на терминала в клипборда. Ако няма зададено заглавие, "
|
||||
"действието няма ефект."
|
||||
|
||||
#: src/input/command.zig:185
|
||||
msgid "Paste from Clipboard"
|
||||
msgstr ""
|
||||
msgstr "Поставяне от клипборда"
|
||||
|
||||
#: src/input/command.zig:186
|
||||
msgid "Paste the contents of the main clipboard."
|
||||
msgstr ""
|
||||
msgstr "Поставяне на съдържанието на основния клипборд."
|
||||
|
||||
#: src/input/command.zig:191
|
||||
msgid "Paste from Selection"
|
||||
msgstr ""
|
||||
msgstr "Поставяне от избраното"
|
||||
|
||||
#: src/input/command.zig:192
|
||||
msgid "Paste the contents of the selection clipboard."
|
||||
msgstr ""
|
||||
msgstr "Поставяне на съдържанието на клипборда за избраното."
|
||||
|
||||
#: src/input/command.zig:197
|
||||
msgid "Start Search"
|
||||
msgstr ""
|
||||
msgstr "Начало на търсенето"
|
||||
|
||||
#: src/input/command.zig:198
|
||||
msgid "Start a search if one isn't already active."
|
||||
msgstr ""
|
||||
msgstr "Започване на търсене, ако в момента няма активно такова."
|
||||
|
||||
#: src/input/command.zig:203
|
||||
msgid "Search Selection"
|
||||
msgstr ""
|
||||
msgstr "Търсене на избраното"
|
||||
|
||||
#: src/input/command.zig:204
|
||||
msgid "Start a search for the current text selection."
|
||||
msgstr ""
|
||||
msgstr "Започване на търсене на текущо избрания текст."
|
||||
|
||||
#: src/input/command.zig:209
|
||||
msgid "End Search"
|
||||
msgstr ""
|
||||
msgstr "Край на търсенето"
|
||||
|
||||
#: src/input/command.zig:210
|
||||
msgid "End the current search if any and hide any GUI elements."
|
||||
msgstr ""
|
||||
"Прекратяване на текущото търсене, ако има такова, и скриване на всички "
|
||||
"елементи на графичния интерфейс."
|
||||
|
||||
#: src/input/command.zig:215
|
||||
msgid "Next Search Result"
|
||||
msgstr ""
|
||||
msgstr "Следващ резултат от търсенето"
|
||||
|
||||
#: src/input/command.zig:216
|
||||
msgid "Navigate to the next search result, if any."
|
||||
msgstr ""
|
||||
msgstr "Преминаване към следващия резултат от търсенето, ако има такъв."
|
||||
|
||||
#: src/input/command.zig:219
|
||||
msgid "Previous Search Result"
|
||||
msgstr ""
|
||||
msgstr "Предишен резултат от търсенето"
|
||||
|
||||
#: src/input/command.zig:220
|
||||
msgid "Navigate to the previous search result, if any."
|
||||
msgstr ""
|
||||
msgstr "Преминаване към предишния резултат от търсенето, ако има такъв."
|
||||
|
||||
#: src/input/command.zig:225
|
||||
msgid "Increase Font Size"
|
||||
msgstr ""
|
||||
msgstr "Увеличаване на размера на шрифта"
|
||||
|
||||
#: src/input/command.zig:226
|
||||
msgid "Increase the font size by 1 point."
|
||||
msgstr ""
|
||||
msgstr "Увеличаване на размера на шрифта с 1 пункт."
|
||||
|
||||
#: src/input/command.zig:231
|
||||
msgid "Decrease Font Size"
|
||||
msgstr ""
|
||||
msgstr "Намаляване на размера на шрифта"
|
||||
|
||||
#: src/input/command.zig:232
|
||||
msgid "Decrease the font size by 1 point."
|
||||
msgstr ""
|
||||
msgstr "Намаляване на размера на шрифта с 1 пункт."
|
||||
|
||||
#: src/input/command.zig:237
|
||||
msgid "Reset Font Size"
|
||||
msgstr ""
|
||||
msgstr "Възстановяване на размера на шрифта"
|
||||
|
||||
#: src/input/command.zig:238
|
||||
msgid "Reset the font size to the default."
|
||||
msgstr ""
|
||||
msgstr "Възстановяване на размера на шрифта до стойността по подразбиране."
|
||||
|
||||
#: src/input/command.zig:243
|
||||
msgid "Clear Screen"
|
||||
msgstr ""
|
||||
msgstr "Изчистване на екрана"
|
||||
|
||||
#: src/input/command.zig:244
|
||||
msgid "Clear the screen and scrollback."
|
||||
msgstr ""
|
||||
msgstr "Изчистване на екрана и буфера за превъртане назад."
|
||||
|
||||
#: src/input/command.zig:249
|
||||
msgid "Select All"
|
||||
msgstr ""
|
||||
msgstr "Избиране на всичко"
|
||||
|
||||
#: src/input/command.zig:250
|
||||
msgid "Select all text on the screen."
|
||||
msgstr ""
|
||||
msgstr "Избиране на целия текст на екрана."
|
||||
|
||||
#: src/input/command.zig:255
|
||||
msgid "Scroll to Top"
|
||||
msgstr ""
|
||||
msgstr "Превъртане до началото"
|
||||
|
||||
#: src/input/command.zig:256
|
||||
msgid "Scroll to the top of the screen."
|
||||
msgstr ""
|
||||
msgstr "Превъртане до началото на екрана."
|
||||
|
||||
#: src/input/command.zig:261
|
||||
msgid "Scroll to Bottom"
|
||||
msgstr ""
|
||||
msgstr "Превъртане до края"
|
||||
|
||||
#: src/input/command.zig:262
|
||||
msgid "Scroll to the bottom of the screen."
|
||||
msgstr ""
|
||||
msgstr "Превъртане до края на екрана."
|
||||
|
||||
#: src/input/command.zig:267
|
||||
msgid "Scroll to Selection"
|
||||
msgstr ""
|
||||
msgstr "Превъртане до избраното"
|
||||
|
||||
#: src/input/command.zig:268
|
||||
msgid "Scroll to the selected text."
|
||||
msgstr ""
|
||||
msgstr "Превъртане до избрания текст."
|
||||
|
||||
#: src/input/command.zig:273
|
||||
msgid "Scroll Page Up"
|
||||
msgstr ""
|
||||
msgstr "Превъртане с една страница нагоре"
|
||||
|
||||
#: src/input/command.zig:274
|
||||
msgid "Scroll the screen up by a page."
|
||||
msgstr ""
|
||||
msgstr "Превъртане на екрана с една страница нагоре."
|
||||
|
||||
#: src/input/command.zig:279
|
||||
msgid "Scroll Page Down"
|
||||
msgstr ""
|
||||
msgstr "Превъртане с една страница надолу"
|
||||
|
||||
#: src/input/command.zig:280
|
||||
msgid "Scroll the screen down by a page."
|
||||
msgstr ""
|
||||
msgstr "Превъртане на екрана с една страница надолу."
|
||||
|
||||
#: src/input/command.zig:286
|
||||
msgid "Copy Screen to Temporary File and Copy Path"
|
||||
msgstr ""
|
||||
msgstr "Записване на екрана във временен файл и копиране на пътя"
|
||||
|
||||
#: src/input/command.zig:287
|
||||
msgid ""
|
||||
"Copy the screen contents to a temporary file and copy the path to the "
|
||||
"clipboard."
|
||||
msgstr ""
|
||||
"Съдържанието на екрана се записва във временен файл, а пътят до файла се "
|
||||
"копира в клипборда."
|
||||
|
||||
#: src/input/command.zig:291
|
||||
msgid "Copy Screen to Temporary File and Paste Path"
|
||||
msgstr ""
|
||||
msgstr "Записване на екрана във временен файл и поставяне на пътя"
|
||||
|
||||
#: src/input/command.zig:292
|
||||
msgid ""
|
||||
"Copy the screen contents to a temporary file and paste the path to the file."
|
||||
msgstr ""
|
||||
"Съдържанието на екрана се записва във временен файл и се поставя пътят до "
|
||||
"файла."
|
||||
|
||||
#: src/input/command.zig:296
|
||||
msgid "Copy Screen to Temporary File and Open"
|
||||
msgstr ""
|
||||
msgstr "Записване на екрана във временен файл и отваряне на файла"
|
||||
|
||||
#: src/input/command.zig:297
|
||||
msgid "Copy the screen contents to a temporary file and open it."
|
||||
msgstr ""
|
||||
"Съдържанието на екрана се записва във временен файл, който след това се "
|
||||
"отваря."
|
||||
|
||||
#: src/input/command.zig:305
|
||||
msgid "Copy Screen as HTML to Temporary File and Copy Path"
|
||||
msgstr ""
|
||||
msgstr "Записване на екрана като HTML във временен файл и копиране на пътя"
|
||||
|
||||
#: src/input/command.zig:306
|
||||
msgid ""
|
||||
"Copy the screen contents as HTML to a temporary file and copy the path to "
|
||||
"the clipboard."
|
||||
msgstr ""
|
||||
"Съдържанието на екрана се записва във временен файл в HTML формат, а пътят "
|
||||
"до файла се копира в клипборда."
|
||||
|
||||
#: src/input/command.zig:313
|
||||
msgid "Copy Screen as HTML to Temporary File and Paste Path"
|
||||
msgstr ""
|
||||
msgstr "Записване на екрана като HTML във временен файл и поставяне на пътя"
|
||||
|
||||
#: src/input/command.zig:314
|
||||
msgid ""
|
||||
"Copy the screen contents as HTML to a temporary file and paste the path to "
|
||||
"the file."
|
||||
msgstr ""
|
||||
"Съдържанието на екрана се записва във временен файл в HTML формат и се "
|
||||
"поставя пътят до файла."
|
||||
|
||||
#: src/input/command.zig:321
|
||||
msgid "Copy Screen as HTML to Temporary File and Open"
|
||||
msgstr ""
|
||||
msgstr "Записване на екрана като HTML във временен файл и отваряне на файла"
|
||||
|
||||
#: src/input/command.zig:322
|
||||
msgid "Copy the screen contents as HTML to a temporary file and open it."
|
||||
msgstr ""
|
||||
"Съдържанието на екрана се записва във временен файл в HTML формат, който "
|
||||
"след това се отваря."
|
||||
|
||||
#: src/input/command.zig:330
|
||||
msgid "Copy Screen as ANSI Sequences to Temporary File and Copy Path"
|
||||
msgstr ""
|
||||
"Записване на екрана с ANSI форматиране във временен файл и копиране на пътя"
|
||||
|
||||
#: src/input/command.zig:331
|
||||
msgid ""
|
||||
"Copy the screen contents as ANSI escape sequences to a temporary file and "
|
||||
"copy the path to the clipboard."
|
||||
msgstr ""
|
||||
"Съдържанието на екрана се записва във временен файл с ANSI форматиране, а "
|
||||
"пътят до файла се копира в клипборда."
|
||||
|
||||
#: src/input/command.zig:338
|
||||
msgid "Copy Screen as ANSI Sequences to Temporary File and Paste Path"
|
||||
msgstr ""
|
||||
"Записване на екрана с ANSI форматиране във временен файл и поставяне на пътя"
|
||||
|
||||
#: src/input/command.zig:339
|
||||
msgid ""
|
||||
"Copy the screen contents as ANSI escape sequences to a temporary file and "
|
||||
"paste the path to the file."
|
||||
msgstr ""
|
||||
"Съдържанието на екрана се записва във временен файл с ANSI форматиране и се "
|
||||
"поставя пътят до файла."
|
||||
|
||||
#: src/input/command.zig:346
|
||||
msgid "Copy Screen as ANSI Sequences to Temporary File and Open"
|
||||
msgstr ""
|
||||
"Записване на екрана с ANSI форматиране във временен файл и отваряне на файла"
|
||||
|
||||
#: src/input/command.zig:347
|
||||
msgid ""
|
||||
"Copy the screen contents as ANSI escape sequences to a temporary file and "
|
||||
"open it."
|
||||
msgstr ""
|
||||
"Съдържанието на екрана се записва във временен файл с ANSI форматиране, "
|
||||
"който след това се отваря."
|
||||
|
||||
#: src/input/command.zig:354
|
||||
msgid "Copy Selection to Temporary File and Copy Path"
|
||||
msgstr ""
|
||||
msgstr "Записване на избраното във временен файл и копиране на пътя"
|
||||
|
||||
#: src/input/command.zig:355
|
||||
msgid ""
|
||||
"Copy the selection contents to a temporary file and copy the path to the "
|
||||
"clipboard."
|
||||
msgstr ""
|
||||
"Избраното съдържание се записва във временен файл, а пътят до файла се "
|
||||
"копира в клипборда."
|
||||
|
||||
#: src/input/command.zig:359
|
||||
msgid "Copy Selection to Temporary File and Paste Path"
|
||||
msgstr ""
|
||||
msgstr "Записване на избраното във временен файл и поставяне на пътя"
|
||||
|
||||
#: src/input/command.zig:360
|
||||
msgid ""
|
||||
"Copy the selection contents to a temporary file and paste the path to the "
|
||||
"file."
|
||||
msgstr ""
|
||||
"Избраното съдържание се записва във временен файл и се поставя пътят до "
|
||||
"файла."
|
||||
|
||||
#: src/input/command.zig:364
|
||||
msgid "Copy Selection to Temporary File and Open"
|
||||
msgstr ""
|
||||
msgstr "Записване на избраното във временен файл и отваряне на файла"
|
||||
|
||||
#: src/input/command.zig:365
|
||||
msgid "Copy the selection contents to a temporary file and open it."
|
||||
msgstr ""
|
||||
"Избраното съдържание се записва във временен файл, който след това се отваря."
|
||||
|
||||
#: src/input/command.zig:373
|
||||
msgid "Copy Selection as HTML to Temporary File and Copy Path"
|
||||
msgstr ""
|
||||
msgstr "Записване на избраното като HTML във временен файл и копиране на пътя"
|
||||
|
||||
#: src/input/command.zig:374
|
||||
msgid ""
|
||||
"Copy the selection contents as HTML to a temporary file and copy the path to "
|
||||
"the clipboard."
|
||||
msgstr ""
|
||||
"Избраното съдържание се записва във временен файл в HTML формат, а пътят до "
|
||||
"файла се копира в клипборда."
|
||||
|
||||
#: src/input/command.zig:381
|
||||
msgid "Copy Selection as HTML to Temporary File and Paste Path"
|
||||
msgstr ""
|
||||
msgstr "Записване на избраното като HTML във временен файл и поставяне на пътя"
|
||||
|
||||
#: src/input/command.zig:382
|
||||
msgid ""
|
||||
"Copy the selection contents as HTML to a temporary file and paste the path "
|
||||
"to the file."
|
||||
msgstr ""
|
||||
"Избраното съдържание се записва във временен файл в HTML формат и се поставя "
|
||||
"пътят до файла."
|
||||
|
||||
#: src/input/command.zig:389
|
||||
msgid "Copy Selection as HTML to Temporary File and Open"
|
||||
msgstr ""
|
||||
msgstr "Записване на избраното като HTML във временен файл и отваряне на файла"
|
||||
|
||||
#: src/input/command.zig:390
|
||||
msgid "Copy the selection contents as HTML to a temporary file and open it."
|
||||
msgstr ""
|
||||
"Избраното съдържание се записва във временен файл в HTML формат, който след "
|
||||
"това се отваря."
|
||||
|
||||
#: src/input/command.zig:398
|
||||
msgid "Copy Selection as ANSI Sequences to Temporary File and Copy Path"
|
||||
msgstr ""
|
||||
"Записване на избраното с ANSI форматиране във временен файл и копиране на "
|
||||
"пътя"
|
||||
|
||||
#: src/input/command.zig:399
|
||||
msgid ""
|
||||
"Copy the selection contents as ANSI escape sequences to a temporary file and "
|
||||
"copy the path to the clipboard."
|
||||
msgstr ""
|
||||
"Избраното съдържание се записва във временен файл с ANSI форматиране, а "
|
||||
"пътят до файла се копира в клипборда."
|
||||
|
||||
#: src/input/command.zig:406
|
||||
msgid "Copy Selection as ANSI Sequences to Temporary File and Paste Path"
|
||||
msgstr ""
|
||||
"Записване на избраното с ANSI форматиране във временен файл и поставяне на "
|
||||
"пътя"
|
||||
|
||||
#: src/input/command.zig:407
|
||||
msgid ""
|
||||
"Copy the selection contents as ANSI escape sequences to a temporary file and "
|
||||
"paste the path to the file."
|
||||
msgstr ""
|
||||
"Избраното съдържание се записва във временен файл с ANSI форматиране и се "
|
||||
"поставя пътят до файла."
|
||||
|
||||
#: src/input/command.zig:414
|
||||
msgid "Copy Selection as ANSI Sequences to Temporary File and Open"
|
||||
msgstr ""
|
||||
"Записване на избраното с ANSI форматиране във временен файл и отваряне на "
|
||||
"файла"
|
||||
|
||||
#: src/input/command.zig:415
|
||||
msgid ""
|
||||
"Copy the selection contents as ANSI escape sequences to a temporary file and "
|
||||
"open it."
|
||||
msgstr ""
|
||||
"Избраното съдържание се записва във временен файл с ANSI форматиране, който "
|
||||
"след това се отваря."
|
||||
|
||||
#: src/input/command.zig:422
|
||||
msgid "Open a new window."
|
||||
msgstr ""
|
||||
msgstr "Отваряне на нов прозорец."
|
||||
|
||||
#: src/input/command.zig:428
|
||||
msgid "Open a new tab."
|
||||
msgstr ""
|
||||
msgstr "Отваряне на нов раздел."
|
||||
|
||||
#: src/input/command.zig:434
|
||||
msgid "Move Tab Left"
|
||||
msgstr ""
|
||||
msgstr "Преместване на раздела наляво"
|
||||
|
||||
#: src/input/command.zig:435
|
||||
msgid "Move the current tab to the left."
|
||||
msgstr ""
|
||||
msgstr "Преместване на текущия раздел наляво."
|
||||
|
||||
#: src/input/command.zig:439
|
||||
msgid "Move Tab Right"
|
||||
msgstr ""
|
||||
msgstr "Преместване на раздела надясно"
|
||||
|
||||
#: src/input/command.zig:440
|
||||
msgid "Move the current tab to the right."
|
||||
msgstr ""
|
||||
msgstr "Преместване на текущия раздел надясно."
|
||||
|
||||
#: src/input/command.zig:446
|
||||
msgid "Move Tab to New Window"
|
||||
msgstr ""
|
||||
msgstr "Преместване на раздела в нов прозорец"
|
||||
|
||||
#: src/input/command.zig:447
|
||||
msgid "Move the current tab to a new window."
|
||||
msgstr ""
|
||||
msgstr "Преместване на текущия раздел в нов прозорец."
|
||||
|
||||
#: src/input/command.zig:452
|
||||
msgid "Toggle Tab Overview"
|
||||
msgstr ""
|
||||
msgstr "Превключване на прегледа на разделите"
|
||||
|
||||
#: src/input/command.zig:453
|
||||
msgid "Toggle the tab overview."
|
||||
msgstr ""
|
||||
msgstr "Превключване на прегледа на разделите."
|
||||
|
||||
#: src/input/command.zig:458
|
||||
msgid "Change Terminal Title…"
|
||||
msgstr ""
|
||||
msgstr "Промяна на заглавието на терминала…"
|
||||
|
||||
#: src/input/command.zig:459
|
||||
msgid "Prompt for a new title for the current terminal."
|
||||
msgstr ""
|
||||
msgstr "Показване на подкана за ново заглавие на текущия терминал."
|
||||
|
||||
#: src/input/command.zig:465
|
||||
msgid "Prompt for a new title for the current tab."
|
||||
msgstr ""
|
||||
msgstr "Показване на подкана за ново заглавие на текущия раздел."
|
||||
|
||||
#: src/input/command.zig:478
|
||||
msgid "Split the terminal to the left."
|
||||
msgstr ""
|
||||
msgstr "Разделяне на терминала наляво."
|
||||
|
||||
#: src/input/command.zig:483
|
||||
msgid "Split the terminal to the right."
|
||||
msgstr ""
|
||||
msgstr "Разделяне на терминала надясно."
|
||||
|
||||
#: src/input/command.zig:488
|
||||
msgid "Split the terminal up."
|
||||
msgstr ""
|
||||
msgstr "Разделяне на терминала нагоре."
|
||||
|
||||
#: src/input/command.zig:493
|
||||
msgid "Split the terminal down."
|
||||
msgstr ""
|
||||
msgstr "Разделяне на терминала надолу."
|
||||
|
||||
#: src/input/command.zig:500
|
||||
msgid "Focus Split: Previous"
|
||||
msgstr ""
|
||||
msgstr "Фокусиране на предишното разделяне"
|
||||
|
||||
#: src/input/command.zig:501
|
||||
msgid "Focus the previous split, if any."
|
||||
msgstr ""
|
||||
msgstr "Фокусиране на предишното разделяне, ако има такова."
|
||||
|
||||
#: src/input/command.zig:505
|
||||
msgid "Focus Split: Next"
|
||||
msgstr ""
|
||||
msgstr "Фокусиране на следващото разделяне"
|
||||
|
||||
#: src/input/command.zig:506
|
||||
msgid "Focus the next split, if any."
|
||||
msgstr ""
|
||||
msgstr "Фокусиране на следващото разделяне, ако има такова."
|
||||
|
||||
#: src/input/command.zig:510
|
||||
msgid "Focus Split: Left"
|
||||
msgstr ""
|
||||
msgstr "Фокусиране на разделянето отляво"
|
||||
|
||||
#: src/input/command.zig:511
|
||||
msgid "Focus the split to the left, if it exists."
|
||||
msgstr ""
|
||||
msgstr "Фокусиране на разделянето отляво, ако съществува."
|
||||
|
||||
#: src/input/command.zig:515
|
||||
msgid "Focus Split: Right"
|
||||
msgstr ""
|
||||
msgstr "Фокусиране на разделянето отдясно"
|
||||
|
||||
#: src/input/command.zig:516
|
||||
msgid "Focus the split to the right, if it exists."
|
||||
msgstr ""
|
||||
msgstr "Фокусиране на разделянето отдясно, ако съществува."
|
||||
|
||||
#: src/input/command.zig:520
|
||||
msgid "Focus Split: Up"
|
||||
msgstr ""
|
||||
msgstr "Фокусиране на разделянето отгоре"
|
||||
|
||||
#: src/input/command.zig:521
|
||||
msgid "Focus the split above, if it exists."
|
||||
msgstr ""
|
||||
msgstr "Фокусиране на разделянето отгоре, ако съществува."
|
||||
|
||||
#: src/input/command.zig:525
|
||||
msgid "Focus Split: Down"
|
||||
msgstr ""
|
||||
msgstr "Фокусиране на разделянето отдолу"
|
||||
|
||||
#: src/input/command.zig:526
|
||||
msgid "Focus the split below, if it exists."
|
||||
msgstr ""
|
||||
msgstr "Фокусиране на разделянето отдолу, ако съществува."
|
||||
|
||||
#: src/input/command.zig:533
|
||||
msgid "Focus Window: Previous"
|
||||
msgstr ""
|
||||
msgstr "Фокусиране на предишния прозорец"
|
||||
|
||||
#: src/input/command.zig:534
|
||||
msgid "Focus the previous window, if any."
|
||||
msgstr ""
|
||||
msgstr "Фокусиране на предишния прозорец, ако има такъв."
|
||||
|
||||
#: src/input/command.zig:538
|
||||
msgid "Focus Window: Next"
|
||||
msgstr ""
|
||||
msgstr "Фокусиране на следващия прозорец"
|
||||
|
||||
#: src/input/command.zig:539
|
||||
msgid "Focus the next window, if any."
|
||||
msgstr ""
|
||||
msgstr "Фокусиране на следващия прозорец, ако има такъв."
|
||||
|
||||
#: src/input/command.zig:545
|
||||
msgid "Toggle Split Zoom"
|
||||
msgstr ""
|
||||
msgstr "Превключване на увеличението на разделянето"
|
||||
|
||||
#: src/input/command.zig:546
|
||||
msgid "Toggle the zoom state of the current split."
|
||||
msgstr ""
|
||||
msgstr "Превключване на увеличението на текущото разделяне."
|
||||
|
||||
#: src/input/command.zig:551
|
||||
msgid "Toggle Read-Only Mode"
|
||||
msgstr ""
|
||||
msgstr "Превключване на режима само за четене"
|
||||
|
||||
#: src/input/command.zig:552
|
||||
msgid "Toggle read-only mode for the current surface."
|
||||
msgstr ""
|
||||
msgstr "Превключване на режима само за четене за текущия терминал."
|
||||
|
||||
#: src/input/command.zig:557
|
||||
msgid "Equalize Splits"
|
||||
msgstr ""
|
||||
msgstr "Изравняване на разделянията"
|
||||
|
||||
#: src/input/command.zig:558
|
||||
msgid "Equalize the size of all splits."
|
||||
msgstr ""
|
||||
msgstr "Изравняване на размера на всички разделяния."
|
||||
|
||||
#: src/input/command.zig:563
|
||||
msgid "Reset Window Size"
|
||||
msgstr ""
|
||||
msgstr "Възстановяване на размера на прозореца"
|
||||
|
||||
#: src/input/command.zig:564
|
||||
msgid "Reset the window size to the default."
|
||||
msgstr ""
|
||||
msgstr "Възстановяване на размера на прозореца до стойността по подразбиране."
|
||||
|
||||
#: src/input/command.zig:569
|
||||
msgid "Toggle Inspector"
|
||||
msgstr ""
|
||||
msgstr "Превключване на инспектора"
|
||||
|
||||
#: src/input/command.zig:570
|
||||
msgid "Toggle the inspector."
|
||||
msgstr ""
|
||||
msgstr "Превключване на инспектора."
|
||||
|
||||
#: src/input/command.zig:575
|
||||
msgid "Show the GTK Inspector"
|
||||
msgstr ""
|
||||
msgstr "Показване на GTK инспектора"
|
||||
|
||||
#: src/input/command.zig:576
|
||||
msgid "Show the GTK inspector."
|
||||
msgstr ""
|
||||
msgstr "Показване на GTK инспектора."
|
||||
|
||||
#: src/input/command.zig:581
|
||||
msgid "Show On-Screen Keyboard"
|
||||
msgstr ""
|
||||
msgstr "Показване на екранната клавиатура"
|
||||
|
||||
#: src/input/command.zig:582
|
||||
msgid "Show the on-screen keyboard if present."
|
||||
msgstr ""
|
||||
msgstr "Показване на екранната клавиатура, ако е налична."
|
||||
|
||||
#: src/input/command.zig:588
|
||||
msgid "Open Config Using OS editor"
|
||||
msgstr ""
|
||||
msgstr "Отваряне на конфигурацията с редактора на операционната система"
|
||||
|
||||
#: src/input/command.zig:589
|
||||
msgid "Open the config file with the OS's default editor."
|
||||
msgstr ""
|
||||
"Отваряне на конфигурационния файл с редактора по подразбиране на "
|
||||
"операционната система."
|
||||
|
||||
#: src/input/command.zig:593
|
||||
msgid "Open Config in New Terminal Window"
|
||||
msgstr ""
|
||||
msgstr "Отваряне на конфигурацията в нов терминален прозорец"
|
||||
|
||||
#: src/input/command.zig:594
|
||||
msgid "Open the config file in a new window using $EDITOR or $VISUAL."
|
||||
msgstr ""
|
||||
"Отваряне на конфигурационния файл в нов прозорец чрез $EDITOR или $VISUAL."
|
||||
|
||||
#: src/input/command.zig:600
|
||||
msgid "Reload Config"
|
||||
msgstr ""
|
||||
msgstr "Презареждане на конфигурацията"
|
||||
|
||||
#: src/input/command.zig:601
|
||||
msgid "Reload the config file."
|
||||
msgstr ""
|
||||
msgstr "Презареждане на конфигурационния файл."
|
||||
|
||||
#: src/input/command.zig:606
|
||||
msgid "Close Terminal"
|
||||
msgstr ""
|
||||
msgstr "Затваряне на терминала"
|
||||
|
||||
#: src/input/command.zig:607
|
||||
msgid "Close the current terminal."
|
||||
msgstr ""
|
||||
msgstr "Затваряне на текущия терминал."
|
||||
|
||||
#: src/input/command.zig:614
|
||||
msgid "Close the current tab."
|
||||
msgstr ""
|
||||
msgstr "Затваряне на текущия раздел."
|
||||
|
||||
#: src/input/command.zig:618
|
||||
msgid "Close Other Tabs"
|
||||
msgstr ""
|
||||
msgstr "Затваряне на другите раздели"
|
||||
|
||||
#: src/input/command.zig:619
|
||||
msgid "Close all tabs in this window except the current one."
|
||||
msgstr ""
|
||||
msgstr "Затваряне на всички раздели в този прозорец освен текущия."
|
||||
|
||||
#: src/input/command.zig:623
|
||||
msgid "Close Tabs to the Right"
|
||||
msgstr ""
|
||||
msgstr "Затваряне на разделите отдясно"
|
||||
|
||||
#: src/input/command.zig:624
|
||||
msgid "Close all tabs to the right of the current one."
|
||||
msgstr ""
|
||||
msgstr "Затваряне на всички раздели вдясно от текущия."
|
||||
|
||||
#: src/input/command.zig:631
|
||||
msgid "Close the current window."
|
||||
msgstr ""
|
||||
msgstr "Затваряне на текущия прозорец."
|
||||
|
||||
#: src/input/command.zig:636
|
||||
msgid "Close All Windows"
|
||||
msgstr ""
|
||||
msgstr "Затваряне на всички прозорци"
|
||||
|
||||
#: src/input/command.zig:637
|
||||
msgid "Close all windows."
|
||||
msgstr ""
|
||||
msgstr "Затваряне на всички прозорци."
|
||||
|
||||
#: src/input/command.zig:642
|
||||
msgid "Toggle Maximize"
|
||||
msgstr ""
|
||||
msgstr "Превключване на максимизирането"
|
||||
|
||||
#: src/input/command.zig:643
|
||||
msgid "Toggle the maximized state of the current window."
|
||||
msgstr ""
|
||||
msgstr "Превключване на максимизираното състояние на текущия прозорец."
|
||||
|
||||
#: src/input/command.zig:648
|
||||
msgid "Toggle Fullscreen"
|
||||
msgstr ""
|
||||
msgstr "Превключване на цял екран"
|
||||
|
||||
#: src/input/command.zig:649
|
||||
msgid "Toggle the fullscreen state of the current window."
|
||||
msgstr ""
|
||||
msgstr "Превключване на състоянието на цял екран на текущия прозорец."
|
||||
|
||||
#: src/input/command.zig:654
|
||||
msgid "Toggle Window Decorations"
|
||||
msgstr ""
|
||||
msgstr "Превключване на декорациите на прозореца"
|
||||
|
||||
#: src/input/command.zig:655
|
||||
msgid "Toggle the window decorations."
|
||||
msgstr ""
|
||||
msgstr "Показване или скриване на декорациите на прозореца."
|
||||
|
||||
#: src/input/command.zig:660
|
||||
msgid "Toggle Float on Top"
|
||||
msgstr ""
|
||||
msgstr "Превключване на задържането на прозореца най-отгоре"
|
||||
|
||||
#: src/input/command.zig:661
|
||||
msgid "Toggle the float on top state of the current window."
|
||||
msgstr ""
|
||||
"Превключване дали текущият прозорец да остава над всички останали прозорци."
|
||||
|
||||
#: src/input/command.zig:666
|
||||
msgid "Toggle Secure Input"
|
||||
msgstr ""
|
||||
msgstr "Превключване на защитеното въвеждане"
|
||||
|
||||
#: src/input/command.zig:667
|
||||
msgid "Toggle secure input mode."
|
||||
msgstr ""
|
||||
msgstr "Превключване на режима за защитено въвеждане."
|
||||
|
||||
#: src/input/command.zig:672
|
||||
msgid "Toggle Mouse Reporting"
|
||||
msgstr ""
|
||||
msgstr "Превключване на предаването на събития от мишката"
|
||||
|
||||
#: src/input/command.zig:673
|
||||
msgid "Toggle whether mouse events are reported to terminal applications."
|
||||
msgstr ""
|
||||
"Превключване дали събитията от мишката да се предават на приложенията в "
|
||||
"терминала."
|
||||
|
||||
#: src/input/command.zig:678
|
||||
msgid "Toggle Background Opacity"
|
||||
msgstr ""
|
||||
msgstr "Превключване на прозрачността на фона"
|
||||
|
||||
#: src/input/command.zig:679
|
||||
msgid "Toggle the background opacity of a window that started transparent."
|
||||
msgstr ""
|
||||
"Превключване между прозрачен и непрозрачен фон за прозорец, стартиран с "
|
||||
"прозрачен фон."
|
||||
|
||||
#: src/input/command.zig:684
|
||||
msgid "Check for Updates"
|
||||
msgstr ""
|
||||
msgstr "Проверка за обновления"
|
||||
|
||||
#: src/input/command.zig:685
|
||||
msgid "Check for updates to the application."
|
||||
msgstr ""
|
||||
msgstr "Проверка за обновления на приложението."
|
||||
|
||||
#: src/input/command.zig:690
|
||||
msgid "Undo"
|
||||
msgstr ""
|
||||
msgstr "Отмяна"
|
||||
|
||||
#: src/input/command.zig:691
|
||||
msgid "Undo the last action."
|
||||
msgstr ""
|
||||
msgstr "Отмяна на последното действие."
|
||||
|
||||
#: src/input/command.zig:696
|
||||
msgid "Redo"
|
||||
msgstr ""
|
||||
msgstr "Повтаряне"
|
||||
|
||||
#: src/input/command.zig:697
|
||||
msgid "Redo the last undone action."
|
||||
msgstr ""
|
||||
msgstr "Повтаряне на последното отменено действие."
|
||||
|
||||
#: src/input/command.zig:703
|
||||
msgid "Quit the application."
|
||||
msgstr ""
|
||||
msgstr "Излизане от приложението."
|
||||
|
||||
#: src/input/command.zig:708
|
||||
msgid "Ghostty"
|
||||
msgstr ""
|
||||
msgstr "Ghostty"
|
||||
|
||||
#: src/input/command.zig:709
|
||||
msgid "Put a little Ghostty in your terminal."
|
||||
msgstr ""
|
||||
msgstr "Добавете малко Ghostty в терминала си."
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
const std = @import("std");
|
||||
const Terminal = @import("../terminal/Terminal.zig");
|
||||
|
||||
/// The bracketed paste (mode 2004) frame written around the data.
|
||||
pub const bracketed_prefix = "\x1b[200~";
|
||||
pub const bracketed_suffix = "\x1b[201~";
|
||||
|
||||
/// The maximum number of bytes `encode` adds around the data, so callers
|
||||
/// can size a buffer for the full encoded result.
|
||||
pub const max_frame_size = bracketed_prefix.len + bracketed_suffix.len;
|
||||
|
||||
pub const Options = struct {
|
||||
/// True if bracketed paste mode is on.
|
||||
bracketed: bool,
|
||||
@@ -93,8 +101,8 @@ pub fn encode(
|
||||
// Bracketed paste mode (mode 2004) wraps pasted data in
|
||||
// fenceposts so that the terminal can ignore things like newlines.
|
||||
if (opts.bracketed) {
|
||||
result[0] = "\x1b[200~";
|
||||
result[2] = "\x1b[201~";
|
||||
result[0] = bracketed_prefix;
|
||||
result[2] = bracketed_suffix;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -116,6 +124,42 @@ pub const Error = error{
|
||||
MutableRequired,
|
||||
};
|
||||
|
||||
/// Encode the given data for pasting directly into a writer. This is
|
||||
/// the same transformation as `encode` (unsafe bytes replaced, bracketed
|
||||
/// frame or newline conversion per `opts`) but the data is copied
|
||||
/// exactly once: into the writer's buffer, where it is modified in place.
|
||||
/// This is the form to use when the data is const and the result is
|
||||
/// being assembled into a single buffer anyway.
|
||||
///
|
||||
/// The data is copied in chunks sized to the writer's buffer, so any
|
||||
/// writer works; a writer with less total capacity than the writer
|
||||
/// needs to hold at once reports `error.WriteFailed` as usual.
|
||||
///
|
||||
/// WARNING: The input data is not checked for safety. See `isSafe`
|
||||
/// and `isSafeWith` to check if the data is safe to paste.
|
||||
pub fn encodeWriter(
|
||||
writer: *std.Io.Writer,
|
||||
data: []const u8,
|
||||
opts: Options,
|
||||
) std.Io.Writer.Error!void {
|
||||
if (opts.bracketed) try writer.writeAll(bracketed_prefix);
|
||||
|
||||
// The byte transformations are position-independent, so the data
|
||||
// can be copied and encoded chunk by chunk. The frame returned by
|
||||
// encode is ignored since it's written around the whole data here.
|
||||
var remaining = data;
|
||||
while (remaining.len > 0) {
|
||||
const dest = try writer.writableSliceGreedy(1);
|
||||
const n = @min(dest.len, remaining.len);
|
||||
@memcpy(dest[0..n], remaining[0..n]);
|
||||
_ = encode(dest[0..n], opts);
|
||||
writer.advance(n);
|
||||
remaining = remaining[n..];
|
||||
}
|
||||
|
||||
if (opts.bracketed) try writer.writeAll(bracketed_suffix);
|
||||
}
|
||||
|
||||
/// Returns true if the data looks safe to paste. Data is considered
|
||||
/// unsafe if it contains any of the following:
|
||||
///
|
||||
@@ -133,6 +177,22 @@ pub fn isSafe(data: []const u8) bool {
|
||||
std.mem.indexOf(u8, data, "\x1b[201~") == null;
|
||||
}
|
||||
|
||||
/// Returns true if the data looks safe to paste given how it will be
|
||||
/// encoded. This is the terminal-state-aware counterpart of `isSafe`:
|
||||
///
|
||||
/// - Bracketed (mode 2004 on): the program receives the data as one
|
||||
/// framed unit, so newlines are fine. The data is unsafe only if it
|
||||
/// contains the end of the frame (`\x1b[201~`), which would let the
|
||||
/// rest of the data escape the frame and inject commands.
|
||||
/// - Unbracketed: the same rule as `isSafe`.
|
||||
///
|
||||
/// Callers wanting the conservative rule regardless of terminal state
|
||||
/// should use `isSafe` instead.
|
||||
pub fn isSafeWith(data: []const u8, opts: Options) bool {
|
||||
if (opts.bracketed) return std.mem.indexOf(u8, data, bracketed_suffix) == null;
|
||||
return isSafe(data);
|
||||
}
|
||||
|
||||
test isSafe {
|
||||
const testing = std.testing;
|
||||
try testing.expect(isSafe("hello"));
|
||||
@@ -141,6 +201,110 @@ test isSafe {
|
||||
try testing.expect(!isSafe("he\x1b[201~llo"));
|
||||
}
|
||||
|
||||
test isSafeWith {
|
||||
const testing = std.testing;
|
||||
|
||||
// Bracketed: newlines are fine, the frame terminator is not.
|
||||
try testing.expect(isSafeWith("hello", .{ .bracketed = true }));
|
||||
try testing.expect(isSafeWith("hello\nworld", .{ .bracketed = true }));
|
||||
try testing.expect(!isSafeWith("he\x1b[201~llo", .{ .bracketed = true }));
|
||||
try testing.expect(!isSafeWith("hello\n\x1b[201~", .{ .bracketed = true }));
|
||||
|
||||
// Unbracketed: the conservative rule.
|
||||
try testing.expect(isSafeWith("hello", .{ .bracketed = false }));
|
||||
try testing.expect(!isSafeWith("hello\nworld", .{ .bracketed = false }));
|
||||
try testing.expect(!isSafeWith("he\x1b[201~llo", .{ .bracketed = false }));
|
||||
}
|
||||
|
||||
test "encodeWriter bracketed" {
|
||||
const testing = std.testing;
|
||||
var buf: [64]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try encodeWriter(&writer, "hel\x1blo\nworld", .{ .bracketed = true });
|
||||
try testing.expectEqualStrings("\x1b[200~hel lo\nworld\x1b[201~", writer.buffered());
|
||||
}
|
||||
|
||||
test "encodeWriter unbracketed" {
|
||||
const testing = std.testing;
|
||||
var buf: [64]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try encodeWriter(&writer, "hel\x00lo\r\nworld", .{ .bracketed = false });
|
||||
try testing.expectEqualStrings("hel lo\r\rworld", writer.buffered());
|
||||
}
|
||||
|
||||
test "encodeWriter empty" {
|
||||
const testing = std.testing;
|
||||
var buf: [64]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try encodeWriter(&writer, "", .{ .bracketed = true });
|
||||
try testing.expectEqualStrings("\x1b[200~\x1b[201~", writer.buffered());
|
||||
writer = .fixed(&buf);
|
||||
try encodeWriter(&writer, "", .{ .bracketed = false });
|
||||
try testing.expectEqualStrings("", writer.buffered());
|
||||
}
|
||||
|
||||
test "encodeWriter chunks through a small writer buffer" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// A writer with a 4-byte staging buffer that drains into a list,
|
||||
// so the data is copied and encoded in several chunks.
|
||||
const Sink = struct {
|
||||
list: std.ArrayList(u8) = .empty,
|
||||
writer: std.Io.Writer,
|
||||
|
||||
fn drain(
|
||||
w: *std.Io.Writer,
|
||||
data: []const []const u8,
|
||||
splat: usize,
|
||||
) std.Io.Writer.Error!usize {
|
||||
const self: *@This() = @alignCast(@fieldParentPtr("writer", w));
|
||||
self.list.appendSlice(testing.allocator, w.buffered()) catch return error.WriteFailed;
|
||||
w.end = 0;
|
||||
var n: usize = 0;
|
||||
for (data[0 .. data.len - 1]) |slice| {
|
||||
self.list.appendSlice(testing.allocator, slice) catch return error.WriteFailed;
|
||||
n += slice.len;
|
||||
}
|
||||
for (0..splat) |_| {
|
||||
self.list.appendSlice(testing.allocator, data[data.len - 1]) catch return error.WriteFailed;
|
||||
}
|
||||
return n + splat * data[data.len - 1].len;
|
||||
}
|
||||
};
|
||||
|
||||
var staging: [4]u8 = undefined;
|
||||
var sink: Sink = .{ .writer = .{
|
||||
.buffer = &staging,
|
||||
.vtable = &.{ .drain = Sink.drain },
|
||||
} };
|
||||
defer sink.list.deinit(alloc);
|
||||
|
||||
const data = "line one\nline\x1btwo\nline three\n";
|
||||
try encodeWriter(&sink.writer, data, .{ .bracketed = true });
|
||||
try sink.writer.flush();
|
||||
try testing.expectEqualStrings(
|
||||
"\x1b[200~line one\nline two\nline three\n\x1b[201~",
|
||||
sink.list.items,
|
||||
);
|
||||
}
|
||||
|
||||
test "encodeWriter too small" {
|
||||
const testing = std.testing;
|
||||
var buf: [4]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try testing.expectError(
|
||||
error.WriteFailed,
|
||||
encodeWriter(&writer, "hello", .{ .bracketed = true }),
|
||||
);
|
||||
}
|
||||
|
||||
test max_frame_size {
|
||||
const testing = std.testing;
|
||||
const result = try encode(@as([]const u8, ""), .{ .bracketed = true });
|
||||
try testing.expectEqual(max_frame_size, result[0].len + result[2].len);
|
||||
}
|
||||
|
||||
test "encode bracketed" {
|
||||
const testing = std.testing;
|
||||
const result = try encode(
|
||||
|
||||
@@ -161,7 +161,7 @@ const vtable: Io.VTable = if (!supported) std.Io.failing.vtable.* else .{
|
||||
.progressParentFile = Io.failingProgressParentFile,
|
||||
|
||||
.random = Io.noRandom,
|
||||
.randomSecure = Io.failingRandomSecure,
|
||||
.randomSecure = randomSecure,
|
||||
|
||||
.now = Io.noNow,
|
||||
.clockResolution = Io.failingClockResolution,
|
||||
@@ -212,6 +212,35 @@ fn swapCancelProtection(
|
||||
|
||||
fn checkCancel(_: ?*anyopaque) Io.Cancelable!void {}
|
||||
|
||||
fn randomSecure(_: ?*anyopaque, buffer: []u8) Io.RandomSecureError!void {
|
||||
if (buffer.len == 0) return;
|
||||
|
||||
// The same sources as `std.Io.Threaded.randomSecure` minus
|
||||
// cancelation and the /dev/urandom fallback: arc4random_buf where
|
||||
// libc provides it (all the BSDs and Darwin, glibc 2.36+), otherwise
|
||||
// the getrandom syscall on Linux. Anything else has no entropy.
|
||||
if (builtin.link_libc and @TypeOf(posix.system.arc4random_buf) != void) {
|
||||
posix.system.arc4random_buf(buffer.ptr, buffer.len);
|
||||
return;
|
||||
}
|
||||
|
||||
if (builtin.os.tag == .linux) {
|
||||
const linux = std.os.linux;
|
||||
var i: usize = 0;
|
||||
while (i < buffer.len) {
|
||||
const rc = linux.getrandom(buffer[i..].ptr, buffer.len - i, 0);
|
||||
switch (linux.errno(rc)) {
|
||||
.SUCCESS => i += rc,
|
||||
.INTR => continue,
|
||||
else => return error.EntropyUnavailable,
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return error.EntropyUnavailable;
|
||||
}
|
||||
|
||||
fn closeFd(fd: posix.fd_t) void {
|
||||
// Never retry close on EINTR: POSIX leaves the fd state unspecified
|
||||
// and Linux always closes it, so retrying risks closing an unrelated
|
||||
@@ -965,6 +994,28 @@ test "Io.Mutex through TinyIo" {
|
||||
test_io.vtable.futexWaitUncancelable(test_io.userdata, &word, 1);
|
||||
}
|
||||
|
||||
test "randomSecure fills with fresh entropy" {
|
||||
if (comptime !supported) return error.SkipZigTest;
|
||||
const tio: TinyIo = .init;
|
||||
const test_io = tio.io();
|
||||
const testing = std.testing;
|
||||
|
||||
var a: [32]u8 = @splat(0);
|
||||
var b: [32]u8 = @splat(0);
|
||||
try test_io.randomSecure(&a);
|
||||
try test_io.randomSecure(&b);
|
||||
|
||||
// Non-zero and non-repeating. A zero fill is what `random` does
|
||||
// without a source, which would make every one-time password the
|
||||
// same; identical draws would mean the same thing.
|
||||
try testing.expect(!std.mem.allEqual(u8, &a, 0));
|
||||
try testing.expect(!std.mem.allEqual(u8, &b, 0));
|
||||
try testing.expect(!std.mem.eql(u8, &a, &b));
|
||||
|
||||
// Zero-length is a no-op.
|
||||
try test_io.randomSecure(a[0..0]);
|
||||
}
|
||||
|
||||
test "unused operations fail gracefully" {
|
||||
if (comptime !supported) return error.SkipZigTest;
|
||||
const tio: TinyIo = .init;
|
||||
|
||||
@@ -95,6 +95,10 @@ pub const TerminalStream = terminal.TerminalStream;
|
||||
pub const Stream = terminal.Stream;
|
||||
pub const StreamAction = terminal.StreamAction;
|
||||
pub const UnknownSequence = terminal.UnknownSequence;
|
||||
|
||||
pub const Paste = terminal.Paste;
|
||||
pub const PasteSource = terminal.PasteSource;
|
||||
pub const PasteError = terminal.PasteError;
|
||||
pub const Cursor = Screen.Cursor;
|
||||
pub const CursorStyle = Screen.CursorStyle;
|
||||
pub const CursorStyleReq = terminal.CursorStyle;
|
||||
@@ -129,8 +133,11 @@ pub const input = struct {
|
||||
// Paste-related APIs
|
||||
pub const PasteError = paste.Error;
|
||||
pub const PasteOptions = paste.Options;
|
||||
pub const max_paste_frame_size = paste.max_frame_size;
|
||||
pub const isSafePaste = paste.isSafe;
|
||||
pub const isSafePasteWith = paste.isSafeWith;
|
||||
pub const encodePaste = paste.encode;
|
||||
pub const encodePasteWriter = paste.encodeWriter;
|
||||
|
||||
// Key encoding
|
||||
pub const Key = key.Key;
|
||||
@@ -208,6 +215,7 @@ comptime {
|
||||
@export(&c.focus_encode, .{ .name = "ghostty_focus_encode" });
|
||||
@export(&c.paste_is_safe, .{ .name = "ghostty_paste_is_safe" });
|
||||
@export(&c.paste_encode, .{ .name = "ghostty_paste_encode" });
|
||||
@export(&c.terminal_paste, .{ .name = "ghostty_terminal_paste" });
|
||||
@export(&c.mouse_event_new, .{ .name = "ghostty_mouse_event_new" });
|
||||
@export(&c.mouse_event_free, .{ .name = "ghostty_mouse_event_free" });
|
||||
@export(&c.mouse_event_set_action, .{ .name = "ghostty_mouse_event_set_action" });
|
||||
|
||||
@@ -44,6 +44,23 @@ pub const Writer = extern struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// C: GhosttyMimeReaderFn
|
||||
pub const MimeReaderFn = *const fn (
|
||||
userdata: ?*anyopaque,
|
||||
mime: lib.String,
|
||||
writer: Writer,
|
||||
) callconv(lib.calling_conv) bool;
|
||||
|
||||
/// C: GhosttyMimeReader
|
||||
pub const MimeReader = extern struct {
|
||||
read: ?MimeReaderFn = null,
|
||||
userdata: ?*anyopaque = null,
|
||||
|
||||
pub fn valid(self: MimeReader) bool {
|
||||
return self.read != null;
|
||||
}
|
||||
};
|
||||
|
||||
/// Adapts a `GhosttyReader` to `std.Io.Reader`.
|
||||
///
|
||||
/// The adapter must have a stable address while `interface` is in use. Its
|
||||
@@ -418,6 +435,8 @@ test "C reader and writer layouts keep callback first" {
|
||||
try std.testing.expectEqual(@sizeOf(?ReaderFn) + @sizeOf(?*anyopaque), @sizeOf(Reader));
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 0), @offsetOf(Writer, "write"));
|
||||
try std.testing.expectEqual(@as(usize, 0), @offsetOf(MimeReader, "read"));
|
||||
try std.testing.expectEqual(@sizeOf(?MimeReaderFn), @offsetOf(MimeReader, "userdata"));
|
||||
try std.testing.expectEqual(@sizeOf(?WriterFn), @offsetOf(Writer, "userdata"));
|
||||
try std.testing.expectEqual(@sizeOf(?WriterFn) + @sizeOf(?*anyopaque), @sizeOf(Writer));
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ pub const mouse_encoder_encode = mouse_encode.encode;
|
||||
|
||||
pub const paste_is_safe = paste.is_safe;
|
||||
pub const paste_encode = paste.encode;
|
||||
pub const terminal_paste = paste.terminal_paste;
|
||||
|
||||
pub const alloc_alloc = allocator.alloc;
|
||||
pub const alloc_free = allocator.free;
|
||||
|
||||
@@ -1,8 +1,130 @@
|
||||
const std = @import("std");
|
||||
const lib = @import("../lib.zig");
|
||||
const paste = @import("../../input/paste.zig");
|
||||
const terminal_paste_pkg = @import("../paste.zig");
|
||||
const clipboard = @import("../clipboard.zig");
|
||||
const io = @import("io.zig");
|
||||
const terminal_c = @import("terminal.zig");
|
||||
const Terminal = terminal_c.Terminal;
|
||||
const ClipboardContent = terminal_c.ClipboardContent;
|
||||
const ClipboardRead = terminal_c.ClipboardRead;
|
||||
const ClipboardReadReply = terminal_c.ClipboardReadReply;
|
||||
const Result = @import("result.zig").Result;
|
||||
|
||||
/// Why a paste happened. The flat C form of the Zig tagged union
|
||||
/// (terminal.paste.Source); the location rides alongside in
|
||||
/// GhosttyPaste and only applies to a clipboard paste.
|
||||
///
|
||||
/// C: GhosttyPasteSource
|
||||
pub const Source = lib.Enum(lib.target, &.{
|
||||
"clipboard",
|
||||
"text",
|
||||
});
|
||||
|
||||
/// A paste of clipboard contents into the terminal. Sized struct.
|
||||
///
|
||||
/// C: GhosttyPaste
|
||||
pub const Request = extern struct {
|
||||
size: usize = @sizeOf(Request),
|
||||
location: clipboard.Location,
|
||||
source: Source,
|
||||
mimes: ?[*]const lib.String,
|
||||
mimes_len: usize,
|
||||
reader: io.MimeReader,
|
||||
allow_unsafe: bool,
|
||||
};
|
||||
|
||||
pub fn terminal_paste(
|
||||
terminal_: Terminal,
|
||||
req_: ?*const Request,
|
||||
out_written: ?*bool,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
const wrapper = terminal_ orelse return .invalid_value;
|
||||
const req = req_ orelse return .invalid_value;
|
||||
|
||||
// Every field is required; a smaller size is a caller from a
|
||||
// different ABI version than any this struct has had.
|
||||
if (req.size < @sizeOf(Request)) return .invalid_value;
|
||||
|
||||
// The handler always has a write_pty trampoline that no-ops without
|
||||
// a C callback, so the "nothing can be written" check is ours.
|
||||
if (wrapper.effects.write_pty == null) return .invalid_value;
|
||||
|
||||
const c_mimes: []const lib.String = if (req.mimes) |ptr|
|
||||
ptr[0..req.mimes_len]
|
||||
else
|
||||
&.{};
|
||||
|
||||
// Only a paste with something to read needs a reader.
|
||||
if (c_mimes.len > 0 and !req.reader.valid()) return .invalid_value;
|
||||
|
||||
// A paste carries a handful of representations, so keep the common
|
||||
// case allocation-free.
|
||||
var sfa = std.heap.stackFallback(256, wrapper.terminal.gpa());
|
||||
const alloc = sfa.get();
|
||||
const mimes = alloc.alloc([]const u8, c_mimes.len) catch return .out_of_memory;
|
||||
defer alloc.free(mimes);
|
||||
for (mimes, c_mimes) |*mime, c_mime| mime.* = c_mime.ptr[0..c_mime.len];
|
||||
|
||||
const written = wrapper.stream.handler.paste(.{
|
||||
.source = switch (req.source) {
|
||||
.clipboard => .{ .clipboard = req.location },
|
||||
.text => .text,
|
||||
},
|
||||
.contents = .{ .reader = .{
|
||||
.mimes = mimes,
|
||||
.read = .{ .ctx = @constCast(req), .read_fn = &readTrampoline },
|
||||
} },
|
||||
.allow_unsafe = req.allow_unsafe,
|
||||
}) catch |err| return switch (err) {
|
||||
error.UnsafePaste => .rejected,
|
||||
error.NoWritePty => .invalid_value,
|
||||
error.OutOfMemory => .out_of_memory,
|
||||
error.ReadFailed,
|
||||
error.EntropyUnavailable,
|
||||
error.Canceled,
|
||||
=> .io_error,
|
||||
};
|
||||
if (out_written) |ptr| ptr.* = written;
|
||||
return .success;
|
||||
}
|
||||
|
||||
/// The sink handed to the C read callback: a GhosttyWriter over the
|
||||
/// Zig sink, remembering whether the sink itself failed so that can be
|
||||
/// told apart from the callback failing to read.
|
||||
const Sink = struct {
|
||||
writer: *std.Io.Writer,
|
||||
write_failed: bool = false,
|
||||
|
||||
fn write(
|
||||
userdata: ?*anyopaque,
|
||||
data: [*]const u8,
|
||||
len: usize,
|
||||
) callconv(lib.calling_conv) bool {
|
||||
const self: *Sink = @ptrCast(@alignCast(userdata.?));
|
||||
self.writer.writeAll(data[0..len]) catch {
|
||||
self.write_failed = true;
|
||||
return false;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
fn readTrampoline(
|
||||
ctx: ?*anyopaque,
|
||||
mime: []const u8,
|
||||
writer: *std.Io.Writer,
|
||||
) clipboard.MimeReader.Error!void {
|
||||
const req: *const Request = @ptrCast(@alignCast(ctx.?));
|
||||
var sink: Sink = .{ .writer = writer };
|
||||
if (!req.reader.read.?(req.reader.userdata, .init(mime), .{
|
||||
.write = &Sink.write,
|
||||
.userdata = &sink,
|
||||
})) {
|
||||
return if (sink.write_failed) error.WriteFailed else error.ReadFailed;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_safe(data: ?[*]const u8, len: usize) callconv(lib.calling_conv) bool {
|
||||
const slice: []const u8 = if (data) |v| v[0..len] else &.{};
|
||||
return paste.isSafe(slice);
|
||||
@@ -127,3 +249,273 @@ test "is_safe with null empty data" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(is_safe(null, 0));
|
||||
}
|
||||
|
||||
/// Capture state for the terminal_paste tests: every pty write and the
|
||||
/// clipboard reads that follow a paste event.
|
||||
const TerminalPasteCapture = struct {
|
||||
var written: [1024]u8 = undefined;
|
||||
var written_len: usize = 0;
|
||||
var write_count: usize = 0;
|
||||
var read_count: usize = 0;
|
||||
var last_read_granted: bool = false;
|
||||
|
||||
fn reset() void {
|
||||
written_len = 0;
|
||||
write_count = 0;
|
||||
read_count = 0;
|
||||
last_read_granted = false;
|
||||
}
|
||||
|
||||
fn writePty(_: Terminal, _: ?*anyopaque, ptr: [*]const u8, len: usize) callconv(lib.calling_conv) void {
|
||||
@memcpy(written[written_len..][0..len], ptr[0..len]);
|
||||
written_len += len;
|
||||
write_count += 1;
|
||||
}
|
||||
|
||||
fn clipboardRead(_: Terminal, _: ?*anyopaque, request: *const ClipboardRead) callconv(lib.calling_conv) void {
|
||||
read_count += 1;
|
||||
last_read_granted = request.granted;
|
||||
const contents = [_]ClipboardContent{.{
|
||||
.mime = .init(@as([]const u8, "text/plain")),
|
||||
.data = .init(@as([]const u8, "Ghostty")),
|
||||
}};
|
||||
request.reply(request, &.{
|
||||
.size = @sizeOf(ClipboardReadReply),
|
||||
.result = .success,
|
||||
.contents = &contents,
|
||||
.contents_len = contents.len,
|
||||
.available = null,
|
||||
.available_len = 0,
|
||||
.remember = false,
|
||||
});
|
||||
}
|
||||
|
||||
fn writtenSlice() []const u8 {
|
||||
return written[0..written_len];
|
||||
}
|
||||
|
||||
/// The representations a test paste serves: the MIME list for the
|
||||
/// request and the data the read callback streams, in pieces.
|
||||
const Contents = struct {
|
||||
mimes: [2]lib.String = undefined,
|
||||
data: [2][]const u8 = undefined,
|
||||
len: usize = 0,
|
||||
reads: [2]usize = @splat(0),
|
||||
fail: bool = false,
|
||||
|
||||
fn init(entries: []const struct { []const u8, []const u8 }) Contents {
|
||||
var self: Contents = .{};
|
||||
for (entries) |entry| {
|
||||
self.mimes[self.len] = .init(entry[0]);
|
||||
self.data[self.len] = entry[1];
|
||||
self.len += 1;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
fn request(self: *Contents) Request {
|
||||
return .{
|
||||
.location = .standard,
|
||||
.source = .clipboard,
|
||||
.mimes = &self.mimes,
|
||||
.mimes_len = self.len,
|
||||
.reader = .{ .read = &read, .userdata = self },
|
||||
.allow_unsafe = false,
|
||||
};
|
||||
}
|
||||
|
||||
fn read(userdata: ?*anyopaque, mime: lib.String, writer: io.Writer) callconv(lib.calling_conv) bool {
|
||||
const self: *Contents = @ptrCast(@alignCast(userdata.?));
|
||||
// The mime is the exact string from the request's list, so
|
||||
// identifying it by pointer works.
|
||||
const index: usize = for (self.mimes[0..self.len], 0..) |m, i| {
|
||||
if (m.ptr == mime.ptr and m.len == mime.len) break i;
|
||||
} else return false;
|
||||
self.reads[index] += 1;
|
||||
if (self.fail) return false;
|
||||
const data = self.data[index];
|
||||
var offset: usize = 0;
|
||||
while (offset < data.len) {
|
||||
const n = @min(3, data.len - offset);
|
||||
if (!writer.write.?(writer.userdata, data[offset..].ptr, n)) return false;
|
||||
offset += n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
test "terminal_paste null handling" {
|
||||
const testing = std.testing;
|
||||
const S = TerminalPasteCapture;
|
||||
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(&lib.alloc.test_allocator, &t, 80, 24));
|
||||
defer terminal_c.free(t);
|
||||
try testing.expectEqual(Result.success, terminal_c.set(t, .write_pty, @ptrCast(&S.writePty)));
|
||||
|
||||
var written: bool = true;
|
||||
var contents: S.Contents = .init(&.{.{ "text/plain", "hello" }});
|
||||
const req = contents.request();
|
||||
try testing.expectEqual(Result.invalid_value, terminal_paste(null, &req, &written));
|
||||
try testing.expectEqual(Result.invalid_value, terminal_paste(t, null, &written));
|
||||
try testing.expect(written);
|
||||
|
||||
// A size smaller than the struct is rejected.
|
||||
var small = req;
|
||||
small.size = @sizeOf(usize);
|
||||
try testing.expectEqual(Result.invalid_value, terminal_paste(t, &small, &written));
|
||||
|
||||
// MIME types without a reader are rejected; none at all is fine.
|
||||
var unreadable = req;
|
||||
unreadable.reader.read = null;
|
||||
try testing.expectEqual(Result.invalid_value, terminal_paste(t, &unreadable, &written));
|
||||
unreadable.mimes = null;
|
||||
unreadable.mimes_len = 0;
|
||||
try testing.expectEqual(Result.success, terminal_paste(t, &unreadable, &written));
|
||||
try testing.expect(!written);
|
||||
}
|
||||
|
||||
test "terminal_paste without write_pty is invalid" {
|
||||
const testing = std.testing;
|
||||
const S = TerminalPasteCapture;
|
||||
S.reset();
|
||||
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(&lib.alloc.test_allocator, &t, 80, 24));
|
||||
defer terminal_c.free(t);
|
||||
|
||||
var contents: S.Contents = .init(&.{.{ "text/plain", "hello" }});
|
||||
const req = contents.request();
|
||||
try testing.expectEqual(Result.invalid_value, terminal_paste(t, &req, null));
|
||||
}
|
||||
|
||||
test "terminal_paste text and unsafe" {
|
||||
const testing = std.testing;
|
||||
const S = TerminalPasteCapture;
|
||||
S.reset();
|
||||
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(&lib.alloc.test_allocator, &t, 80, 24));
|
||||
defer terminal_c.free(t);
|
||||
try testing.expectEqual(Result.success, terminal_c.set(t, .write_pty, @ptrCast(&S.writePty)));
|
||||
|
||||
// Plain text, NULL out_written pointer is fine. The text is read
|
||||
// once, the image never.
|
||||
var contents: S.Contents = .init(&.{
|
||||
.{ "image/png", "\x89PNG" },
|
||||
.{ "text/plain", "hel\x1blo" },
|
||||
});
|
||||
const req = contents.request();
|
||||
try testing.expectEqual(Result.success, terminal_paste(t, &req, null));
|
||||
try testing.expectEqualStrings("hel lo", S.writtenSlice());
|
||||
try testing.expectEqual(@as(usize, 1), S.write_count);
|
||||
try testing.expectEqual(@as(usize, 0), contents.reads[0]);
|
||||
try testing.expectEqual(@as(usize, 1), contents.reads[1]);
|
||||
|
||||
// Unsafe is refused with nothing written, then allowed.
|
||||
S.reset();
|
||||
var written: bool = false;
|
||||
var unsafe_contents: S.Contents = .init(&.{.{ "text/plain", "rm -rf /\n" }});
|
||||
var unsafe = unsafe_contents.request();
|
||||
try testing.expectEqual(Result.rejected, terminal_paste(t, &unsafe, &written));
|
||||
try testing.expectEqual(@as(usize, 0), S.write_count);
|
||||
try testing.expect(!written);
|
||||
try testing.expectEqual(@as(usize, 1), unsafe_contents.reads[0]);
|
||||
|
||||
unsafe.allow_unsafe = true;
|
||||
try testing.expectEqual(Result.success, terminal_paste(t, &unsafe, &written));
|
||||
try testing.expect(written);
|
||||
try testing.expectEqualStrings("rm -rf /\r", S.writtenSlice());
|
||||
try testing.expectEqual(@as(usize, 2), unsafe_contents.reads[0]);
|
||||
|
||||
// Bracketed paste mode frames the text through the real mode path.
|
||||
S.reset();
|
||||
const decset = "\x1b[?2004h";
|
||||
terminal_c.vt_write(t, decset, decset.len);
|
||||
try testing.expectEqual(Result.success, terminal_paste(t, &req, &written));
|
||||
try testing.expect(written);
|
||||
try testing.expectEqualStrings("\x1b[200~hel lo\x1b[201~", S.writtenSlice());
|
||||
|
||||
// No text representation writes nothing and reads nothing.
|
||||
S.reset();
|
||||
var image: S.Contents = .init(&.{.{ "image/png", "\x89PNG" }});
|
||||
const image_req = image.request();
|
||||
try testing.expectEqual(Result.success, terminal_paste(t, &image_req, &written));
|
||||
try testing.expect(!written);
|
||||
try testing.expectEqual(@as(usize, 0), S.write_count);
|
||||
try testing.expectEqual(@as(usize, 0), image.reads[0]);
|
||||
|
||||
// A failing reader is an I/O error.
|
||||
S.reset();
|
||||
contents.fail = true;
|
||||
try testing.expectEqual(Result.io_error, terminal_paste(t, &req, &written));
|
||||
try testing.expectEqual(@as(usize, 0), S.write_count);
|
||||
}
|
||||
|
||||
test "terminal_paste event" {
|
||||
const testing = std.testing;
|
||||
const S = TerminalPasteCapture;
|
||||
S.reset();
|
||||
|
||||
var t: Terminal = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(&lib.alloc.test_allocator, &t, 80, 24));
|
||||
defer terminal_c.free(t);
|
||||
try testing.expectEqual(Result.success, terminal_c.set(t, .write_pty, @ptrCast(&S.writePty)));
|
||||
t.?.terminal.modes.set(.kitty_paste_events, true);
|
||||
|
||||
// Without a clipboard_read callback the paste stays text.
|
||||
var written: bool = false;
|
||||
var contents: S.Contents = .init(&.{
|
||||
.{ "text/plain", "secret" },
|
||||
.{ "image/png", "\x89PNG" },
|
||||
});
|
||||
var req = contents.request();
|
||||
req.location = .primary;
|
||||
try testing.expectEqual(Result.success, terminal_paste(t, &req, &written));
|
||||
try testing.expect(written);
|
||||
try testing.expectEqualStrings("secret", S.writtenSlice());
|
||||
|
||||
// With one, an event is sent listing every MIME type and no data
|
||||
// is read, let alone written.
|
||||
S.reset();
|
||||
contents.reads = @splat(0);
|
||||
try testing.expectEqual(Result.success, terminal_c.set(t, .clipboard_read, @ptrCast(&S.clipboardRead)));
|
||||
try testing.expectEqual(Result.success, terminal_paste(t, &req, &written));
|
||||
try testing.expect(written);
|
||||
try testing.expectEqual(@as(usize, 1), S.write_count);
|
||||
try testing.expectEqual(@as(usize, 3), std.mem.count(u8, S.writtenSlice(), "\x1b]5522;"));
|
||||
try testing.expect(std.mem.startsWith(u8, S.writtenSlice(), "\x1b]5522;type=read:status=OK:loc=primary:pw="));
|
||||
try testing.expect(std.mem.indexOf(u8, S.writtenSlice(), "secret") == null);
|
||||
try testing.expect(std.mem.indexOf(u8, S.writtenSlice(), ";dGV4dC9wbGFpbiBpbWFnZS9wbmcK\x1b\\") != null);
|
||||
try testing.expectEqual(@as(usize, 0), contents.reads[0]);
|
||||
try testing.expectEqual(@as(usize, 0), contents.reads[1]);
|
||||
|
||||
// The program's read with the event password is granted once.
|
||||
const ok_prefix = "\x1b]5522;type=read:status=OK:loc=primary:pw=";
|
||||
const pw_end = std.mem.indexOfPos(u8, S.writtenSlice(), ok_prefix.len, "\x1b\\").?;
|
||||
var read_buf: [256]u8 = undefined;
|
||||
const read = try std.fmt.bufPrint(
|
||||
&read_buf,
|
||||
"\x1b]5522;type=read:pw={s}:name=UGFzdGUgZXZlbnQ=;dGV4dC9wbGFpbg==\x1b\\",
|
||||
.{S.writtenSlice()[ok_prefix.len..pw_end]},
|
||||
);
|
||||
S.reset();
|
||||
terminal_c.vt_write(t, read.ptr, read.len);
|
||||
try testing.expectEqual(@as(usize, 1), S.read_count);
|
||||
try testing.expect(S.last_read_granted);
|
||||
try testing.expect(std.mem.indexOf(u8, S.writtenSlice(), ";R2hvc3R0eQ==\x1b\\") != null);
|
||||
|
||||
S.reset();
|
||||
terminal_c.vt_write(t, read.ptr, read.len);
|
||||
try testing.expectEqual(@as(usize, 1), S.read_count);
|
||||
try testing.expect(!S.last_read_granted);
|
||||
|
||||
// Text sources never become events.
|
||||
S.reset();
|
||||
var ime = req;
|
||||
ime.source = .text;
|
||||
try testing.expectEqual(Result.success, terminal_paste(t, &ime, &written));
|
||||
try testing.expect(written);
|
||||
try testing.expectEqualStrings("secret", S.writtenSlice());
|
||||
}
|
||||
|
||||
@@ -7,4 +7,5 @@ pub const Result = enum(c_int) {
|
||||
no_value = -4,
|
||||
io_error = -5,
|
||||
limit_exceeded = -6,
|
||||
rejected = -7,
|
||||
};
|
||||
|
||||
@@ -23,6 +23,13 @@ pub const DecodePngFn = *const fn (
|
||||
*Image,
|
||||
) callconv(lib.calling_conv) bool;
|
||||
|
||||
/// C: GhosttySysRandomSecureFn
|
||||
pub const RandomSecureFn = *const fn (
|
||||
?*anyopaque,
|
||||
[*]u8,
|
||||
usize,
|
||||
) callconv(lib.calling_conv) bool;
|
||||
|
||||
/// C: GhosttySysLogLevel
|
||||
pub const LogLevel = enum(c_int) {
|
||||
@"error" = 0,
|
||||
@@ -55,12 +62,14 @@ pub const Option = enum(c_int) {
|
||||
userdata = 0,
|
||||
decode_png = 1,
|
||||
log = 2,
|
||||
random_secure = 3,
|
||||
|
||||
pub fn InType(comptime self: Option) type {
|
||||
return switch (self) {
|
||||
.userdata => ?*const anyopaque,
|
||||
.decode_png => ?DecodePngFn,
|
||||
.log => ?LogFn,
|
||||
.random_secure => ?RandomSecureFn,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -71,6 +80,7 @@ const Global = struct {
|
||||
userdata: ?*anyopaque = null,
|
||||
decode_png: ?DecodePngFn = null,
|
||||
log: ?LogFn = null,
|
||||
random_secure: ?RandomSecureFn = null,
|
||||
};
|
||||
|
||||
/// Global state for the C sys interface.
|
||||
@@ -98,6 +108,12 @@ fn decodePngWrapper(
|
||||
};
|
||||
}
|
||||
|
||||
/// Zig-compatible wrapper that calls through to the stored C callback.
|
||||
fn randomSecureWrapper(buffer: []u8) terminal_sys.RandomSecureError!void {
|
||||
const func = global.random_secure orelse return error.EntropyUnavailable;
|
||||
if (!func(global.userdata, buffer.ptr, buffer.len)) return error.EntropyUnavailable;
|
||||
}
|
||||
|
||||
pub fn set(
|
||||
option: Option,
|
||||
value: ?*const anyopaque,
|
||||
@@ -127,6 +143,10 @@ fn setTyped(
|
||||
terminal_sys.decode_png = if (value != null) &decodePngWrapper else null;
|
||||
},
|
||||
.log => global.log = value,
|
||||
.random_secure => {
|
||||
global.random_secure = value;
|
||||
terminal_sys.random_secure = if (value != null) &randomSecureWrapper else null;
|
||||
},
|
||||
}
|
||||
return .success;
|
||||
}
|
||||
|
||||
@@ -353,7 +353,7 @@ const Effects = struct {
|
||||
};
|
||||
};
|
||||
|
||||
fn writePtyTrampoline(handler: *Handler, data: [:0]const u8) void {
|
||||
fn writePtyTrampoline(handler: *Handler, data: []const u8) void {
|
||||
const wrapper = TerminalWrapper.fromHandler(handler);
|
||||
const func = wrapper.effects.write_pty orelse return;
|
||||
func(@ptrCast(wrapper), wrapper.effects.userdata, data.ptr, data.len);
|
||||
|
||||
@@ -37,6 +37,7 @@ const kitty_graphics = @import("kitty_graphics.zig");
|
||||
const mouse_encode = @import("mouse_encode.zig");
|
||||
const mouse_event = @import("mouse_event.zig");
|
||||
const osc = @import("osc.zig");
|
||||
const paste = @import("paste.zig");
|
||||
const render = @import("render.zig");
|
||||
const result = @import("result.zig");
|
||||
const row = @import("row.zig");
|
||||
@@ -190,8 +191,10 @@ const type_decls = [_]TypeDecl{
|
||||
.initStruct("GhosttyFormatterTerminalOptions", formatter.TerminalOptions),
|
||||
.initStruct("GhosttyGridRef", grid_ref.CGridRef),
|
||||
.initStruct("GhosttyKittyGraphicsPlacementRenderInfo", kitty_graphics.PlacementRenderInfo),
|
||||
.initStruct("GhosttyMimeReader", io.MimeReader),
|
||||
.initStruct("GhosttyMouseEncoderSize", mouse_encode.Size),
|
||||
.initStruct("GhosttyMousePosition", mouse_event.Position),
|
||||
.initStruct("GhosttyPaste", paste.Request),
|
||||
.initTaggedStruct("GhosttyPoint", point.Point.C, "tag", "value", .generated),
|
||||
.initStruct("GhosttyPointCoordinate", point.Coordinate),
|
||||
.initUnion("GhosttyPointValue", point.Point.CValue, point.Point.C),
|
||||
@@ -272,6 +275,7 @@ const type_decls = [_]TypeDecl{
|
||||
"GHOSTTY_OSC_COMMAND_",
|
||||
"TYPE_MAX_VALUE",
|
||||
),
|
||||
.initEnum("GhosttyPasteSource", paste.Source, "GHOSTTY_PASTE_SOURCE_"),
|
||||
.initEnum("GhosttyPointTag", point.Tag, "GHOSTTY_POINT_TAG_"),
|
||||
.initEnum("GhosttyRenderStateCursorVisualStyle", render.CursorVisualStyle, "GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_"),
|
||||
.initEnum("GhosttyRenderStateData", render.Data, "GHOSTTY_RENDER_STATE_DATA_"),
|
||||
|
||||
@@ -32,6 +32,43 @@ pub const Content = struct {
|
||||
data: []const u8,
|
||||
};
|
||||
|
||||
/// Requests content of a specific mime-type. For now this is used for
|
||||
/// on-demand clipboard access since content can be large (particularly
|
||||
/// non-text content), but it is generic so that this could handle other
|
||||
/// mime-typed sources in the future like maybe drag-and-drop.
|
||||
///
|
||||
/// C: GhosttyMimeReader
|
||||
pub const MimeReader = struct {
|
||||
/// Passed through to `read_fn`.
|
||||
ctx: ?*anyopaque = null,
|
||||
|
||||
/// Write all the data of the representation named by `mime` to
|
||||
/// `sink`, in as many writes as is convenient. The mime and sink
|
||||
/// are borrowed, only valid for the duration of the call, and
|
||||
/// nothing written to the sink is retained, so the data may be
|
||||
/// borrowed from anywhere. Return error.ReadFailed if the data
|
||||
/// can't be produced and propagate error.WriteFailed from the
|
||||
/// sink.
|
||||
read_fn: *const fn (
|
||||
ctx: ?*anyopaque,
|
||||
mime: []const u8,
|
||||
sink: *std.Io.Writer,
|
||||
) Error!void,
|
||||
|
||||
pub const Error = error{
|
||||
/// The data could not be read.
|
||||
ReadFailed,
|
||||
} || std.Io.Writer.Error;
|
||||
|
||||
pub fn read(
|
||||
self: MimeReader,
|
||||
mime: []const u8,
|
||||
sink: *std.Io.Writer,
|
||||
) Error!void {
|
||||
return self.read_fn(self.ctx, mime, sink);
|
||||
}
|
||||
};
|
||||
|
||||
/// One atomic clipboard write.
|
||||
///
|
||||
/// Contents are borrowed and only valid for the duration of a clipboard write
|
||||
|
||||
@@ -57,8 +57,10 @@ pub const max_write_aliases = write.max_write_aliases;
|
||||
|
||||
pub const Response = response.Response;
|
||||
pub const ReadSuccess = response.ReadSuccess;
|
||||
pub const PasteEvent = response.PasteEvent;
|
||||
pub const read_chunk_size = response.read_chunk_size;
|
||||
pub const max_read_mimes = response.max_read_mimes;
|
||||
pub const max_listing_mimes = response.max_listing_mimes;
|
||||
pub const targets_mime = response.targets_mime;
|
||||
|
||||
pub const Grants = grants.Grants;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const clipboard_command = @import("clipboard_command.zig");
|
||||
const sys = @import("../sys.zig");
|
||||
|
||||
const max_pw_len = clipboard_command.max_pw_len;
|
||||
|
||||
@@ -105,13 +106,31 @@ pub const Grants = struct {
|
||||
/// The length of a one-time password generated for paste events.
|
||||
pub const otp_len = 22;
|
||||
|
||||
/// Generate a one-time password for a paste event. The alphabet matches
|
||||
/// kitty (alphanumeric without easily-confused characters), but the spec
|
||||
/// doesn't demand this.
|
||||
pub fn generateOtp(random: std.Random) [otp_len]u8 {
|
||||
const alphabet = "23456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
|
||||
/// The one-time password alphabet. This matches kitty (alphanumeric
|
||||
/// without easily-confused characters), but the spec doesn't demand
|
||||
/// this.
|
||||
pub const otp_alphabet = "23456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
|
||||
|
||||
/// Generate a one-time password for a paste event.
|
||||
///
|
||||
/// The password is a secret: a program that learns it can read the
|
||||
/// clipboard without a prompt. Entropy comes from `sys.random_secure`
|
||||
/// if set, otherwise from the Io; see `sys.randomSecure`.
|
||||
pub fn generateOtp(io: std.Io) std.Io.RandomSecureError![otp_len]u8 {
|
||||
var result: [otp_len]u8 = undefined;
|
||||
for (&result) |*c| c.* = alphabet[random.uintLessThan(usize, alphabet.len)];
|
||||
var len: usize = 0;
|
||||
while (len < result.len) {
|
||||
var raw: [2 * otp_len]u8 = undefined;
|
||||
try sys.randomSecure(io, &raw);
|
||||
const limit = (std.math.maxInt(u8) + 1) / otp_alphabet.len * otp_alphabet.len;
|
||||
for (raw) |byte| {
|
||||
if (byte >= limit) continue;
|
||||
result[len] = otp_alphabet[byte % otp_alphabet.len];
|
||||
len += 1;
|
||||
if (len == result.len) break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -182,3 +201,38 @@ test "grants: capacity evicts the oldest" {
|
||||
const newest = try std.fmt.bufPrint(&buf, "pw{}", .{Grants.max_entries});
|
||||
try testing.expect(grants.use(alloc, newest, .read));
|
||||
}
|
||||
|
||||
test "generateOtp: length and alphabet with a real Io" {
|
||||
const testing = std.testing;
|
||||
|
||||
const otp = try generateOtp(testing.io);
|
||||
try testing.expectEqual(otp_len, otp.len);
|
||||
for (otp) |c| try testing.expect(std.mem.indexOfScalar(u8, otp_alphabet, c) != null);
|
||||
|
||||
// Two passwords don't collide (a repeat would mean no entropy).
|
||||
const other = try generateOtp(testing.io);
|
||||
try testing.expect(!std.mem.eql(u8, &otp, &other));
|
||||
}
|
||||
|
||||
test "generateOtp: no entropy is an error, never a weak password" {
|
||||
const testing = std.testing;
|
||||
try testing.expectError(error.EntropyUnavailable, generateOtp(std.Io.failing));
|
||||
}
|
||||
|
||||
test "generateOtp: sys override supplies entropy without an Io source" {
|
||||
const testing = std.testing;
|
||||
const S = struct {
|
||||
var counter: u8 = 0;
|
||||
fn fill(buffer: []u8) sys.RandomSecureError!void {
|
||||
for (buffer) |*b| {
|
||||
b.* = counter;
|
||||
counter +%= 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
sys.random_secure = &S.fill;
|
||||
defer sys.random_secure = null;
|
||||
|
||||
const otp = try generateOtp(std.Io.failing);
|
||||
for (otp) |c| try testing.expect(std.mem.indexOfScalar(u8, otp_alphabet, c) != null);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ pub const max_read_mimes = 4;
|
||||
/// The special MIME type that requests the list of available types.
|
||||
pub const targets_mime = ".";
|
||||
|
||||
/// Maximum MIME types reported in a paste event's targets listing.
|
||||
pub const max_listing_mimes = 16;
|
||||
|
||||
/// A single response packet.
|
||||
pub const Response = struct {
|
||||
op: Operation,
|
||||
@@ -182,6 +185,36 @@ pub const ReadSuccess = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// An unsolicited Kitty paste event (mode 5522): a read response that
|
||||
/// lists the clipboard's available MIME types and carries the one-time
|
||||
/// password the program uses for its follow-up read.
|
||||
pub const PasteEvent = struct {
|
||||
/// True if the paste came from the primary selection, reported as
|
||||
/// `loc=primary` on the OK packet.
|
||||
primary: bool = false,
|
||||
|
||||
/// The one-time password, echoed in every packet.
|
||||
pw: []const u8,
|
||||
|
||||
/// The MIME types available on the clipboard.
|
||||
available: []const []const u8,
|
||||
|
||||
terminator: Terminator = .st,
|
||||
|
||||
pub fn encode(
|
||||
self: *const PasteEvent,
|
||||
writer: *std.Io.Writer,
|
||||
) std.Io.Writer.Error!void {
|
||||
try (ReadSuccess{
|
||||
.primary = self.primary,
|
||||
.pw = self.pw,
|
||||
.list = true,
|
||||
.available = self.available,
|
||||
.terminator = self.terminator,
|
||||
}).encode(writer);
|
||||
}
|
||||
};
|
||||
|
||||
test "response: basic status packet" {
|
||||
const testing = std.testing;
|
||||
|
||||
@@ -383,3 +416,54 @@ test "read success: paste event carries pw in every packet" {
|
||||
writer.buffered(),
|
||||
);
|
||||
}
|
||||
|
||||
test "paste event: pw in every packet, listing of every type" {
|
||||
const testing = std.testing;
|
||||
|
||||
var buf: [512]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try (PasteEvent{
|
||||
.pw = "otp",
|
||||
.available = &.{ "text/plain", "image/png" },
|
||||
}).encode(&writer);
|
||||
// Payload is base64 of "text/plain image/png\n".
|
||||
try testing.expectEqualStrings(
|
||||
"\x1b]5522;type=read:status=OK:pw=b3Rw\x1b\\" ++
|
||||
"\x1b]5522;type=read:status=DATA:mime=Lg==:pw=b3Rw;dGV4dC9wbGFpbiBpbWFnZS9wbmcK\x1b\\" ++
|
||||
"\x1b]5522;type=read:status=DONE:pw=b3Rw\x1b\\",
|
||||
writer.buffered(),
|
||||
);
|
||||
}
|
||||
|
||||
test "paste event: primary is reported only on the OK packet" {
|
||||
const testing = std.testing;
|
||||
|
||||
var buf: [512]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try (PasteEvent{
|
||||
.primary = true,
|
||||
.pw = "otp",
|
||||
.available = &.{"text/plain"},
|
||||
.terminator = .bel,
|
||||
}).encode(&writer);
|
||||
try testing.expectEqualStrings(
|
||||
"\x1b]5522;type=read:status=OK:loc=primary:pw=b3Rw\x07" ++
|
||||
"\x1b]5522;type=read:status=DATA:mime=Lg==:pw=b3Rw;dGV4dC9wbGFpbgo=\x07" ++
|
||||
"\x1b]5522;type=read:status=DONE:pw=b3Rw\x07",
|
||||
writer.buffered(),
|
||||
);
|
||||
}
|
||||
|
||||
test "paste event: empty listing packet is still sent" {
|
||||
const testing = std.testing;
|
||||
|
||||
var buf: [512]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try (PasteEvent{ .pw = "otp", .available = &.{} }).encode(&writer);
|
||||
try testing.expectEqualStrings(
|
||||
"\x1b]5522;type=read:status=OK:pw=b3Rw\x1b\\" ++
|
||||
"\x1b]5522;type=read:status=DATA:mime=Lg==:pw=b3Rw\x1b\\" ++
|
||||
"\x1b]5522;type=read:status=DONE:pw=b3Rw\x1b\\",
|
||||
writer.buffered(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ pub const kitty = @import("kitty.zig");
|
||||
pub const modes = @import("modes.zig");
|
||||
pub const page = @import("page.zig");
|
||||
pub const parse_table = @import("parse_table.zig");
|
||||
pub const paste = @import("paste.zig");
|
||||
pub const search = @import("search.zig");
|
||||
pub const snapshot = @import("snapshot/main.zig");
|
||||
pub const sgr = @import("sgr.zig");
|
||||
@@ -60,6 +61,11 @@ pub const TerminalStream = stream_terminal.Stream;
|
||||
pub const Stream = stream.Stream;
|
||||
pub const StreamAction = stream.Action;
|
||||
pub const UnknownSequence = stream_terminal.Handler.UnknownSequence;
|
||||
pub const Paste = paste.Request;
|
||||
pub const PasteContents = paste.Contents;
|
||||
pub const PasteSource = paste.Source;
|
||||
pub const MimeReader = clipboard.MimeReader;
|
||||
pub const PasteError = stream_terminal.Handler.PasteError;
|
||||
pub const Cursor = Screen.Cursor;
|
||||
pub const CursorStyle = Screen.CursorStyle;
|
||||
pub const CursorStyleReq = ansi.CursorStyle;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
//! to ensure all our various types and logic remain in sync.
|
||||
|
||||
const std = @import("std");
|
||||
const build_options = @import("terminal_options");
|
||||
const testing = std.testing;
|
||||
|
||||
/// A struct that maintains the state of all the settable modes.
|
||||
@@ -331,10 +332,12 @@ const entries: []const ModeEntry = &.{
|
||||
// paste sends an unsolicited OSC 5522 targets listing with a
|
||||
// one-time password instead of pasting the text.
|
||||
// See https://sw.kovidgoyal.net/kitty/clipboard/
|
||||
//
|
||||
// Forcibly disabled for now since the functionality isn't exposed
|
||||
// yet to libghostty or any GUI apps.
|
||||
.{ .name = "kitty_paste_events", .value = 5522, .disabled = true },
|
||||
.{
|
||||
.name = "kitty_paste_events",
|
||||
.value = 5522,
|
||||
// Only libghostty-vt supports this currently
|
||||
.disabled = build_options.artifact != .lib,
|
||||
},
|
||||
};
|
||||
|
||||
test {
|
||||
|
||||
247
src/terminal/paste.zig
Normal file
247
src/terminal/paste.zig
Normal file
@@ -0,0 +1,247 @@
|
||||
//! Pasting into a terminal.
|
||||
//!
|
||||
//! This is the single place that turns "the user pasted" into bytes for
|
||||
//! the pty, applying the terminal's current state:
|
||||
//!
|
||||
//! * Mode 5522 (Kitty clipboard protocol paste events) set, a
|
||||
//! user-initiated clipboard paste, and the embedder able to serve
|
||||
//! the program's follow-up clipboard read: send a paste event
|
||||
//! listing the clipboard's MIME types with a fresh one-time password
|
||||
//! and record a one-time read grant for it. No data is read.
|
||||
//! * Otherwise: write the first text representation, with unsafe bytes
|
||||
//! replaced (xterm behavior), framed per mode 2004 (bracketed paste)
|
||||
//! or with newlines converted to carriage returns if not.
|
||||
//!
|
||||
//! The precedence (5522 event, else 2004 framing, else plain) and the
|
||||
//! safety rule live only here so every embedder of the terminal shares
|
||||
//! one implementation.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const clipboard = @import("clipboard.zig");
|
||||
const kitty_clipboard = @import("kitty/clipboard.zig");
|
||||
const input_paste = @import("../input/paste.zig");
|
||||
const Terminal = @import("Terminal.zig");
|
||||
|
||||
/// Why a paste happened. Only clipboard pastes may become paste
|
||||
/// events, which is why only they carry a location: it's meaningless
|
||||
/// for text insertion.
|
||||
///
|
||||
/// C: GhosttyPasteSource, flattened next to the location since C has
|
||||
/// no tagged unions.
|
||||
pub const Source = union(enum) {
|
||||
/// The user pasted from a clipboard: keybind, menu, middle click.
|
||||
/// The payload is the clipboard the contents came from.
|
||||
clipboard: clipboard.Location,
|
||||
|
||||
/// Text inserted some other way: IME commit, drag and drop,
|
||||
/// scripted input.
|
||||
text,
|
||||
};
|
||||
|
||||
/// A paste of clipboard contents into the terminal. What actually gets
|
||||
/// written depends on terminal state; see `paste`.
|
||||
pub const Request = struct {
|
||||
/// Why this paste happened, and for a clipboard paste, from which
|
||||
/// clipboard. Only a user-initiated clipboard paste may become a
|
||||
/// paste event; text insertion always writes text.
|
||||
source: Source = .{ .clipboard = .standard },
|
||||
|
||||
/// The representations available. Borrowed only during the
|
||||
/// duration of the paste function call.
|
||||
contents: Contents,
|
||||
|
||||
/// Write data that could inject commands (see `paste`). The usual
|
||||
/// flow is to call with false, confirm with the user on
|
||||
/// error.UnsafePaste, and call again with true.
|
||||
allow_unsafe: bool = false,
|
||||
};
|
||||
|
||||
/// The representations available for a paste, in the embedder's
|
||||
/// preferred order.
|
||||
pub const Contents = union(enum) {
|
||||
/// Every representation already in memory. For text the embedder
|
||||
/// holds anyway (an IME commit, dropped text) or small clipboards.
|
||||
memory: []const clipboard.Content,
|
||||
|
||||
/// Representations read on demand, so nothing is loaded that isn't
|
||||
/// pasted. This is the form for a real clipboard, whose non-text
|
||||
/// items may be huge.
|
||||
reader: Reader,
|
||||
|
||||
/// The on-demand form: the MIME types available plus the reader
|
||||
/// that produces the data of any one of them.
|
||||
pub const Reader = struct {
|
||||
/// The MIME types available, in preferred order.
|
||||
mimes: []const []const u8,
|
||||
|
||||
/// Produces the data of any entry of `mimes`, which is passed
|
||||
/// through to it as is. A paste reads at most once: the text
|
||||
/// representation being pasted, never anything else and never
|
||||
/// anything for a paste event. There is no requirement across
|
||||
/// paste calls, so a source that changes between an unsafe
|
||||
/// refusal and the embedder's confirmed retry simply pastes
|
||||
/// its current contents.
|
||||
read: clipboard.MimeReader,
|
||||
};
|
||||
|
||||
/// The number of representations.
|
||||
pub fn len(self: Contents) usize {
|
||||
return switch (self) {
|
||||
.memory => |v| v.len,
|
||||
.reader => |v| v.mimes.len,
|
||||
};
|
||||
}
|
||||
|
||||
/// The MIME type of representation `index`.
|
||||
pub fn mime(self: Contents, index: usize) []const u8 {
|
||||
return switch (self) {
|
||||
.memory => |v| v[index].mime,
|
||||
.reader => |v| v.mimes[index],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// What a caller supplies to `paste`: the terminal state the decision
|
||||
/// depends on, the session state an event records into, and the sink.
|
||||
pub const Context = struct {
|
||||
/// The terminal whose modes decide the encoding.
|
||||
terminal: *const Terminal,
|
||||
|
||||
/// Allocator for transient state: the buffered read and a paste
|
||||
/// event's grant. Must be the allocator `kitty_clipboard.grants`
|
||||
/// is freed with.
|
||||
alloc: Allocator,
|
||||
|
||||
/// Receives the encoded text in chunks, or the event. Must be
|
||||
/// buffered (the encoder works in its buffer) and is not flushed
|
||||
/// by `paste`; the caller flushes once it returns.
|
||||
writer: *std.Io.Writer,
|
||||
|
||||
/// Kitty clipboard protocol session state, or null if the embedder
|
||||
/// does not serve Kitty clipboard reads (`clipboard.Read`). A
|
||||
/// paste event is that protocol: it is only useful if the
|
||||
/// program's follow-up read can be answered, since otherwise the
|
||||
/// read would be refused and the user's paste would vanish. With
|
||||
/// null, `paste` always writes text.
|
||||
kitty_clipboard: ?KittyClipboard,
|
||||
|
||||
pub const KittyClipboard = struct {
|
||||
/// Session grants. A paste event records its one-time password
|
||||
/// here so the program's follow-up read is served without a
|
||||
/// prompt.
|
||||
grants: *kitty_clipboard.Grants,
|
||||
|
||||
/// Secure entropy for one-time passwords. See generateOtp for
|
||||
/// why there is no fallback when this has none.
|
||||
io: std.Io,
|
||||
};
|
||||
};
|
||||
|
||||
pub const Error = Allocator.Error ||
|
||||
std.Io.RandomSecureError ||
|
||||
clipboard.MimeReader.Error ||
|
||||
error{
|
||||
/// The data could inject commands and allow_unsafe was false.
|
||||
UnsafePaste,
|
||||
};
|
||||
|
||||
/// Paste into the terminal, applying the terminal's current state as
|
||||
/// described in the module docs. Returns true if anything was written
|
||||
/// to `ctx.writer`: the encoded text or a paste event. False means
|
||||
/// there was nothing to paste (no non-empty text representation).
|
||||
///
|
||||
/// The safety rule for a text paste (`input.paste.isSafeWith`): a
|
||||
/// bracketed paste is unsafe only if it contains the bracket terminator
|
||||
/// (CSI 201~); an unbracketed paste is unsafe if it contains a newline
|
||||
/// or the terminator. Embedders wanting a stricter rule check
|
||||
/// `input.paste.isSafe` themselves before calling. A paste event never
|
||||
/// puts the data on the input stream, so the rule doesn't apply to it.
|
||||
///
|
||||
/// The contents are read at most once per call and buffered whole, so
|
||||
/// the source needs no stability across reads. Nothing reaches the
|
||||
/// writer until the read completed and the text passed the rule:
|
||||
/// every error writes nothing, and a failed event records no grant.
|
||||
pub fn paste(ctx: Context, req: Request) Error!bool {
|
||||
// If the source is a clipboard and mode 5522 is enabled and
|
||||
// the caller can handle kitty events, then do a kitty event.
|
||||
if (req.source == .clipboard and
|
||||
ctx.terminal.modes.get(.kitty_paste_events))
|
||||
{
|
||||
if (ctx.kitty_clipboard) |kitty| {
|
||||
try pasteKittyEvent(ctx, kitty, req);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// For non-Kitty paste events we can only accept text content.
|
||||
const index: usize = for (0..req.contents.len()) |i| {
|
||||
if (clipboard.isTextMime(req.contents.mime(i))) break i;
|
||||
} else return false;
|
||||
|
||||
const opts: input_paste.Options = .fromTerminal(ctx.terminal);
|
||||
|
||||
// In-memory contents are used directly to avoid a double copy but
|
||||
// reader-based contents are read into memory so we can do the unsafe
|
||||
// scan.
|
||||
var aw: std.Io.Writer.Allocating = .init(ctx.alloc);
|
||||
defer aw.deinit();
|
||||
const text: []const u8 = switch (req.contents) {
|
||||
.memory => |v| v[index].data,
|
||||
.reader => |v| text: {
|
||||
v.read.read(v.mimes[index], &aw.writer) catch |err| switch (err) {
|
||||
// An allocating writer only fails to allocate.
|
||||
error.WriteFailed => return error.OutOfMemory,
|
||||
error.ReadFailed => |e| return e,
|
||||
};
|
||||
break :text aw.writer.buffered();
|
||||
},
|
||||
};
|
||||
if (text.len == 0) return false;
|
||||
if (!req.allow_unsafe and !input_paste.isSafeWith(
|
||||
text,
|
||||
opts,
|
||||
)) return error.UnsafePaste;
|
||||
|
||||
// The text is copied exactly once per chunk, into the writer,
|
||||
// where the encoder strips and converts it in place.
|
||||
try input_paste.encodeWriter(ctx.writer, text, opts);
|
||||
return true;
|
||||
}
|
||||
|
||||
fn pasteKittyEvent(
|
||||
ctx: Context,
|
||||
kitty: Context.KittyClipboard,
|
||||
req: Request,
|
||||
) Error!void {
|
||||
const otp = try kitty_clipboard.generateOtp(kitty.io);
|
||||
|
||||
// Every representation is listed, never read. The listing is
|
||||
// bounded; a clipboard with more types than that is not a thing.
|
||||
var mimes_buf: [kitty_clipboard.max_listing_mimes][]const u8 = undefined;
|
||||
const mimes_len = @min(req.contents.len(), mimes_buf.len);
|
||||
for (mimes_buf[0..mimes_len], 0..) |*mime, i| mime.* = req.contents.mime(i);
|
||||
|
||||
// The grant is recorded before the event can reach the program,
|
||||
// and revoked if the event fails to be written so a failure never
|
||||
// leaves a grant for an event that was never sent. Using a
|
||||
// one-time grant consumes it, which is the revocation.
|
||||
try kitty.grants.grant(ctx.alloc, &otp, .read, true);
|
||||
errdefer _ = kitty.grants.use(ctx.alloc, &otp, .read);
|
||||
|
||||
try (kitty_clipboard.PasteEvent{
|
||||
// The protocol only distinguishes the clipboard from the
|
||||
// primary selection, so both non-standard locations report as
|
||||
// primary. Only a clipboard paste gets here; see `paste`.
|
||||
.primary = req.source.clipboard != .standard,
|
||||
.pw = &otp,
|
||||
.available = mimes_buf[0..mimes_len],
|
||||
}).encode(ctx.writer);
|
||||
}
|
||||
|
||||
test {
|
||||
// The behavior is tested end to end through the stream handler
|
||||
// (stream_terminal.zig), which is the primary caller.
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -52,3 +52,51 @@ fn decodePngWuffs(
|
||||
.data = result.data,
|
||||
};
|
||||
}
|
||||
|
||||
/// Fill a buffer with cryptographically secure random bytes. If null,
|
||||
/// the terminal's `std.Io` (`randomSecure`) is used. This is an override
|
||||
/// for embedders whose Io has no entropy source (e.g. wasm32-freestanding,
|
||||
/// where TinyIo degrades to `std.Io.failing`) or that want to control
|
||||
/// the source; when set it is used on every target.
|
||||
///
|
||||
/// This is used for secrets, so it must be a real CSPRNG. An error
|
||||
/// makes the operation that needed the entropy fail; nothing falls back
|
||||
/// to weaker randomness.
|
||||
pub var random_secure: ?RandomSecureFn = null;
|
||||
|
||||
pub const RandomSecureError = error{EntropyUnavailable};
|
||||
pub const RandomSecureFn = *const fn ([]u8) RandomSecureError!void;
|
||||
|
||||
/// Fill `buffer` with secure random bytes from `random_secure` if set,
|
||||
/// otherwise from `io`. Every use of secure entropy in the terminal
|
||||
/// package goes through this so the override applies uniformly.
|
||||
pub fn randomSecure(io: std.Io, buffer: []u8) std.Io.RandomSecureError!void {
|
||||
if (random_secure) |func| return func(buffer);
|
||||
return io.randomSecure(buffer);
|
||||
}
|
||||
|
||||
test "randomSecure: override is preferred over the Io" {
|
||||
const testing = std.testing;
|
||||
const S = struct {
|
||||
fn fill(buffer: []u8) RandomSecureError!void {
|
||||
@memset(buffer, 0xAB);
|
||||
}
|
||||
fn fail(_: []u8) RandomSecureError!void {
|
||||
return error.EntropyUnavailable;
|
||||
}
|
||||
};
|
||||
|
||||
// Without the override a failing Io fails.
|
||||
var buf: [8]u8 = @splat(0);
|
||||
try testing.expectError(error.EntropyUnavailable, randomSecure(std.Io.failing, &buf));
|
||||
|
||||
// With it, the Io is never consulted.
|
||||
random_secure = &S.fill;
|
||||
defer random_secure = null;
|
||||
try randomSecure(std.Io.failing, &buf);
|
||||
try testing.expect(std.mem.allEqual(u8, &buf, 0xAB));
|
||||
|
||||
// An override failure surfaces as the Io's error.
|
||||
random_secure = &S.fail;
|
||||
try testing.expectError(error.EntropyUnavailable, randomSecure(testing.io, &buf));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user