mirror of
https://github.com/neovim/neovim.git
synced 2026-04-04 14:49:31 +00:00
Problem: Applications running inside :terminal that use DEC private mode 2026 (synchronized output) to batch screen updates get garbled rendering. Neovim's embedded libvterm does not handle mode 2026, so the synchronization sequences are ignored and intermediate screen states leak through as visual corruption. Solution: Add mode 2026 support to libvterm's state machine and wire it through to terminal.c. When an application enables mode 2026, invalidation of the terminal buffer is deferred until the application disables it, causing all accumulated screen updates to flush as a single atomic refresh. * fix(terminal): harden sync output redraw gating Problem: The initial mode 2026 implementation gated invalidate_terminal() but missed three other redraw paths: term_sb_push/term_sb_pop bypassed the gate by directly adding to invalidated_terminals, refresh_timer_cb could fire mid-sync flushing partial state, and the 10ms timer delay after sync-end left a window for stale repaints. Solution: - Gate term_sb_push/term_sb_pop during synchronized output - Skip syncing terminals in refresh_timer_cb - On sync end, schedule a zero-delay full-screen refresh via sync_flush_pending flag in terminal_receive() - Add news.txt entry for mode 2026 support Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(terminal): add vterm unit tests for mode 2026 Add unit-level tests for synchronized output (mode 2026) to vterm_spec.lua, covering settermprop callbacks and DECRQM query/response. Suggested-by: justinmk Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(terminal): address review feedback for mode 2026 - Use multiqueue_put(main_loop.events) instead of restarting the global refresh timer on sync end, to avoid affecting other invalidated terminals. - Add screen:expect_unchanged() to verify screen doesn't update during sync mode. - Merge buffer-lines test into existing test. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
814 lines
17 KiB
C
814 lines
17 KiB
C
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "nvim/grid.h"
|
|
#include "nvim/mbyte.h"
|
|
#include "nvim/vterm/pen.h"
|
|
#include "nvim/vterm/screen.h"
|
|
#include "nvim/vterm/vterm_internal_defs.h"
|
|
#include "vterm_test.h"
|
|
|
|
int parser_text(const char bytes[], size_t len, void *user)
|
|
{
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
fprintf(f, "text ");
|
|
size_t i;
|
|
for (i = 0; i < len; i++) {
|
|
unsigned char b = (unsigned char)bytes[i];
|
|
if (b < 0x20 || b == 0x7f || (b >= 0x80 && b < 0xa0)) {
|
|
break;
|
|
}
|
|
fprintf(f, i ? ",%x" : "%x", b);
|
|
}
|
|
fprintf(f, "\n");
|
|
fclose(f);
|
|
|
|
return (int)i;
|
|
}
|
|
|
|
static void printchars(const char *s, size_t len, FILE *f)
|
|
{
|
|
while (len--) {
|
|
fprintf(f, "%c", (s++)[0]);
|
|
}
|
|
}
|
|
|
|
int parser_csi(const char *leader, const long args[], int argcount, const char *intermed,
|
|
char command, void *user)
|
|
{
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
fprintf(f, "csi %02x", command);
|
|
|
|
if (leader && leader[0]) {
|
|
fprintf(f, " L=");
|
|
for (int i = 0; leader[i]; i++) {
|
|
fprintf(f, "%02x", leader[i]);
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < argcount; i++) {
|
|
char sep = i ? ',' : ' ';
|
|
|
|
if (args[i] == CSI_ARG_MISSING) {
|
|
fprintf(f, "%c*", sep);
|
|
} else {
|
|
fprintf(f, "%c%ld%s", sep, CSI_ARG(args[i]), CSI_ARG_HAS_MORE(args[i]) ? "+" : "");
|
|
}
|
|
}
|
|
|
|
if (intermed && intermed[0]) {
|
|
fprintf(f, " I=");
|
|
for (int i = 0; intermed[i]; i++) {
|
|
fprintf(f, "%02x", intermed[i]);
|
|
}
|
|
}
|
|
|
|
fprintf(f, "\n");
|
|
|
|
fclose(f);
|
|
|
|
return 1;
|
|
}
|
|
|
|
int parser_osc(int command, VTermStringFragment frag, void *user)
|
|
{
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
fprintf(f, "osc ");
|
|
|
|
if (frag.initial) {
|
|
if (command == -1) {
|
|
fprintf(f, "[");
|
|
} else {
|
|
fprintf(f, "[%d;", command);
|
|
}
|
|
}
|
|
|
|
printchars(frag.str, frag.len, f);
|
|
|
|
if (frag.final) {
|
|
fprintf(f, "]");
|
|
}
|
|
|
|
fprintf(f, "\n");
|
|
fclose(f);
|
|
|
|
return 1;
|
|
}
|
|
|
|
int parser_dcs(const char *command, size_t commandlen, VTermStringFragment frag, void *user)
|
|
{
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
fprintf(f, "dcs ");
|
|
|
|
if (frag.initial) {
|
|
fprintf(f, "[");
|
|
for (size_t i = 0; i < commandlen; i++) {
|
|
fprintf(f, "%c", command[i]);
|
|
}
|
|
}
|
|
|
|
printchars(frag.str, frag.len, f);
|
|
|
|
if (frag.final) {
|
|
fprintf(f, "]");
|
|
}
|
|
|
|
fprintf(f, "\n");
|
|
fclose(f);
|
|
|
|
return 1;
|
|
}
|
|
|
|
int parser_apc(VTermStringFragment frag, void *user)
|
|
{
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
fprintf(f, "apc ");
|
|
|
|
if (frag.initial) {
|
|
fprintf(f, "[");
|
|
}
|
|
|
|
printchars(frag.str, frag.len, f);
|
|
|
|
if (frag.final) {
|
|
fprintf(f, "]");
|
|
}
|
|
|
|
fprintf(f, "\n");
|
|
fclose(f);
|
|
|
|
return 1;
|
|
}
|
|
|
|
int parser_pm(VTermStringFragment frag, void *user)
|
|
{
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
fprintf(f, "pm ");
|
|
|
|
if (frag.initial) {
|
|
fprintf(f, "[");
|
|
}
|
|
|
|
printchars(frag.str, frag.len, f);
|
|
|
|
if (frag.final) {
|
|
fprintf(f, "]");
|
|
}
|
|
|
|
fprintf(f, "\n");
|
|
fclose(f);
|
|
|
|
return 1;
|
|
}
|
|
|
|
int parser_sos(VTermStringFragment frag, void *user)
|
|
{
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
fprintf(f, "sos ");
|
|
|
|
if (frag.initial) {
|
|
fprintf(f, "[");
|
|
}
|
|
|
|
printchars(frag.str, frag.len, f);
|
|
|
|
if (frag.final) {
|
|
fprintf(f, "]");
|
|
}
|
|
|
|
fprintf(f, "\n");
|
|
fclose(f);
|
|
|
|
return 1;
|
|
}
|
|
|
|
int selection_set(VTermSelectionMask mask, VTermStringFragment frag, void *user)
|
|
{
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
fprintf(f, "selection-set mask=%04X ", mask);
|
|
if (frag.initial) {
|
|
fprintf(f, "[");
|
|
}
|
|
printchars(frag.str, frag.len, f);
|
|
if (frag.final) {
|
|
fprintf(f, "]");
|
|
}
|
|
fprintf(f, "\n");
|
|
|
|
fclose(f);
|
|
return 1;
|
|
}
|
|
|
|
int selection_query(VTermSelectionMask mask, void *user)
|
|
{
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
fprintf(f, "selection-query mask=%04X\n", mask);
|
|
|
|
fclose(f);
|
|
return 1;
|
|
}
|
|
|
|
static void print_schar(FILE *f, schar_T schar)
|
|
{
|
|
char buf[MAX_SCHAR_SIZE];
|
|
schar_get(buf, schar);
|
|
StrCharInfo ci = utf_ptr2StrCharInfo(buf);
|
|
bool did = false;
|
|
while (*ci.ptr != 0) {
|
|
if (did) {
|
|
fprintf(f, ",");
|
|
}
|
|
|
|
if (ci.chr.len == 1 && ci.chr.value >= 0x80) {
|
|
fprintf(f, "??%x", ci.chr.value);
|
|
} else {
|
|
fprintf(f, "%x", ci.chr.value);
|
|
}
|
|
did = true;
|
|
ci = utf_ptr2StrCharInfo(ci.ptr + ci.chr.len);
|
|
}
|
|
}
|
|
|
|
bool want_state_putglyph;
|
|
int state_putglyph(VTermGlyphInfo *info, VTermPos pos, void *user)
|
|
{
|
|
if (!want_state_putglyph) {
|
|
return 1;
|
|
}
|
|
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
fprintf(f, "putglyph ");
|
|
print_schar(f, info->schar);
|
|
fprintf(f, " %d %d,%d", info->width, pos.row, pos.col);
|
|
if (info->protected_cell) {
|
|
fprintf(f, " prot");
|
|
}
|
|
if (info->dwl) {
|
|
fprintf(f, " dwl");
|
|
}
|
|
if (info->dhl) {
|
|
fprintf(f, " dhl-%s", info->dhl == 1 ? "top" : info->dhl == 2 ? "bottom" : "?");
|
|
}
|
|
fprintf(f, "\n");
|
|
|
|
fclose(f);
|
|
|
|
return 1;
|
|
}
|
|
|
|
bool want_state_movecursor;
|
|
VTermPos state_pos;
|
|
int state_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user)
|
|
{
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
state_pos = pos;
|
|
|
|
if (want_state_movecursor) {
|
|
fprintf(f, "movecursor %d,%d\n", pos.row, pos.col);
|
|
}
|
|
|
|
fclose(f);
|
|
return 1;
|
|
}
|
|
|
|
bool want_state_scrollrect;
|
|
int state_scrollrect(VTermRect rect, int downward, int rightward, void *user)
|
|
{
|
|
if (!want_state_scrollrect) {
|
|
return 0;
|
|
}
|
|
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
|
|
fprintf(f, "scrollrect %d..%d,%d..%d => %+d,%+d\n",
|
|
rect.start_row, rect.end_row, rect.start_col, rect.end_col,
|
|
downward, rightward);
|
|
|
|
fclose(f);
|
|
return 1;
|
|
}
|
|
|
|
bool want_state_moverect;
|
|
int state_moverect(VTermRect dest, VTermRect src, void *user)
|
|
{
|
|
if (!want_state_moverect) {
|
|
return 0;
|
|
}
|
|
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
fprintf(f, "moverect %d..%d,%d..%d -> %d..%d,%d..%d\n",
|
|
src.start_row, src.end_row, src.start_col, src.end_col,
|
|
dest.start_row, dest.end_row, dest.start_col, dest.end_col);
|
|
|
|
fclose(f);
|
|
return 1;
|
|
}
|
|
|
|
void print_color(const VTermColor *col)
|
|
{
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
if (VTERM_COLOR_IS_RGB(col)) {
|
|
fprintf(f, "rgb(%d,%d,%d", col->rgb.red, col->rgb.green, col->rgb.blue);
|
|
} else if (VTERM_COLOR_IS_INDEXED(col)) {
|
|
fprintf(f, "idx(%d", col->indexed.idx);
|
|
} else {
|
|
fprintf(f, "invalid(%d", col->type);
|
|
}
|
|
if (VTERM_COLOR_IS_DEFAULT_FG(col)) {
|
|
fprintf(f, ",is_default_fg");
|
|
}
|
|
if (VTERM_COLOR_IS_DEFAULT_BG(col)) {
|
|
fprintf(f, ",is_default_bg");
|
|
}
|
|
fprintf(f, ")");
|
|
fclose(f);
|
|
}
|
|
|
|
static VTermValueType vterm_get_prop_type(VTermProp prop)
|
|
{
|
|
switch (prop) {
|
|
case VTERM_PROP_CURSORVISIBLE:
|
|
return VTERM_VALUETYPE_BOOL;
|
|
case VTERM_PROP_CURSORBLINK:
|
|
return VTERM_VALUETYPE_BOOL;
|
|
case VTERM_PROP_ALTSCREEN:
|
|
return VTERM_VALUETYPE_BOOL;
|
|
case VTERM_PROP_TITLE:
|
|
return VTERM_VALUETYPE_STRING;
|
|
case VTERM_PROP_ICONNAME:
|
|
return VTERM_VALUETYPE_STRING;
|
|
case VTERM_PROP_REVERSE:
|
|
return VTERM_VALUETYPE_BOOL;
|
|
case VTERM_PROP_CURSORSHAPE:
|
|
return VTERM_VALUETYPE_INT;
|
|
case VTERM_PROP_MOUSE:
|
|
return VTERM_VALUETYPE_INT;
|
|
case VTERM_PROP_FOCUSREPORT:
|
|
return VTERM_VALUETYPE_BOOL;
|
|
case VTERM_PROP_THEMEUPDATES:
|
|
return VTERM_VALUETYPE_BOOL;
|
|
case VTERM_PROP_SYNCOUTPUT:
|
|
return VTERM_VALUETYPE_BOOL;
|
|
|
|
case VTERM_N_PROPS:
|
|
return 0;
|
|
}
|
|
return 0; // UNREACHABLE
|
|
}
|
|
|
|
bool want_state_settermprop;
|
|
int state_settermprop(VTermProp prop, VTermValue *val, void *user)
|
|
{
|
|
if (!want_state_settermprop) {
|
|
return 1;
|
|
}
|
|
|
|
int errcode = 0;
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
|
|
VTermValueType type = vterm_get_prop_type(prop);
|
|
switch (type) {
|
|
case VTERM_VALUETYPE_BOOL:
|
|
fprintf(f, "settermprop %d %s\n", prop, val->boolean ? "true" : "false");
|
|
errcode = 1;
|
|
goto end;
|
|
case VTERM_VALUETYPE_INT:
|
|
fprintf(f, "settermprop %d %d\n", prop, val->number);
|
|
errcode = 1;
|
|
goto end;
|
|
case VTERM_VALUETYPE_STRING:
|
|
fprintf(f, "settermprop %d %s\"%.*s\"%s\n", prop,
|
|
val->string.initial ? "[" : "", (int)val->string.len, val->string.str,
|
|
val->string.final ? "]" : "");
|
|
errcode = 0;
|
|
goto end;
|
|
case VTERM_VALUETYPE_COLOR:
|
|
fprintf(f, "settermprop %d ", prop);
|
|
print_color(&val->color);
|
|
fprintf(f, "\n");
|
|
errcode = 1;
|
|
goto end;
|
|
case VTERM_N_VALUETYPES:
|
|
goto end;
|
|
}
|
|
|
|
end:
|
|
fclose(f);
|
|
return errcode;
|
|
}
|
|
|
|
bool want_state_erase;
|
|
int state_erase(VTermRect rect, int selective, void *user)
|
|
{
|
|
if (!want_state_erase) {
|
|
return 1;
|
|
}
|
|
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
|
|
fprintf(f, "erase %d..%d,%d..%d%s\n",
|
|
rect.start_row, rect.end_row, rect.start_col, rect.end_col,
|
|
selective ? " selective" : "");
|
|
|
|
fclose(f);
|
|
return 1;
|
|
}
|
|
|
|
struct {
|
|
int bold;
|
|
int underline;
|
|
int italic;
|
|
int blink;
|
|
int reverse;
|
|
int conceal;
|
|
int strike;
|
|
int font;
|
|
int small;
|
|
int baseline;
|
|
int dim;
|
|
int overline;
|
|
VTermColor foreground;
|
|
VTermColor background;
|
|
} state_pen;
|
|
|
|
int state_setpenattr(VTermAttr attr, VTermValue *val, void *user)
|
|
{
|
|
switch (attr) {
|
|
case VTERM_ATTR_BOLD:
|
|
state_pen.bold = val->boolean;
|
|
break;
|
|
case VTERM_ATTR_UNDERLINE:
|
|
state_pen.underline = val->number;
|
|
break;
|
|
case VTERM_ATTR_ITALIC:
|
|
state_pen.italic = val->boolean;
|
|
break;
|
|
case VTERM_ATTR_BLINK:
|
|
state_pen.blink = val->boolean;
|
|
break;
|
|
case VTERM_ATTR_REVERSE:
|
|
state_pen.reverse = val->boolean;
|
|
break;
|
|
case VTERM_ATTR_CONCEAL:
|
|
state_pen.conceal = val->boolean;
|
|
break;
|
|
case VTERM_ATTR_STRIKE:
|
|
state_pen.strike = val->boolean;
|
|
break;
|
|
case VTERM_ATTR_FONT:
|
|
state_pen.font = val->number;
|
|
break;
|
|
case VTERM_ATTR_SMALL:
|
|
state_pen.small = val->boolean;
|
|
break;
|
|
case VTERM_ATTR_BASELINE:
|
|
state_pen.baseline = val->number;
|
|
break;
|
|
case VTERM_ATTR_DIM:
|
|
state_pen.dim = val->boolean;
|
|
break;
|
|
case VTERM_ATTR_OVERLINE:
|
|
state_pen.overline = val->boolean;
|
|
break;
|
|
case VTERM_ATTR_FOREGROUND:
|
|
state_pen.foreground = val->color;
|
|
break;
|
|
case VTERM_ATTR_BACKGROUND:
|
|
state_pen.background = val->color;
|
|
break;
|
|
|
|
case VTERM_N_ATTRS:
|
|
return 0;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
bool want_state_scrollback;
|
|
int state_sb_clear(void *user)
|
|
{
|
|
if (!want_state_scrollback) {
|
|
return 1;
|
|
}
|
|
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
fprintf(f, "sb_clear\n");
|
|
fclose(f);
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool want_screen_scrollback;
|
|
int screen_sb_pushline(int cols, const VTermScreenCell *cells, void *user)
|
|
{
|
|
if (!want_screen_scrollback) {
|
|
return 1;
|
|
}
|
|
|
|
int eol = cols;
|
|
while (eol && !cells[eol - 1].schar) {
|
|
eol--;
|
|
}
|
|
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
fprintf(f, "sb_pushline %d =", cols);
|
|
for (int c = 0; c < eol; c++) {
|
|
fprintf(f, " ");
|
|
print_schar(f, cells[c].schar);
|
|
}
|
|
fprintf(f, "\n");
|
|
|
|
fclose(f);
|
|
|
|
return 1;
|
|
}
|
|
|
|
int screen_sb_popline(int cols, VTermScreenCell *cells, void *user)
|
|
{
|
|
if (!want_screen_scrollback) {
|
|
return 0;
|
|
}
|
|
|
|
// All lines of scrollback contain "ABCDE"
|
|
for (int col = 0; col < cols; col++) {
|
|
if (col < 5) {
|
|
cells[col].schar = schar_from_ascii((uint32_t)('A' + col));
|
|
} else {
|
|
cells[col].schar = 0;
|
|
}
|
|
|
|
cells[col].width = 1;
|
|
}
|
|
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
fprintf(f, "sb_popline %d\n", cols);
|
|
fclose(f);
|
|
return 1;
|
|
}
|
|
|
|
int screen_sb_clear(void *user)
|
|
{
|
|
if (!want_screen_scrollback) {
|
|
return 1;
|
|
}
|
|
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
fprintf(f, "sb_clear\n");
|
|
fclose(f);
|
|
return 0;
|
|
}
|
|
|
|
void term_output(const char *s, size_t len, void *user)
|
|
{
|
|
FILE *f = fopen(VTERM_TEST_FILE, "a");
|
|
fprintf(f, "output ");
|
|
for (size_t i = 0; i < len; i++) {
|
|
fprintf(f, "%x%s", (unsigned char)s[i], i < len - 1 ? "," : "\n");
|
|
}
|
|
fclose(f);
|
|
}
|
|
|
|
int vterm_state_get_penattr(const VTermState *state, VTermAttr attr, VTermValue *val)
|
|
{
|
|
switch (attr) {
|
|
case VTERM_ATTR_BOLD:
|
|
val->boolean = state->pen.bold;
|
|
return 1;
|
|
|
|
case VTERM_ATTR_UNDERLINE:
|
|
val->number = state->pen.underline;
|
|
return 1;
|
|
|
|
case VTERM_ATTR_ITALIC:
|
|
val->boolean = state->pen.italic;
|
|
return 1;
|
|
|
|
case VTERM_ATTR_BLINK:
|
|
val->boolean = state->pen.blink;
|
|
return 1;
|
|
|
|
case VTERM_ATTR_REVERSE:
|
|
val->boolean = state->pen.reverse;
|
|
return 1;
|
|
|
|
case VTERM_ATTR_CONCEAL:
|
|
val->boolean = state->pen.conceal;
|
|
return 1;
|
|
|
|
case VTERM_ATTR_STRIKE:
|
|
val->boolean = state->pen.strike;
|
|
return 1;
|
|
|
|
case VTERM_ATTR_FONT:
|
|
val->number = state->pen.font;
|
|
return 1;
|
|
|
|
case VTERM_ATTR_FOREGROUND:
|
|
val->color = state->pen.fg;
|
|
return 1;
|
|
|
|
case VTERM_ATTR_BACKGROUND:
|
|
val->color = state->pen.bg;
|
|
return 1;
|
|
|
|
case VTERM_ATTR_SMALL:
|
|
val->boolean = state->pen.small;
|
|
return 1;
|
|
|
|
case VTERM_ATTR_BASELINE:
|
|
val->number = state->pen.baseline;
|
|
return 1;
|
|
|
|
case VTERM_ATTR_URI:
|
|
val->number = state->pen.uri;
|
|
return 1;
|
|
|
|
case VTERM_ATTR_DIM:
|
|
val->boolean = state->pen.dim;
|
|
return 1;
|
|
|
|
case VTERM_ATTR_OVERLINE:
|
|
val->boolean = state->pen.overline;
|
|
return 1;
|
|
|
|
case VTERM_N_ATTRS:
|
|
return 0;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int attrs_differ(VTermAttrMask attrs, ScreenCell *a, ScreenCell *b)
|
|
{
|
|
if ((attrs & VTERM_ATTR_BOLD_MASK) && (a->pen.bold != b->pen.bold)) {
|
|
return 1;
|
|
}
|
|
if ((attrs & VTERM_ATTR_UNDERLINE_MASK) && (a->pen.underline != b->pen.underline)) {
|
|
return 1;
|
|
}
|
|
if ((attrs & VTERM_ATTR_ITALIC_MASK) && (a->pen.italic != b->pen.italic)) {
|
|
return 1;
|
|
}
|
|
if ((attrs & VTERM_ATTR_BLINK_MASK) && (a->pen.blink != b->pen.blink)) {
|
|
return 1;
|
|
}
|
|
if ((attrs & VTERM_ATTR_REVERSE_MASK) && (a->pen.reverse != b->pen.reverse)) {
|
|
return 1;
|
|
}
|
|
if ((attrs & VTERM_ATTR_CONCEAL_MASK) && (a->pen.conceal != b->pen.conceal)) {
|
|
return 1;
|
|
}
|
|
if ((attrs & VTERM_ATTR_STRIKE_MASK) && (a->pen.strike != b->pen.strike)) {
|
|
return 1;
|
|
}
|
|
if ((attrs & VTERM_ATTR_FONT_MASK) && (a->pen.font != b->pen.font)) {
|
|
return 1;
|
|
}
|
|
if ((attrs & VTERM_ATTR_FOREGROUND_MASK) && !vterm_color_is_equal(&a->pen.fg, &b->pen.fg)) {
|
|
return 1;
|
|
}
|
|
if ((attrs & VTERM_ATTR_BACKGROUND_MASK) && !vterm_color_is_equal(&a->pen.bg, &b->pen.bg)) {
|
|
return 1;
|
|
}
|
|
if ((attrs & VTERM_ATTR_SMALL_MASK) && (a->pen.small != b->pen.small)) {
|
|
return 1;
|
|
}
|
|
if ((attrs & VTERM_ATTR_BASELINE_MASK) && (a->pen.baseline != b->pen.baseline)) {
|
|
return 1;
|
|
}
|
|
if ((attrs & VTERM_ATTR_URI_MASK) && (a->pen.uri != b->pen.uri)) {
|
|
return 1;
|
|
}
|
|
if ((attrs & VTERM_ATTR_DIM_MASK) && (a->pen.dim != b->pen.dim)) {
|
|
return 1;
|
|
}
|
|
if ((attrs & VTERM_ATTR_OVERLINE_MASK) && (a->pen.overline != b->pen.overline)) {
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int vterm_screen_get_attrs_extent(const VTermScreen *screen, VTermRect *extent, VTermPos pos,
|
|
VTermAttrMask attrs)
|
|
{
|
|
ScreenCell *target = getcell(screen, pos.row, pos.col);
|
|
|
|
// TODO(vterm): bounds check
|
|
extent->start_row = pos.row;
|
|
extent->end_row = pos.row + 1;
|
|
|
|
if (extent->start_col < 0) {
|
|
extent->start_col = 0;
|
|
}
|
|
if (extent->end_col < 0) {
|
|
extent->end_col = screen->cols;
|
|
}
|
|
|
|
int col;
|
|
|
|
for (col = pos.col - 1; col >= extent->start_col; col--) {
|
|
if (attrs_differ(attrs, target, getcell(screen, pos.row, col))) {
|
|
break;
|
|
}
|
|
}
|
|
extent->start_col = col + 1;
|
|
|
|
for (col = pos.col + 1; col < extent->end_col; col++) {
|
|
if (attrs_differ(attrs, target, getcell(screen, pos.row, col))) {
|
|
break;
|
|
}
|
|
}
|
|
extent->end_col = col - 1;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/// Does not NUL-terminate the buffer
|
|
size_t vterm_screen_get_text(const VTermScreen *screen, char *buffer, size_t len,
|
|
const VTermRect rect)
|
|
{
|
|
size_t outpos = 0;
|
|
int padding = 0;
|
|
|
|
#define PUT(bytes, thislen) \
|
|
if (true) { \
|
|
if (buffer && outpos + thislen <= len) \
|
|
memcpy((char *)buffer + outpos, bytes, thislen); \
|
|
outpos += thislen; \
|
|
} \
|
|
|
|
for (int row = rect.start_row; row < rect.end_row; row++) {
|
|
for (int col = rect.start_col; col < rect.end_col; col++) {
|
|
ScreenCell *cell = getcell(screen, row, col);
|
|
|
|
if (cell->schar == 0) {
|
|
// Erased cell, might need a space
|
|
padding++;
|
|
} else if (cell->schar == (uint32_t)-1) {
|
|
// Gap behind a double-width char, do nothing
|
|
} else {
|
|
while (padding) {
|
|
PUT(" ", 1);
|
|
padding--;
|
|
}
|
|
char buf[MAX_SCHAR_SIZE + 1];
|
|
size_t thislen = schar_get(buf, cell->schar);
|
|
PUT(buf, thislen);
|
|
}
|
|
}
|
|
|
|
if (row < rect.end_row - 1) {
|
|
PUT("\n", 1);
|
|
padding = 0;
|
|
}
|
|
}
|
|
|
|
return outpos;
|
|
}
|
|
|
|
int vterm_screen_is_eol(const VTermScreen *screen, VTermPos pos)
|
|
{
|
|
// This cell is EOL if this and every cell to the right is black
|
|
for (; pos.col < screen->cols; pos.col++) {
|
|
ScreenCell *cell = getcell(screen, pos.row, pos.col);
|
|
if (cell->schar != 0) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
void vterm_state_get_cursorpos(const VTermState *state, VTermPos *cursorpos)
|
|
{
|
|
*cursorpos = state->pos;
|
|
}
|
|
|
|
void vterm_state_set_bold_highbright(VTermState *state, int bold_is_highbright)
|
|
{
|
|
state->bold_is_highbright = bold_is_highbright;
|
|
}
|
|
|
|
/// Compares two colours. Returns true if the colors are equal, false otherwise.
|
|
int vterm_color_is_equal(const VTermColor *a, const VTermColor *b)
|
|
{
|
|
// First make sure that the two colours are of the same type (RGB/Indexed)
|
|
if (a->type != b->type) {
|
|
return false;
|
|
}
|
|
|
|
// Depending on the type inspect the corresponding members
|
|
if (VTERM_COLOR_IS_INDEXED(a)) {
|
|
return a->indexed.idx == b->indexed.idx;
|
|
} else if (VTERM_COLOR_IS_RGB(a)) {
|
|
return (a->rgb.red == b->rgb.red)
|
|
&& (a->rgb.green == b->rgb.green)
|
|
&& (a->rgb.blue == b->rgb.blue);
|
|
}
|
|
|
|
return 0;
|
|
}
|