From 4384435941b8c7658856b381d071aee2e0035e89 Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Mon, 12 Oct 2026 17:49:34 +0300 Subject: term: move the term core to subprojects/term The core now builds, tests, and benchmarks alone, as a Meson subproject with no dependency on the main tree. - include/term/ holds term.h and the header-only unicode/. src/ holds the core and its private headers. The main tree gets only include/term through term_dep, so it cannot reach a private header. - The term tests, the term benchmarks, the Kitty parser suite, and tools/amalgamate.py and tools/generate_unicode.py move with it. The tests run as term:. - The main build passes its -march (arch_flag) and b_ndebug to the subproject. A directory configured before this commit needs meson configure -Dterm:arch_flag=-march=... once. - The term tests compile with -UNDEBUG. They do work inside assert. - The subproject exports term_test_dep with term_api.h, for the main tree's render, thumbnail, color, and selection tests. - The Kitty bridge used struct vector2i and has not compiled since the public API redesign. It builds again. 2 of its 14 tests fail: REP after a tab, and a cursor report after a resize. - dirgraph.py drops the term and unicode layers. Its self-test uses pty. - The symbols test ignores the __odr_asan symbols that ASan adds. Co-Authored-By: Claude Opus 5.5 --- include/term/term.h | 421 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 421 insertions(+) create mode 100644 include/term/term.h (limited to 'include/term/term.h') diff --git a/include/term/term.h b/include/term/term.h new file mode 100644 index 0000000..4853ebf --- /dev/null +++ b/include/term/term.h @@ -0,0 +1,421 @@ +#ifndef TERM_H +#define TERM_H + +/* + * Terminal emulator core. + * + * A struct term parses the bytes that a program writes and keeps the screen, + * the history, and the modes. The application feeds bytes with term_feed() + * and reads state with the getters below. A renderer never reads the + * terminal directly: it copies the viewport into a struct term_snapshot and + * paints from that copy. + * + * All functions take the terminal as the first argument. Functions that take + * a const struct term do not change what it shows. A function that returns + * text copies it into a buffer that the caller owns. + * + * Threads. Any thread can call any function. Each call that reads or changes + * a terminal enters its writer section, so calls on one terminal run one at a + * time. A snapshot belongs to one thread at a time. term_free() is the last + * call on a terminal. + */ + +#include +#include +#include + +#include "term_input.h" +#include "term_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct term; +struct term_snapshot; + +/* A row holds at most this many cells. */ +#define TERM_MAX_COLS 65535 +/* The history limit when term_options.scrollback is 0. */ +#define TERM_SCROLLBACK_DEFAULT 10000 + +/* + * Colors. A term_color is either a direct color (alpha 0xFF, then RGB) or a + * palette slot: an index 0-255, the default foreground, or the default + * background. A slot resolves when a snapshot is painted, so a theme change + * recolors text that is already on screen. + */ +typedef uint32_t term_color; + +#define TERM_COLOR_SLOT 0x01000000u +/* The palette index of a slot; see struct term_view.palette. */ +#define TERM_COLOR_SLOT_INDEX 0x1FFu +#define TERM_COLOR_FG (TERM_COLOR_SLOT | 0x100u) +#define TERM_COLOR_BG (TERM_COLOR_SLOT | 0x101u) + +static inline term_color term_color_index(int n) +{ + return TERM_COLOR_SLOT | ((uint32_t)n & 0xFFu); +} + +static inline term_color term_color_rgb(uint8_t r, uint8_t g, uint8_t b) +{ + return 0xFF000000u | (uint32_t)r << 16 | (uint32_t)g << 8 | b; +} + +static inline bool term_color_is_slot(term_color c) +{ + return (c >> 24) == (TERM_COLOR_SLOT >> 24); +} + +/* Parse an X11 or OSC color specification ("#rgb", "rgb:r/g/b", ...). */ +TERM_API bool term_color_parse(const char *spec, uint32_t *argb); + +/* The colors a terminal starts with. All values are ARGB. An all-zero theme + * selects the built-in theme. */ +struct term_theme { + uint32_t fg, bg, cursor; + uint32_t ansi[16]; +}; + +/* Fill theme with the built-in theme. */ +TERM_API void term_theme_default(struct term_theme *theme); +/* Fill theme with the high-contrast theme. */ +TERM_API void term_theme_high_contrast(struct term_theme *theme); +/* Sets one palette key: foreground, background, or color0 to color15. + * An empty value resets the key to the built-in theme. False, with no + * change, for an unknown key or a bad color. */ +TERM_API bool term_theme_set_key(struct term_theme *p, const char *key, const char *val); + +enum term_cursor_style { + TERM_CURSOR_BLOCK, + TERM_CURSOR_UNDERLINE, + TERM_CURSOR_BAR, +}; + +/* Mouse reporting modes and encodings. The values are the DEC mode numbers. */ +enum term_mouse { + TERM_MOUSE_OFF = 0, + TERM_MOUSE_X10 = 9, + TERM_MOUSE_NORMAL = 1000, + TERM_MOUSE_BUTTON = 1002, + TERM_MOUSE_ANY = 1003, +}; + +enum term_mouse_encoding { + TERM_MOUSE_ENC_X10 = 0, + TERM_MOUSE_ENC_UTF8 = 1005, + TERM_MOUSE_ENC_SGR = 1006, + TERM_MOUSE_ENC_URXVT = 1015, + TERM_MOUSE_ENC_SGR_PIXELS = 1016, +}; + +/* + * Callbacks. They run inside term_feed() and term_focus(), on the thread + * that calls those functions. A callback must not call back into the + * terminal. String arguments are valid only during the call. + */ +struct term_callbacks { + void *ud; + /* Bytes the terminal sends back to the program (replies to queries). */ + void (*reply)(void *ud, const char *bytes, size_t len); + void (*title)(void *ud, const char *title); + /* OSC 52. selection names the target, such as "c". */ + void (*clipboard)(void *ud, const char *selection, const char *data, size_t len); + void (*notify)(void *ud, const char *title, const char *body); + void (*bell)(void *ud); + /* Parser trace lines for debugging. NULL turns tracing off. The line has no newline. */ + void (*trace)(void *ud, const char *line); +}; + +/* Options for term_new(). An all-zero value is valid. */ +struct term_options { + struct term_vector2i size; /* columns and rows; each is at least 1 */ + int scrollback; /* history lines; 0 selects TERM_SCROLLBACK_DEFAULT */ + struct term_theme theme; + enum term_cursor_style cursor_style; + bool cursor_steady; /* false: the cursor blinks */ + struct term_callbacks callbacks; +}; + +/* NULL when memory runs out. */ +/* The Unicode version of the width tables, such as "18.0.0". A host can + * pass it to programs as UNICODE_VERSION. */ +TERM_API const char *term_unicode_version(void); + +/* The length of the prefix of buf made of bytes in [0x20, hi). With hi + * 0x7f, it finds a run of printable ASCII in UTF-8 text, so a host can + * skip shaping for it. Any thread can call it. */ +TERM_API size_t term_scan_printable_span(const char *buf, size_t len, unsigned char hi); + +/* The instruction set level of the running code: "base", "avx2", "avx512", + * or "neon". On x86-64, the first term_new() picks the highest level the CPU + * supports. Set TERM_HEADLESS_ISA to a lower level to force it, for tests + * and comparisons. */ +TERM_API const char *term_isa(void); + +TERM_API struct term *term_new(const struct term_options *opt); +TERM_API void term_free(struct term *t); + +/* Hold the writer section across several calls, so that no other thread + * changes the terminal between them. The section is a spin lock: keep it + * short. The thread that holds it can call every function, and it can enter + * the section again. */ +TERM_API void term_lock(struct term *t); +TERM_API void term_unlock(struct term *t); + +/* Change the size in cells. Returns false when memory runs out; the terminal + * is then unchanged. The screen reflows to the new width. */ +TERM_API bool term_resize(struct term *t, struct term_vector2i size); + +/* Replace the theme. A different theme also drops the colors that the + * program set with OSC; the same theme changes nothing. */ +TERM_API void term_set_theme(struct term *t, const struct term_theme *theme); + +/* The cursor style that DECSCUSR 0 and a reset restore. */ +TERM_API void term_set_cursor_default(struct term *t, enum term_cursor_style style, bool steady); + +/* + * Bytes from the program. + * + * TERM_FEED_ONLCR: a line feed also returns the cursor to column 0, as a tty + * with OPOST and ONLCR does. Set it when the bytes did not pass through the + * tty output processing (for example the fast pipe). + */ +enum term_feed_flags { + TERM_FEED_ONLCR = 1u << 0, +}; + +TERM_API void term_feed(struct term *t, const char *bytes, size_t len, unsigned flags); + +/* Report a focus change. The terminal replies only in focus-event mode. */ +TERM_API void term_focus(struct term *t, bool focused); + +/* Modes that affect how the application encodes input. */ +struct term_modes { + unsigned keyboard; /* kitty keyboard protocol flags */ + enum term_mouse mouse; + enum term_mouse_encoding mouse_encoding; + struct term_vector2i grid; /* columns and rows */ + bool app_cursor; /* DECCKM */ + bool app_keypad; /* DECKPAM */ + bool backarrow_bs; /* DECBKM */ + bool bracketed_paste; + bool focus_events; + bool alternate; /* the alternate screen is active */ + bool reverse_video; /* DECSCNM */ + bool synchronized; /* mode 2026 */ +}; + +struct term_cursor { + struct term_vector2i pos; + enum term_cursor_style style; + bool visible; + bool blink; +}; + +TERM_API struct term_vector2i term_size(const struct term *t); +TERM_API struct term_modes term_modes(const struct term *t); + +/* + * Encode an input event as the bytes the program expects. Get modes from + * term_modes(). Record ev in keyboard with term_keyboard_update() first. Send + * the bytes to the program yourself. + */ +TERM_API struct term_input_result term_input_encode_key( + const struct term_keyboard_state *keyboard, + const struct term_key_event *ev, + const struct term_modes *modes, + char *buf, size_t capacity +); + +TERM_API struct term_input_result term_input_encode_mouse( + const struct term_mouse_event *ev, + const struct term_modes *modes, + char *buf, size_t capacity +); + +/* Bytes term_input_encode_paste() adds to the text at most. */ +#define TERM_PASTE_EXTRA 12 + +/* + * Encode pasted text. Drop control characters and bidi controls, so the + * program sees only what the user saw. Send a line break as CR. Add the + * bracketed-paste markers when the program asked for them. A buffer of + * len + TERM_PASTE_EXTRA bytes is always large enough. Text that is empty + * after the filter gives TERM_INPUT_CONSUMED. + */ +TERM_API struct term_input_result term_input_encode_paste( + const struct term_modes *modes, + const char *text, size_t len, + char *buf, size_t capacity +); +/* The cursor in screen coordinates (not the viewport). */ +TERM_API struct term_cursor term_cursor(const struct term *t); +/* Copy the directory from OSC 7 into buf, with the rules of + * term_selection_text(). Return 0 when there is none. */ +TERM_API size_t term_cwd(const struct term *t, char *buf, size_t cap); +/* Copy the pointer shape name from OSC 22 the same way. */ +TERM_API size_t term_pointer_shape(const struct term *t, char *buf, size_t cap); + +/* + * Viewport. The viewport shows as many rows as the screen. Offset 0 shows + * the live screen; a larger offset shows that many rows of history above it. + * The alternate screen has no history, so its offset is always 0. + */ +struct term_scrollback { + int offset; /* rows between the viewport and the live screen */ + int total; /* history rows at the current width */ +}; + +TERM_API struct term_scrollback term_scrollback(const struct term *t); +/* Scroll the viewport. A positive count moves back into history. */ +TERM_API void term_scroll(struct term *t, int lines); +TERM_API void term_scroll_to(struct term *t, int offset); + +/* + * Selection, in viewport coordinates. A selection starts at one cell and + * extends to another; the unit widens both ends to words or whole lines. + * The terminal clears the selection when the text under it changes. + */ +enum term_select_unit { + TERM_SELECT_CELL, + TERM_SELECT_WORD, + TERM_SELECT_LINE, +}; + +/* Start a selection at cell. For TERM_SELECT_CELL, right_half means the + * pointer is on the right half of the cell, so the selection starts after it. */ +TERM_API void term_select_start(struct term *t, struct term_vector2i cell, bool right_half, + enum term_select_unit unit); +/* Extend the selection from its start to cell. */ +TERM_API void term_select_extend(struct term *t, struct term_vector2i cell, bool right_half); +TERM_API void term_select_clear(struct term *t); +TERM_API bool term_has_selection(const struct term *t); +/* Copy the selected text as UTF-8 into buf. Return its length without the NUL. + * With a NULL buf or a zero cap, only the length is returned. Otherwise the + * copy is cut to cap - 1 bytes and NUL-terminated. Return 0 when nothing is + * selected. Hold term_lock across the length query and the copy to keep the + * length stable. */ +TERM_API size_t term_selection_text(const struct term *t, char *buf, size_t cap); +/* The OSC 8 link or the detected URL at a viewport cell. Copies it with the + * same rules as term_selection_text(). Return 0 when there is no link. */ +TERM_API size_t term_link_at(const struct term *t, struct term_vector2i cell, char *buf, size_t cap); + +/* Cell attributes. */ +enum term_attr { + TERM_ATTR_BOLD = 1u << 0, + TERM_ATTR_ITALIC = 1u << 1, + TERM_ATTR_FAINT = 1u << 2, + TERM_ATTR_STRIKE = 1u << 3, + TERM_ATTR_OVERLINE = 1u << 4, + TERM_ATTR_LINK = 1u << 5, + TERM_ATTR_EMOJI = 1u << 6, /* the text requested emoji presentation */ + TERM_ATTR_TEXT = 1u << 7, /* the text requested text presentation */ +}; + +#define TERM_ATTR_UNDERLINE_SHIFT 8 +#define TERM_ATTR_UNDERLINE_MASK (7u << TERM_ATTR_UNDERLINE_SHIFT) + +enum term_underline { + TERM_UNDERLINE_NONE, + TERM_UNDERLINE_SINGLE, + TERM_UNDERLINE_DOUBLE, + TERM_UNDERLINE_CURLY, + TERM_UNDERLINE_DOTTED, + TERM_UNDERLINE_DASHED, +}; + +static inline enum term_underline term_attr_underline(uint16_t attrs) +{ + return (enum term_underline)((attrs & TERM_ATTR_UNDERLINE_MASK) >> TERM_ATTR_UNDERLINE_SHIFT); +} + +/* text refers to a grapheme cluster in the snapshot; see term_snapshot_cluster(). */ +#define TERM_TEXT_CLUSTER 0x80000000u + +/* One cell as a snapshot shows it. */ +struct term_cell { + uint32_t text; /* a code point, TERM_TEXT_CLUSTER | n, or 0 for an empty cell */ + term_color fg, bg; + uint16_t attrs; /* enum term_attr and the underline style */ + uint8_t width; /* 1; 2 for a wide glyph; 0 for the right half of a wide glyph */ + uint8_t reserved; +}; + +#ifdef __cplusplus +static_assert(sizeof(struct term_cell) == 16, "four cells fill one 64-byte line"); +#else +_Static_assert(sizeof(struct term_cell) == 16, "four cells fill one 64-byte line"); +#endif + +/* + * Snapshot: a copy of the viewport for a renderer. It holds no pointer into + * the terminal, so it stays valid while the terminal changes. One terminal + * may have several snapshots, for example a window and a tab preview. + */ +TERM_API struct term_snapshot *term_snapshot_new(void); +TERM_API void term_snapshot_free(struct term_snapshot *s); + +/* Copy the viewport of t into s. Keep rows with unchanged content keys. + * Update the history view index and refine counts for visible wide lines. + * Return false when memory runs out. The snapshot is then empty. */ +TERM_API bool term_snapshot_update(struct term_snapshot *s, struct term *t); + +/* Palette entries of struct term_view.palette after the 256 indexed colors. */ +enum { + TERM_PALETTE_FG = 256, + TERM_PALETTE_BG = 257, + TERM_PALETTE_CURSOR = 258, + TERM_PALETTE_SIZE = 259, +}; + +struct term_view { + struct term_vector2i size; + struct term_cursor cursor; /* viewport coordinates; not visible when scrolled away */ + int scroll_offset; + /* Rows the content moved up since the previous update, when that movement + * explains the change; otherwise 0. */ + int scrolled; + bool reverse_video; + /* Changes when any part of the view changes. */ + uint64_t version; + /* Changes when a palette entry changes. */ + uint64_t palette_version; + const uint32_t *palette; /* TERM_PALETTE_SIZE ARGB entries */ +}; + +TERM_API const struct term_view *term_snapshot_view(const struct term_snapshot *s); +/* The cells of viewport row y: view->size.x entries. */ +TERM_API const struct term_cell *term_snapshot_row(const struct term_snapshot *s, int y); +/* An identity for the content of row y. Two rows with the same key hold the + * same cells, also when the content moved to another row. */ +TERM_API uint64_t term_snapshot_row_key(const struct term_snapshot *s, int y); + +/* Number of rows in history above the screen. 0 if alternate screen or no history. */ +TERM_API int term_history_rows(const struct term *t); +/* Copy up to max_rows history rows before the current viewport as UTF-8 into + * buf, with the same rules as term_selection_text(). Join soft-wrapped rows. + * End closed lines with a newline. This does not change the selection. + * Return 0 when there is no history text, and also when the text cannot be + * built. */ +TERM_API size_t term_history_text(struct term *t, int max_rows, char *buf, size_t cap); +TERM_API struct term_span term_snapshot_row_selection(const struct term_snapshot *s, int y); + +/* The code points of a cluster cell. Returns the number of code points and + * writes at most cap of them. */ +TERM_API int term_snapshot_cluster(const struct term_snapshot *s, uint32_t text, uint32_t *cps, int cap); + +/* The ARGB value of a color in a view. */ +static inline uint32_t term_view_color(const struct term_view *v, term_color c) +{ + if (!term_color_is_slot(c)) + return c; + return v->palette[c & TERM_COLOR_SLOT_INDEX]; +} + +#ifdef __cplusplus +} +#endif + +#endif -- cgit v1.2.3