From 289832404790f8d594a5fe308d1ff280b45cc9f8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 5 Mar 2026 19:43:05 +0530 Subject: [PATCH] Start work on DnD protocol --- docs/conf.py | 3 +- gen/apc_parsers.py | 10 ++ kitty/control-codes.h | 2 + kitty/data-types.c | 1 + kitty/fast_data_types.pyi | 1 + kitty/parse-dnd-command.h | 190 ++++++++++++++++++++++++++++++++++++++ kitty/screen.c | 12 +++ kitty/screen.h | 7 ++ kitty/vt-parser.c | 3 + 9 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 kitty/parse-dnd-command.h diff --git a/docs/conf.py b/docs/conf.py index 55d4e5ecb..016fa5d3a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,7 +30,7 @@ if kitty_src not in sys.path: from kitty.conf.types import Definition, expand_opt_references # noqa from kitty.constants import str_version, website_url # noqa -from kitty.fast_data_types import Shlex, TEXT_SIZE_CODE # noqa +from kitty.fast_data_types import DND_CODE, Shlex, TEXT_SIZE_CODE # noqa # config {{{ # -- Project information ----------------------------------------------------- @@ -121,6 +121,7 @@ string_replacements = { '_kitty_install_cmd': 'curl -L https://sw.kovidgoyal.net/kitty/installer.sh | sh /dev/stdin', '_build_go_version': go_version('../go.mod'), '_text_size_code': str(TEXT_SIZE_CODE), + '_dnd_code': str(DND_CODE), } diff --git a/gen/apc_parsers.py b/gen/apc_parsers.py index 45faca68e..c250d4e62 100755 --- a/gen/apc_parsers.py +++ b/gen/apc_parsers.py @@ -316,6 +316,7 @@ def parsers() -> None: } text = generate('parse_graphics_code', 'screen_handle_graphics_command', 'graphics_command', keymap, 'GraphicsCommand') write_header(text, 'kitty/parse-graphics-command.h') + keymap = { 'w': ('width', 'uint'), 's': ('scale', 'uint'), @@ -329,6 +330,15 @@ def parsers() -> None: payload_is_base64=False, start_parsing_at=0, field_sep=':') write_header(text, 'kitty/parse-multicell-command.h') + keymap = { + 't': ('type', flag('ae')), + 'm': ('more', 'uint'), + } + text = generate( + 'parse_dnd_code', 'screen_handle_dnd_command', 'dnd_command', keymap, 'DnDCommand', + payload_is_base64=True, start_parsing_at=0, field_sep=':') + write_header(text, 'kitty/parse-dnd-command.h') + def main(args: list[str]=sys.argv) -> None: parsers() diff --git a/kitty/control-codes.h b/kitty/control-codes.h index 7df0c6c2e..1f1130cb7 100644 --- a/kitty/control-codes.h +++ b/kitty/control-codes.h @@ -235,3 +235,5 @@ #define PENDING_MODE 2026 // Text size OSC number #define TEXT_SIZE_CODE 66 +// Drag and drop protocol +#define DND_CODE 72 diff --git a/kitty/data-types.c b/kitty/data-types.c index c7d6ec0e3..c38810eb0 100644 --- a/kitty/data-types.c +++ b/kitty/data-types.c @@ -864,6 +864,7 @@ PyInit_fast_data_types(void) { PyModule_AddIntMacro(m, ESC_DCS); PyModule_AddIntMacro(m, ESC_PM); PyModule_AddIntMacro(m, TEXT_SIZE_CODE); + PyModule_AddIntMacro(m, DND_CODE); PyModule_AddIntMacro(m, COLOR_NOT_SET); PyModule_AddIntMacro(m, COLOR_IS_SPECIAL); PyModule_AddIntMacro(m, COLOR_IS_INDEX); diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index a7908e2bc..f9d39f5b2 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -326,6 +326,7 @@ WINDOW_MAXIMIZED: int WINDOW_MINIMIZED: int WINDOW_HIDDEN: int TEXT_SIZE_CODE: int +DND_CODE: int TOP_EDGE: int BOTTOM_EDGE: int LEFT_EDGE: int diff --git a/kitty/parse-dnd-command.h b/kitty/parse-dnd-command.h new file mode 100644 index 000000000..41497be0e --- /dev/null +++ b/kitty/parse-dnd-command.h @@ -0,0 +1,190 @@ +// This file is generated by apc_parsers.py do not edit! + +#pragma once + +#include "base64.h" + +static inline void parse_dnd_code(PS *self, uint8_t *parser_buf, + const size_t parser_buf_pos) { + unsigned int pos = 0; + + enum PARSER_STATES { KEY, EQUAL, UINT, INT, FLAG, AFTER_VALUE, PAYLOAD }; + enum PARSER_STATES state = KEY, value_state = FLAG; + DnDCommand g = {0}; + unsigned int i, code; + uint64_t lcode; + int64_t accumulator; + bool is_negative; + (void)is_negative; + size_t sz; + + enum KEYS { type = 't', more = 'm' }; + + enum KEYS key = 'a'; + if (parser_buf[pos] == ';') + state = AFTER_VALUE; + + while (pos < parser_buf_pos) { + switch (state) { + case KEY: + key = parser_buf[pos++]; + state = EQUAL; + switch (key) { + case type: + value_state = FLAG; + break; + case more: + value_state = UINT; + break; + default: + REPORT_ERROR( + "Malformed DnDCommand control block, invalid key character: 0x%x", + key); + return; + } + break; + + case EQUAL: + if (parser_buf[pos++] != '=') { + REPORT_ERROR("Malformed DnDCommand control block, no = after key, " + "found: 0x%x instead", + parser_buf[pos - 1]); + return; + } + state = value_state; + break; + + case FLAG: + switch (key) { + + case type: { + g.type = parser_buf[pos++]; + if (g.type != 'a' && g.type != 'e') { + REPORT_ERROR("Malformed DnDCommand control block, unknown flag value " + "for type: 0x%x", + g.type); + return; + }; + } break; + + default: + break; + } + state = AFTER_VALUE; + break; + + case INT: +#define READ_UINT \ + for (i = pos, accumulator = 0; i < MIN(parser_buf_pos, pos + 10); i++) { \ + int64_t n = parser_buf[i] - '0'; \ + if (n < 0 || n > 9) \ + break; \ + accumulator += n * digit_multipliers[i - pos]; \ + } \ + if (i == pos) { \ + REPORT_ERROR("Malformed DnDCommand control block, expecting an integer " \ + "value for key: %c", \ + key & 0xFF); \ + return; \ + } \ + lcode = accumulator / digit_multipliers[i - pos - 1]; \ + pos = i; \ + if (lcode > UINT32_MAX) { \ + REPORT_ERROR("Malformed DnDCommand control block, number is too large"); \ + return; \ + } \ + code = lcode; + + is_negative = false; + if (parser_buf[pos] == '-') { + is_negative = true; + pos++; + } +#define I(x) \ + case x: \ + g.x = is_negative ? 0 - (int32_t)code : (int32_t)code; \ + break + READ_UINT; + switch (key) { + ; + default: + break; + } + state = AFTER_VALUE; + break; +#undef I + case UINT: + READ_UINT; +#define U(x) \ + case x: \ + g.x = code; \ + break + switch (key) { + U(more); + default: + break; + } + state = AFTER_VALUE; + break; +#undef U +#undef READ_UINT + + case AFTER_VALUE: + switch (parser_buf[pos++]) { + default: + REPORT_ERROR("Malformed DnDCommand control block, expecting a : or " + "semi-colon after a value, found: 0x%x", + parser_buf[pos - 1]); + return; + case ':': + state = KEY; + break; + case ';': + state = PAYLOAD; + break; + } + break; + + case PAYLOAD: { + sz = parser_buf_pos - pos; + g.payload_sz = MAX(BUF_EXTRA, sz); + if (!base64_decode8(parser_buf + pos, sz, parser_buf, &g.payload_sz)) { + g.payload_sz = MAX(BUF_EXTRA, sz); + REPORT_ERROR("Failed to parse DnDCommand command payload with error: " + " invalid base64 data in chunk of size: %zu with output " + "buffer size: %zu", + sz, g.payload_sz); + return; + } + pos = parser_buf_pos; + } break; + + } // end switch + } // end while + + switch (state) { + case EQUAL: + REPORT_ERROR("Malformed DnDCommand control block, no = after key"); + return; + case INT: + case UINT: + REPORT_ERROR( + "Malformed DnDCommand control block, expecting an integer value"); + return; + case FLAG: + REPORT_ERROR("Malformed DnDCommand control block, expecting a flag value"); + return; + default: + break; + } + + REPORT_VA_COMMAND("K s {sc sI ss#}", self->window_id, "dnd_command", + + "type", g.type, + + "more", (unsigned int)g.more, + + "", (char *)parser_buf, g.payload_sz); + + screen_handle_dnd_command(self->screen, &g, parser_buf); +} diff --git a/kitty/screen.c b/kitty/screen.c index 2a8c30863..066e95574 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -1507,6 +1507,18 @@ screen_dirty_line_graphics(Screen *self, const unsigned int top, const unsigned grman_remove_cell_images(main_buf ? self->main_grman : self->alt_grman, top, bottom); } +void +screen_handle_dnd_command(Screen *self, const DnDCommand *cmd, const uint8_t *payload) { + if (!self->window_id) return; + switch(cmd->type) { + case 'a': + (void)payload; + break; + case 'e': + break; + } +} + void screen_handle_graphics_command(Screen *self, const GraphicsCommand *cmd, const uint8_t *payload) { unsigned int x = self->cursor->x, y = self->cursor->y; diff --git a/kitty/screen.h b/kitty/screen.h index 092aeaaf2..337433d55 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -14,6 +14,12 @@ typedef enum ScrollTypes { SCROLL_LINE = -999999, SCROLL_PAGE, SCROLL_FULL } ScrollType; +typedef struct DnDCommand { + char type; + unsigned more; + size_t payload_sz; +} DnDCommand; + typedef struct { bool mLNM, mIRM, mDECTCEM, mDECSCNM, mDECOM, mDECAWM, mDECCOLM, mDECARM, mDECCKM, mCOLOR_PREFERENCE_NOTIFICATION, mBRACKETED_PASTE, mFOCUS_TRACKING, mDECSACE, mHANDLE_TERMIOS_SIGNALS, mINBAND_RESIZE_NOTIFICATION, @@ -290,6 +296,7 @@ void set_active_hyperlink(Screen*, char*, char*); hyperlink_id_type screen_mark_hyperlink(Screen*, index_type, index_type); void screen_handle_graphics_command(Screen *self, const GraphicsCommand *cmd, const uint8_t *payload); void screen_handle_multicell_command(Screen *self, const MultiCellCommand *cmd, const uint8_t *payload); +void screen_handle_dnd_command(Screen *self, const DnDCommand *cmd, const uint8_t *payload); bool screen_open_url(Screen*); bool screen_set_last_visited_prompt(Screen*, index_type); bool screen_select_cmd_output(Screen*, index_type); diff --git a/kitty/vt-parser.c b/kitty/vt-parser.c index 877f23e27..f6ab307dc 100644 --- a/kitty/vt-parser.c +++ b/kitty/vt-parser.c @@ -396,6 +396,7 @@ find_st_terminator(PS *self, size_t *end_pos) { // OSC {{{ #include "parse-multicell-command.h" +#include "parse-dnd-command.h" static bool is_osc_52(PS *self) { @@ -574,6 +575,8 @@ dispatch_osc(PS *self, uint8_t *buf, size_t limit, bool is_extended_osc) { case TEXT_SIZE_CODE: parse_multicell_code(self, buf + i, limit - i); break; + case DND_CODE: + parse_dnd_code(self, buf + i, limit - i); break; case 133: #ifdef DUMP_COMMANDS START_DISPATCH