aboutsummaryrefslogtreecommitdiff
path: root/src/nvim/vterm
diff options
context:
space:
mode:
Diffstat (limited to 'src/nvim/vterm')
-rw-r--r--src/nvim/vterm/LICENSE23
-rw-r--r--src/nvim/vterm/README.md1
-rw-r--r--src/nvim/vterm/encoding.c278
-rw-r--r--src/nvim/vterm/encoding.h10
-rw-r--r--src/nvim/vterm/keyboard.c252
-rw-r--r--src/nvim/vterm/keyboard.h10
-rw-r--r--src/nvim/vterm/mouse.c113
-rw-r--r--src/nvim/vterm/mouse.h10
-rw-r--r--src/nvim/vterm/parser.c411
-rw-r--r--src/nvim/vterm/parser.h9
-rw-r--r--src/nvim/vterm/pen.c644
-rw-r--r--src/nvim/vterm/pen.h9
-rw-r--r--src/nvim/vterm/screen.c1103
-rw-r--r--src/nvim/vterm/screen.h9
-rw-r--r--src/nvim/vterm/state.c2310
-rw-r--r--src/nvim/vterm/state.h7
-rw-r--r--src/nvim/vterm/vterm.c335
-rw-r--r--src/nvim/vterm/vterm.h161
-rw-r--r--src/nvim/vterm/vterm_defs.h319
-rw-r--r--src/nvim/vterm/vterm_internal_defs.h266
-rw-r--r--src/nvim/vterm/vterm_keycodes_defs.h58
21 files changed, 6338 insertions, 0 deletions
diff --git a/src/nvim/vterm/LICENSE b/src/nvim/vterm/LICENSE
new file mode 100644
index 0000000000..0d051634b2
--- /dev/null
+++ b/src/nvim/vterm/LICENSE
@@ -0,0 +1,23 @@
+
+
+The MIT License
+
+Copyright (c) 2008 Paul Evans <leonerd@leonerd.org.uk>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/src/nvim/vterm/README.md b/src/nvim/vterm/README.md
new file mode 100644
index 0000000000..4cc43bfb94
--- /dev/null
+++ b/src/nvim/vterm/README.md
@@ -0,0 +1 @@
+Adopted from [libvterm](https://www.leonerd.org.uk/code/libvterm/)
diff --git a/src/nvim/vterm/encoding.c b/src/nvim/vterm/encoding.c
new file mode 100644
index 0000000000..cc3208cfa2
--- /dev/null
+++ b/src/nvim/vterm/encoding.c
@@ -0,0 +1,278 @@
+#include "nvim/vterm/encoding.h"
+#include "nvim/vterm/vterm_internal_defs.h"
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "vterm/encoding.c.generated.h"
+#endif
+
+#define UNICODE_INVALID 0xFFFD
+
+#if defined(DEBUG) && DEBUG > 1
+# define DEBUG_PRINT_UTF8
+#endif
+
+struct UTF8DecoderData {
+ // number of bytes remaining in this codepoint
+ int bytes_remaining;
+
+ // number of bytes total in this codepoint once it's finished
+ // (for detecting overlongs)
+ int bytes_total;
+
+ int this_cp;
+};
+
+static void init_utf8(VTermEncoding *enc, void *data_)
+{
+ struct UTF8DecoderData *data = data_;
+
+ data->bytes_remaining = 0;
+ data->bytes_total = 0;
+}
+
+static void decode_utf8(VTermEncoding *enc, void *data_, uint32_t cp[], int *cpi, int cplen,
+ const char bytes[], size_t *pos, size_t bytelen)
+{
+ struct UTF8DecoderData *data = data_;
+
+#ifdef DEBUG_PRINT_UTF8
+ printf("BEGIN UTF-8\n");
+#endif
+
+ for (; *pos < bytelen && *cpi < cplen; (*pos)++) {
+ uint8_t c = (uint8_t)bytes[*pos];
+
+#ifdef DEBUG_PRINT_UTF8
+ printf(" pos=%zd c=%02x rem=%d\n", *pos, c, data->bytes_remaining);
+#endif
+
+ if (c < 0x20) { // C0
+ return;
+ } else if (c >= 0x20 && c < 0x7f) {
+ if (data->bytes_remaining) {
+ cp[(*cpi)++] = UNICODE_INVALID;
+ }
+
+ cp[(*cpi)++] = c;
+#ifdef DEBUG_PRINT_UTF8
+ printf(" UTF-8 char: U+%04x\n", c);
+#endif
+ data->bytes_remaining = 0;
+ } else if (c == 0x7f) { // DEL
+ return;
+ } else if (c >= 0x80 && c < 0xc0) {
+ if (!data->bytes_remaining) {
+ cp[(*cpi)++] = UNICODE_INVALID;
+ continue;
+ }
+
+ data->this_cp <<= 6;
+ data->this_cp |= c & 0x3f;
+ data->bytes_remaining--;
+
+ if (!data->bytes_remaining) {
+#ifdef DEBUG_PRINT_UTF8
+ printf(" UTF-8 raw char U+%04x bytelen=%d ", data->this_cp, data->bytes_total);
+#endif
+ // Check for overlong sequences
+ switch (data->bytes_total) {
+ case 2:
+ if (data->this_cp < 0x0080) {
+ data->this_cp = UNICODE_INVALID;
+ }
+ break;
+ case 3:
+ if (data->this_cp < 0x0800) {
+ data->this_cp = UNICODE_INVALID;
+ }
+ break;
+ case 4:
+ if (data->this_cp < 0x10000) {
+ data->this_cp = UNICODE_INVALID;
+ }
+ break;
+ case 5:
+ if (data->this_cp < 0x200000) {
+ data->this_cp = UNICODE_INVALID;
+ }
+ break;
+ case 6:
+ if (data->this_cp < 0x4000000) {
+ data->this_cp = UNICODE_INVALID;
+ }
+ break;
+ }
+ // Now look for plain invalid ones
+ if ((data->this_cp >= 0xD800 && data->this_cp <= 0xDFFF)
+ || data->this_cp == 0xFFFE
+ || data->this_cp == 0xFFFF) {
+ data->this_cp = UNICODE_INVALID;
+ }
+#ifdef DEBUG_PRINT_UTF8
+ printf(" char: U+%04x\n", data->this_cp);
+#endif
+ cp[(*cpi)++] = (uint32_t)data->this_cp;
+ }
+ } else if (c >= 0xc0 && c < 0xe0) {
+ if (data->bytes_remaining) {
+ cp[(*cpi)++] = UNICODE_INVALID;
+ }
+
+ data->this_cp = c & 0x1f;
+ data->bytes_total = 2;
+ data->bytes_remaining = 1;
+ } else if (c >= 0xe0 && c < 0xf0) {
+ if (data->bytes_remaining) {
+ cp[(*cpi)++] = UNICODE_INVALID;
+ }
+
+ data->this_cp = c & 0x0f;
+ data->bytes_total = 3;
+ data->bytes_remaining = 2;
+ } else if (c >= 0xf0 && c < 0xf8) {
+ if (data->bytes_remaining) {
+ cp[(*cpi)++] = UNICODE_INVALID;
+ }
+
+ data->this_cp = c & 0x07;
+ data->bytes_total = 4;
+ data->bytes_remaining = 3;
+ } else if (c >= 0xf8 && c < 0xfc) {
+ if (data->bytes_remaining) {
+ cp[(*cpi)++] = UNICODE_INVALID;
+ }
+
+ data->this_cp = c & 0x03;
+ data->bytes_total = 5;
+ data->bytes_remaining = 4;
+ } else if (c >= 0xfc && c < 0xfe) {
+ if (data->bytes_remaining) {
+ cp[(*cpi)++] = UNICODE_INVALID;
+ }
+
+ data->this_cp = c & 0x01;
+ data->bytes_total = 6;
+ data->bytes_remaining = 5;
+ } else {
+ cp[(*cpi)++] = UNICODE_INVALID;
+ }
+ }
+}
+
+static VTermEncoding encoding_utf8 = {
+ .init = &init_utf8,
+ .decode = &decode_utf8,
+};
+
+static void decode_usascii(VTermEncoding *enc, void *data, uint32_t cp[], int *cpi, int cplen,
+ const char bytes[], size_t *pos, size_t bytelen)
+{
+ int is_gr = bytes[*pos] & 0x80;
+
+ for (; *pos < bytelen && *cpi < cplen; (*pos)++) {
+ uint8_t c = (uint8_t)(bytes[*pos] ^ is_gr);
+
+ if (c < 0x20 || c == 0x7f || c >= 0x80) {
+ return;
+ }
+
+ cp[(*cpi)++] = c;
+ }
+}
+
+static VTermEncoding encoding_usascii = {
+ .decode = &decode_usascii,
+};
+
+struct StaticTableEncoding {
+ const VTermEncoding enc;
+ const uint32_t chars[128];
+};
+
+static void decode_table(VTermEncoding *enc, void *data, uint32_t cp[], int *cpi, int cplen,
+ const char bytes[], size_t *pos, size_t bytelen)
+{
+ struct StaticTableEncoding *table = (struct StaticTableEncoding *)enc;
+ int is_gr = bytes[*pos] & 0x80;
+
+ for (; *pos < bytelen && *cpi < cplen; (*pos)++) {
+ uint8_t c = (uint8_t)(bytes[*pos] ^ is_gr);
+
+ if (c < 0x20 || c == 0x7f || c >= 0x80) {
+ return;
+ }
+
+ if (table->chars[c]) {
+ cp[(*cpi)++] = table->chars[c];
+ } else {
+ cp[(*cpi)++] = c;
+ }
+ }
+}
+
+static const struct StaticTableEncoding encoding_DECdrawing = {
+ { .decode = &decode_table },
+ {
+ [0x60] = 0x25C6, // BLACK DIAMOND
+ [0x61] = 0x2592, // MEDIUM SHADE (checkerboard)
+ [0x62] = 0x2409, // SYMBOL FOR HORIZONTAL TAB
+ [0x63] = 0x240C, // SYMBOL FOR FORM FEED
+ [0x64] = 0x240D, // SYMBOL FOR CARRIAGE RETURN
+ [0x65] = 0x240A, // SYMBOL FOR LINE FEED
+ [0x66] = 0x00B0, // DEGREE SIGN
+ [0x67] = 0x00B1, // PLUS-MINUS SIGN (plus or minus)
+ [0x68] = 0x2424, // SYMBOL FOR NEW LINE
+ [0x69] = 0x240B, // SYMBOL FOR VERTICAL TAB
+ [0x6a] = 0x2518, // BOX DRAWINGS LIGHT UP AND LEFT (bottom-right corner)
+ [0x6b] = 0x2510, // BOX DRAWINGS LIGHT DOWN AND LEFT (top-right corner)
+ [0x6c] = 0x250C, // BOX DRAWINGS LIGHT DOWN AND RIGHT (top-left corner)
+ [0x6d] = 0x2514, // BOX DRAWINGS LIGHT UP AND RIGHT (bottom-left corner)
+ [0x6e] = 0x253C, // BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL (crossing lines)
+ [0x6f] = 0x23BA, // HORIZONTAL SCAN LINE-1
+ [0x70] = 0x23BB, // HORIZONTAL SCAN LINE-3
+ [0x71] = 0x2500, // BOX DRAWINGS LIGHT HORIZONTAL
+ [0x72] = 0x23BC, // HORIZONTAL SCAN LINE-7
+ [0x73] = 0x23BD, // HORIZONTAL SCAN LINE-9
+ [0x74] = 0x251C, // BOX DRAWINGS LIGHT VERTICAL AND RIGHT
+ [0x75] = 0x2524, // BOX DRAWINGS LIGHT VERTICAL AND LEFT
+ [0x76] = 0x2534, // BOX DRAWINGS LIGHT UP AND HORIZONTAL
+ [0x77] = 0x252C, // BOX DRAWINGS LIGHT DOWN AND HORIZONTAL
+ [0x78] = 0x2502, // BOX DRAWINGS LIGHT VERTICAL
+ [0x79] = 0x2A7D, // LESS-THAN OR SLANTED EQUAL-TO
+ [0x7a] = 0x2A7E, // GREATER-THAN OR SLANTED EQUAL-TO
+ [0x7b] = 0x03C0, // GREEK SMALL LETTER PI
+ [0x7c] = 0x2260, // NOT EQUAL TO
+ [0x7d] = 0x00A3, // POUND SIGN
+ [0x7e] = 0x00B7, // MIDDLE DOT
+ }
+};
+
+static const struct StaticTableEncoding encoding_uk = {
+ { .decode = &decode_table },
+ {
+ [0x23] = 0x00a3, // £
+ }
+};
+
+static struct {
+ VTermEncodingType type;
+ char designation;
+ VTermEncoding *enc;
+}
+encodings[] = {
+ { ENC_UTF8, 'u', &encoding_utf8 },
+ { ENC_SINGLE_94, '0', (VTermEncoding *)&encoding_DECdrawing },
+ { ENC_SINGLE_94, 'A', (VTermEncoding *)&encoding_uk },
+ { ENC_SINGLE_94, 'B', &encoding_usascii },
+ { 0 },
+};
+
+VTermEncoding *vterm_lookup_encoding(VTermEncodingType type, char designation)
+{
+ for (int i = 0; encodings[i].designation; i++) {
+ if (encodings[i].type == type && encodings[i].designation == designation) {
+ return encodings[i].enc;
+ }
+ }
+ return NULL;
+}
diff --git a/src/nvim/vterm/encoding.h b/src/nvim/vterm/encoding.h
new file mode 100644
index 0000000000..204b6d90c9
--- /dev/null
+++ b/src/nvim/vterm/encoding.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <stddef.h>
+
+#include "nvim/vterm/vterm_defs.h"
+#include "nvim/vterm/vterm_internal_defs.h"
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "vterm/encoding.h.generated.h"
+#endif
diff --git a/src/nvim/vterm/keyboard.c b/src/nvim/vterm/keyboard.c
new file mode 100644
index 0000000000..696b09157e
--- /dev/null
+++ b/src/nvim/vterm/keyboard.c
@@ -0,0 +1,252 @@
+#include <stdio.h>
+
+#include "nvim/ascii_defs.h"
+#include "nvim/tui/termkey/termkey.h"
+#include "nvim/vterm/keyboard.h"
+#include "nvim/vterm/vterm.h"
+#include "nvim/vterm/vterm_internal_defs.h"
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "vterm/keyboard.c.generated.h"
+#endif
+
+void vterm_keyboard_unichar(VTerm *vt, uint32_t c, VTermModifier mod)
+{
+ // The shift modifier is never important for Unicode characters apart from Space
+ if (c != ' ') {
+ mod &= (unsigned)~VTERM_MOD_SHIFT;
+ }
+
+ if (mod == 0) {
+ // Normal text - ignore just shift
+ char str[6];
+ int seqlen = fill_utf8((int)c, str);
+ vterm_push_output_bytes(vt, str, (size_t)seqlen);
+ return;
+ }
+
+ int needs_CSIu;
+ switch (c) {
+ // Special Ctrl- letters that can't be represented elsewise
+ case 'i':
+ case 'j':
+ case 'm':
+ case '[':
+ needs_CSIu = 1;
+ break;
+ // Ctrl-\ ] ^ _ don't need CSUu
+ case '\\':
+ case ']':
+ case '^':
+ case '_':
+ needs_CSIu = 0;
+ break;
+ // Shift-space needs CSIu
+ case ' ':
+ needs_CSIu = !!(mod & VTERM_MOD_SHIFT);
+ break;
+ // All other characters needs CSIu except for letters a-z
+ default:
+ needs_CSIu = (c < 'a' || c > 'z');
+ }
+
+ // ALT we can just prefix with ESC; anything else requires CSI u
+ if (needs_CSIu && (mod & (unsigned)~VTERM_MOD_ALT)) {
+ vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d;%du", c, mod + 1);
+ return;
+ }
+
+ if (mod & VTERM_MOD_CTRL) {
+ c &= 0x1f;
+ }
+
+ vterm_push_output_sprintf(vt, "%s%c", mod & VTERM_MOD_ALT ? ESC_S : "", c);
+}
+
+typedef struct {
+ enum {
+ KEYCODE_NONE,
+ KEYCODE_LITERAL,
+ KEYCODE_TAB,
+ KEYCODE_ENTER,
+ KEYCODE_SS3,
+ KEYCODE_CSI,
+ KEYCODE_CSI_CURSOR,
+ KEYCODE_CSINUM,
+ KEYCODE_KEYPAD,
+ } type;
+ char literal;
+ int csinum;
+} keycodes_s;
+
+static keycodes_s keycodes[] = {
+ { KEYCODE_NONE, NUL, 0 }, // NONE
+
+ { KEYCODE_ENTER, '\r', 0 }, // ENTER
+ { KEYCODE_TAB, '\t', 0 }, // TAB
+ { KEYCODE_LITERAL, '\x7f', 0 }, // BACKSPACE == ASCII DEL
+ { KEYCODE_LITERAL, '\x1b', 0 }, // ESCAPE
+
+ { KEYCODE_CSI_CURSOR, 'A', 0 }, // UP
+ { KEYCODE_CSI_CURSOR, 'B', 0 }, // DOWN
+ { KEYCODE_CSI_CURSOR, 'D', 0 }, // LEFT
+ { KEYCODE_CSI_CURSOR, 'C', 0 }, // RIGHT
+
+ { KEYCODE_CSINUM, '~', 2 }, // INS
+ { KEYCODE_CSINUM, '~', 3 }, // DEL
+ { KEYCODE_CSI_CURSOR, 'H', 0 }, // HOME
+ { KEYCODE_CSI_CURSOR, 'F', 0 }, // END
+ { KEYCODE_CSINUM, '~', 5 }, // PAGEUP
+ { KEYCODE_CSINUM, '~', 6 }, // PAGEDOWN
+};
+
+static keycodes_s keycodes_fn[] = {
+ { KEYCODE_NONE, NUL, 0 }, // F0 - shouldn't happen
+ { KEYCODE_SS3, 'P', 0 }, // F1
+ { KEYCODE_SS3, 'Q', 0 }, // F2
+ { KEYCODE_SS3, 'R', 0 }, // F3
+ { KEYCODE_SS3, 'S', 0 }, // F4
+ { KEYCODE_CSINUM, '~', 15 }, // F5
+ { KEYCODE_CSINUM, '~', 17 }, // F6
+ { KEYCODE_CSINUM, '~', 18 }, // F7
+ { KEYCODE_CSINUM, '~', 19 }, // F8
+ { KEYCODE_CSINUM, '~', 20 }, // F9
+ { KEYCODE_CSINUM, '~', 21 }, // F10
+ { KEYCODE_CSINUM, '~', 23 }, // F11
+ { KEYCODE_CSINUM, '~', 24 }, // F12
+};
+
+static keycodes_s keycodes_kp[] = {
+ { KEYCODE_KEYPAD, '0', 'p' }, // KP_0
+ { KEYCODE_KEYPAD, '1', 'q' }, // KP_1
+ { KEYCODE_KEYPAD, '2', 'r' }, // KP_2
+ { KEYCODE_KEYPAD, '3', 's' }, // KP_3
+ { KEYCODE_KEYPAD, '4', 't' }, // KP_4
+ { KEYCODE_KEYPAD, '5', 'u' }, // KP_5
+ { KEYCODE_KEYPAD, '6', 'v' }, // KP_6
+ { KEYCODE_KEYPAD, '7', 'w' }, // KP_7
+ { KEYCODE_KEYPAD, '8', 'x' }, // KP_8
+ { KEYCODE_KEYPAD, '9', 'y' }, // KP_9
+ { KEYCODE_KEYPAD, '*', 'j' }, // KP_MULT
+ { KEYCODE_KEYPAD, '+', 'k' }, // KP_PLUS
+ { KEYCODE_KEYPAD, ',', 'l' }, // KP_COMMA
+ { KEYCODE_KEYPAD, '-', 'm' }, // KP_MINUS
+ { KEYCODE_KEYPAD, '.', 'n' }, // KP_PERIOD
+ { KEYCODE_KEYPAD, '/', 'o' }, // KP_DIVIDE
+ { KEYCODE_KEYPAD, '\n', 'M' }, // KP_ENTER
+ { KEYCODE_KEYPAD, '=', 'X' }, // KP_EQUAL
+};
+
+void vterm_keyboard_key(VTerm *vt, VTermKey key, VTermModifier mod)
+{
+ if (key == VTERM_KEY_NONE) {
+ return;
+ }
+
+ keycodes_s k;
+ if (key < VTERM_KEY_FUNCTION_0) {
+ if (key >= sizeof(keycodes)/sizeof(keycodes[0])) {
+ return;
+ }
+ k = keycodes[key];
+ } else if (key >= VTERM_KEY_FUNCTION_0 && key <= VTERM_KEY_FUNCTION_MAX) {
+ if ((key - VTERM_KEY_FUNCTION_0) >= sizeof(keycodes_fn)/sizeof(keycodes_fn[0])) {
+ return;
+ }
+ k = keycodes_fn[key - VTERM_KEY_FUNCTION_0];
+ } else if (key >= VTERM_KEY_KP_0) {
+ if ((key - VTERM_KEY_KP_0) >= sizeof(keycodes_kp)/sizeof(keycodes_kp[0])) {
+ return;
+ }
+ k = keycodes_kp[key - VTERM_KEY_KP_0];
+ }
+
+ switch (k.type) {
+ case KEYCODE_NONE:
+ break;
+
+ case KEYCODE_TAB:
+ // Shift-Tab is CSI Z but plain Tab is 0x09
+ if (mod == VTERM_MOD_SHIFT) {
+ vterm_push_output_sprintf_ctrl(vt, C1_CSI, "Z");
+ } else if (mod & VTERM_MOD_SHIFT) {
+ vterm_push_output_sprintf_ctrl(vt, C1_CSI, "1;%dZ", mod + 1);
+ } else {
+ goto case_LITERAL;
+ }
+ break;
+
+ case KEYCODE_ENTER:
+ // Enter is CRLF in newline mode, but just LF in linefeed
+ if (vt->state->mode.newline) {
+ vterm_push_output_sprintf(vt, "\r\n");
+ } else {
+ goto case_LITERAL;
+ }
+ break;
+
+ case KEYCODE_LITERAL:
+ case_LITERAL:
+ if (mod & (VTERM_MOD_SHIFT|VTERM_MOD_CTRL)) {
+ vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d;%du", k.literal, mod + 1);
+ } else {
+ vterm_push_output_sprintf(vt, mod & VTERM_MOD_ALT ? ESC_S "%c" : "%c", k.literal);
+ }
+ break;
+
+ case KEYCODE_SS3:
+ case_SS3:
+ if (mod == 0) {
+ vterm_push_output_sprintf_ctrl(vt, C1_SS3, "%c", k.literal);
+ } else {
+ goto case_CSI;
+ }
+ break;
+
+ case KEYCODE_CSI:
+ case_CSI:
+ if (mod == 0) {
+ vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%c", k.literal);
+ } else {
+ vterm_push_output_sprintf_ctrl(vt, C1_CSI, "1;%d%c", mod + 1, k.literal);
+ }
+ break;
+
+ case KEYCODE_CSINUM:
+ if (mod == 0) {
+ vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d%c", k.csinum, k.literal);
+ } else {
+ vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d;%d%c", k.csinum, mod + 1, k.literal);
+ }
+ break;
+
+ case KEYCODE_CSI_CURSOR:
+ if (vt->state->mode.cursor) {
+ goto case_SS3;
+ } else {
+ goto case_CSI;
+ }
+
+ case KEYCODE_KEYPAD:
+ if (vt->state->mode.keypad) {
+ k.literal = (char)k.csinum;
+ goto case_SS3;
+ } else {
+ goto case_LITERAL;
+ }
+ }
+}
+
+void vterm_keyboard_start_paste(VTerm *vt)
+{
+ if (vt->state->mode.bracketpaste) {
+ vterm_push_output_sprintf_ctrl(vt, C1_CSI, "200~");
+ }
+}
+
+void vterm_keyboard_end_paste(VTerm *vt)
+{
+ if (vt->state->mode.bracketpaste) {
+ vterm_push_output_sprintf_ctrl(vt, C1_CSI, "201~");
+ }
+}
diff --git a/src/nvim/vterm/keyboard.h b/src/nvim/vterm/keyboard.h
new file mode 100644
index 0000000000..af5f4a3ed1
--- /dev/null
+++ b/src/nvim/vterm/keyboard.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <stddef.h>
+
+#include "nvim/vterm/vterm_defs.h"
+#include "nvim/vterm/vterm_keycodes_defs.h"
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "vterm/keyboard.h.generated.h"
+#endif
diff --git a/src/nvim/vterm/mouse.c b/src/nvim/vterm/mouse.c
new file mode 100644
index 0000000000..9b8be4b60a
--- /dev/null
+++ b/src/nvim/vterm/mouse.c
@@ -0,0 +1,113 @@
+#include "nvim/tui/termkey/termkey.h"
+#include "nvim/vterm/mouse.h"
+#include "nvim/vterm/vterm.h"
+#include "nvim/vterm/vterm_internal_defs.h"
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "vterm/mouse.c.generated.h"
+#endif
+
+static void output_mouse(VTermState *state, int code, int pressed, int modifiers, int col, int row)
+{
+ modifiers <<= 2;
+
+ switch (state->mouse_protocol) {
+ case MOUSE_X10:
+ if (col + 0x21 > 0xff) {
+ col = 0xff - 0x21;
+ }
+ if (row + 0x21 > 0xff) {
+ row = 0xff - 0x21;
+ }
+
+ if (!pressed) {
+ code = 3;
+ }
+
+ vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "M%c%c%c",
+ (code | modifiers) + 0x20, col + 0x21, row + 0x21);
+ break;
+
+ case MOUSE_UTF8: {
+ char utf8[18];
+ size_t len = 0;
+
+ if (!pressed) {
+ code = 3;
+ }
+
+ len += (size_t)fill_utf8((code | modifiers) + 0x20, utf8 + len);
+ len += (size_t)fill_utf8(col + 0x21, utf8 + len);
+ len += (size_t)fill_utf8(row + 0x21, utf8 + len);
+ utf8[len] = 0;
+
+ vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "M%s", utf8);
+ }
+ break;
+
+ case MOUSE_SGR:
+ vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "<%d;%d;%d%c",
+ code | modifiers, col + 1, row + 1, pressed ? 'M' : 'm');
+ break;
+
+ case MOUSE_RXVT:
+ if (!pressed) {
+ code = 3;
+ }
+
+ vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "%d;%d;%dM",
+ code | modifiers, col + 1, row + 1);
+ break;
+ }
+}
+
+void vterm_mouse_move(VTerm *vt, int row, int col, VTermModifier mod)
+{
+ VTermState *state = vt->state;
+
+ if (col == state->mouse_col && row == state->mouse_row) {
+ return;
+ }
+
+ state->mouse_col = col;
+ state->mouse_row = row;
+
+ if ((state->mouse_flags & MOUSE_WANT_DRAG && state->mouse_buttons)
+ || (state->mouse_flags & MOUSE_WANT_MOVE)) {
+ int button = state->mouse_buttons & 0x01 ? 1
+ : state->mouse_buttons & 0x02 ? 2
+ : state->mouse_buttons &
+ 0x04 ? 3 : 4;
+ output_mouse(state, button - 1 + 0x20, 1, (int)mod, col, row);
+ }
+}
+
+void vterm_mouse_button(VTerm *vt, int button, bool pressed, VTermModifier mod)
+{
+ VTermState *state = vt->state;
+
+ int old_buttons = state->mouse_buttons;
+
+ if (button > 0 && button <= 3) {
+ if (pressed) {
+ state->mouse_buttons |= (1 << (button - 1));
+ } else {
+ state->mouse_buttons &= ~(1 << (button - 1));
+ }
+ }
+
+ // Most of the time we don't get button releases from 4/5
+ if (state->mouse_buttons == old_buttons && button < 4) {
+ return;
+ }
+
+ if (!state->mouse_flags) {
+ return;
+ }
+
+ if (button < 4) {
+ output_mouse(state, button - 1, pressed, (int)mod, state->mouse_col, state->mouse_row);
+ } else if (button < 8) {
+ output_mouse(state, button - 4 + 0x40, pressed, (int)mod, state->mouse_col, state->mouse_row);
+ }
+}
diff --git a/src/nvim/vterm/mouse.h b/src/nvim/vterm/mouse.h
new file mode 100644
index 0000000000..477f4028a2
--- /dev/null
+++ b/src/nvim/vterm/mouse.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <stddef.h>
+
+#include "nvim/vterm/vterm_defs.h"
+#include "nvim/vterm/vterm_keycodes_defs.h"
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "vterm/mouse.h.generated.h"
+#endif
diff --git a/src/nvim/vterm/parser.c b/src/nvim/vterm/parser.c
new file mode 100644
index 0000000000..79d348f2c1
--- /dev/null
+++ b/src/nvim/vterm/parser.c
@@ -0,0 +1,411 @@
+#include <assert.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "nvim/vterm/parser.h"
+#include "nvim/vterm/vterm.h"
+#include "nvim/vterm/vterm_internal_defs.h"
+
+#ifdef INCLUDE_GENERATED_DECLARATIONS
+# include "vterm/parser.c.generated.h"
+#endif
+
+#undef DEBUG_PARSER
+
+static bool is_intermed(uint8_t c)
+{
+ return c >= 0x20 && c <= 0x2f;
+}
+
+static void do_control(VTerm *vt, uint8_t control)
+{
+ if (vt->parser.callbacks && vt->parser.callbacks->control) {
+ if ((*vt->parser.callbacks->control)(control, vt->parser.cbdata)) {
+ return;
+ }
+ }
+
+ DEBUG_LOG("libvterm: Unhandled control 0x%02x\n", control);
+}
+
+static void do_csi(VTerm *vt, char command)
+{
+#ifdef DEBUG_PARSER
+ printf("Parsed CSI args as:\n", arglen, args);
+ printf(" leader: %s\n", vt->parser.v.csi.leader);
+ for (int argi = 0; argi < vt->parser.v.csi.argi; argi++) {
+ printf(" %lu", CSI_ARG(vt->parser.v.csi.args[argi]));
+ if (!CSI_ARG_HAS_MORE(vt->parser.v.csi.args[argi])) {
+ printf("\n");
+ }
+ printf(" intermed: %s\n", vt->parser.intermed);
+ }
+#endif
+
+ if (vt->parser.callbacks && vt->parser.callbacks->csi) {
+ if ((*vt->parser.callbacks->csi)(vt->parser.v.csi.leaderlen ? vt->parser.v.csi.leader : NULL,
+ vt->parser.v.csi.args,
+ vt->parser.v.csi.argi,
+ vt->parser.intermedlen ? vt->parser.intermed : NULL,
+ command,
+ vt->parser.cbdata)) {
+ return;
+ }
+ }
+
+ DEBUG_LOG("libvterm: Unhandled CSI %c\n", command);
+}
+
+static void do_escape(VTerm *vt, char command)
+{
+ char seq[INTERMED_MAX + 1];
+
+ size_t len = (size_t)vt->parser.intermedlen;
+ strncpy(seq, vt->parser.intermed, len); // NOLINT(runtime/printf)
+ seq[len++] = command;
+ seq[len] = 0;
+
+ if (vt->parser.callbacks && vt->parser.callbacks->escape) {
+ if ((*vt->parser.callbacks->escape)(seq, len, vt->parser.cbdata)) {
+ return;
+ }
+ }
+
+ DEBUG_LOG("libvterm: Unhandled escape ESC 0x%02x\n", command);
+}
+
+static void string_fragment(VTerm *vt, const char *str, size_t len, bool final)
+{
+ VTermStringFragment frag = {
+ .str = str,
+ .len = len,
+ .initial = vt->parser.string_initial,
+ .final = final,
+ };
+
+ switch (vt->parser.state) {
+ case OSC:
+ if (vt->parser.callbacks && vt->parser.callbacks->osc) {
+ (*vt->parser.callbacks->osc)(vt->parser.v.osc.command, frag, vt->parser.cbdata);
+ }
+ break;
+
+ case DCS_VTERM:
+ if (vt->parser.callbacks && vt->parser.callbacks->dcs) {
+ (*vt->parser.callbacks->dcs)(vt->parser.v.dcs.command, (size_t)vt->parser.v.dcs.commandlen,
+ frag,
+ vt->parser.cbdata);
+ }
+ break;
+
+ case APC:
+ if (vt->parser.callbacks && vt->parser.callbacks->apc) {
+ (*vt->parser.callbacks->apc)(frag, vt->parser.cbdata);
+ }
+ break;
+
+ case PM:
+ if (vt->parser.callbacks && vt->parser.callbacks->pm) {
+ (*vt->parser.callbacks->pm)(frag, vt->parser.cbdata);
+ }
+ break;
+
+ case SOS:
+ if (vt->parser.callbacks && vt->parser.callbacks->sos) {
+ (*vt->parser.callbacks->sos)(frag, vt->parser.cbdata);
+ }
+ break;
+
+ case NORMAL:
+ case CSI_LEADER:
+ case CSI_ARGS:
+ case CSI_INTERMED:
+ case OSC_COMMAND:
+ case DCS_COMMAND:
+ break;
+ }
+
+ vt->parser.string_initial = false;
+}
+
+size_t vterm_input_write(VTerm *vt, const char *bytes, size_t len)
+{
+ size_t pos = 0;
+ const char *string_start;
+
+ switch (vt->parser.state) {
+ case NORMAL:
+ case CSI_LEADER:
+ case CSI_ARGS:
+ case CSI_INTERMED:
+ case OSC_COMMAND:
+ case DCS_COMMAND:
+ string_start = NULL;
+ break;
+ case OSC:
+ case DCS_VTERM:
+ case APC:
+ case PM:
+ case SOS:
+ string_start = bytes;
+ break;
+ }
+
+#define ENTER_STATE(st) do { vt->parser.state = st; string_start = NULL; } while (0)
+#define ENTER_NORMAL_STATE() ENTER_STATE(NORMAL)
+
+#define IS_STRING_STATE() (vt->parser.state >= OSC_COMMAND)
+
+ for (; pos < len; pos++) {
+ uint8_t c = (uint8_t)bytes[pos];
+ bool c1_allowed = !vt->mode.utf8;
+
+ if (c == 0x00 || c == 0x7f) { // NUL, DEL
+ if (IS_STRING_STATE()) {
+ string_fragment(vt, string_start, (size_t)(bytes + pos - string_start), false);
+ string_start = bytes + pos + 1;
+ }
+ if (vt->parser.emit_nul) {
+ do_control(vt, c);
+ }
+ continue;
+ }
+ if (c == 0x18 || c == 0x1a) { // CAN, SUB
+ vt->parser.in_esc = false;
+ ENTER_NORMAL_STATE();
+ if (vt->parser.emit_nul) {
+ do_control(vt, c);
+ }
+ continue;
+ } else if (c == 0x1b) { // ESC
+ vt->parser.intermedlen = 0;
+ if (!IS_STRING_STATE()) {
+ vt->parser.state = NORMAL;
+ }
+ vt->parser.in_esc = true;
+ continue;
+ } else if (c == 0x07 // BEL, can stand for ST in OSC or DCS state
+ && IS_STRING_STATE()) {} else if (c < 0x20) { // other C0
+ if (vt->parser.state == SOS) {
+ continue; // All other C0s permitted in SOS
+ }
+ if (IS_STRING_STATE()) {
+ string_fragment(vt, string_start, (size_t)(bytes + pos - string_start), false);
+ }
+ do_control(vt, c);
+ if (IS_STRING_STATE()) {
+ string_start = bytes + pos + 1;
+ }
+ continue;
+ }
+
+ size_t string_len = (size_t)(bytes + pos - string_start);
+
+ if (vt->parser.in_esc) {
+ // Hoist an ESC letter into a C1 if we're not in a string mode
+ // Always accept ESC \ == ST even in string mode
+ if (!vt->parser.intermedlen
+ && c >= 0x40 && c < 0x60
+ && ((!IS_STRING_STATE() || c == 0x5c))) {
+ c += 0x40;
+ c1_allowed = true;
+ if (string_len) {
+ assert(string_len > 0);
+ string_len -= 1;
+ }
+ vt->parser.in_esc = false;
+ } else {
+ string_start = NULL;
+ vt->parser.state = NORMAL;
+ }
+ }
+
+ switch (vt->parser.state) {
+ case CSI_LEADER:
+ // Extract leader bytes 0x3c to 0x3f
+ if (c >= 0x3c && c <= 0x3f) {
+ if (vt->parser.v.csi.leaderlen < CSI_LEADER_MAX - 1) {
+ vt->parser.v.csi.leader[vt->parser.v.csi.leaderlen++] = (char)c;
+ }
+ break;
+ }
+
+ vt->parser.v.csi.leader[vt->parser.v.csi.leaderlen] = 0;
+