From 1a21a5666a0e463ef591fb36fd2b7a3f85188908 Mon Sep 17 00:00:00 2001 From: Scott Percival Date: Fri, 26 Jul 2024 13:45:26 +0800 Subject: [PATCH] GUACAMOLE-1973: Add support for XTerm bracketed-paste mode --- src/terminal/terminal-handlers.c | 1 + src/terminal/terminal.c | 86 ++++++++++++++++++++++++++- src/terminal/terminal/terminal-priv.h | 5 ++ src/terminal/terminal/terminal.h | 16 +++++ 4 files changed, 106 insertions(+), 2 deletions(-) diff --git a/src/terminal/terminal-handlers.c b/src/terminal/terminal-handlers.c index 7bec0f2765..a73da1818e 100644 --- a/src/terminal/terminal-handlers.c +++ b/src/terminal/terminal-handlers.c @@ -434,6 +434,7 @@ static bool* __guac_terminal_get_flag(guac_terminal* term, int num, char private switch (num) { case 1: return &(term->application_cursor_keys); /* DECCKM */ case 25: return &(term->cursor_visible); /* DECTECM */ + case 2004: return &(term->bracketed_paste_mode); /* XTerm bracketed-paste */ } } diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c index 192c85a193..b471886eb2 100644 --- a/src/terminal/terminal.c +++ b/src/terminal/terminal.c @@ -230,6 +230,7 @@ void guac_terminal_reset(guac_terminal* term) { term->application_cursor_keys = false; term->automatic_carriage_return = false; term->insert_mode = false; + term->bracketed_paste_mode = false; /* Reset tabs */ term->tab_interval = 8; @@ -1500,6 +1501,87 @@ int guac_terminal_send_string(guac_terminal* term, const char* data) { } +int guac_terminal_send_clipboard(guac_terminal *term) { + char *filtered = guac_mem_alloc(term->clipboard->length + 12); + uint8_t *src_ptr = (uint8_t *)term->clipboard->buffer; + uint8_t *src_end = (uint8_t *)(term->clipboard->buffer + term->clipboard->length); + uint8_t *dst_ptr = (uint8_t *)filtered; + int filtered_len = 0; + + /* Send the paste start sequence */ + if (term->bracketed_paste_mode) { + memcpy(dst_ptr, "\x1B[200~", 6); + dst_ptr += 6; + filtered_len += 6; + } + + while (src_ptr < src_end) { + + /* Exclude Unicode CO and C1 control characters except tab, line feed + * and carriage return. */ + bool is_control = (((*src_ptr >= 0x00) && (*src_ptr < 0x20)) || + ((*src_ptr >= 0x80) && (*src_ptr < 0xa0))) && + (*src_ptr != 0x09) && (*src_ptr != 0x0a) && (*src_ptr != 0x0d); + + /* Allow UTF-8 codepoints */ + if ((*src_ptr & 0xe0) == 0xc0) { + + /* UTF-8 2-byte codepoint */ + if ((src_ptr + 1 < src_end) && ((src_ptr[1] & 0xc0) == 0x80)) { + dst_ptr[0] = src_ptr[0]; + dst_ptr[1] = src_ptr[1]; + dst_ptr += 2; + filtered_len += 2; + src_ptr++; + } + } + else if ((*src_ptr & 0xf0) == 0xe0) { + + /* UTF-8 3-byte codepoint */ + if ((src_ptr + 2 < src_end) && ((src_ptr[1] & 0xc0) == 0x80) && + ((src_ptr[2] & 0xc0) == 0x80)) { + dst_ptr[0] = src_ptr[0]; + dst_ptr[1] = src_ptr[1]; + dst_ptr[2] = src_ptr[2]; + dst_ptr += 3; + filtered_len += 3; + src_ptr += 2; + } + } + else if ((*src_ptr & 0xf8) == 0xf0) { + + /* UTF-8 4-byte codepoint */ + if ((src_ptr + 3 < src_end) && ((src_ptr[1] & 0xc0) == 0x80) && + ((src_ptr[2] & 0xc0) == 0x80) && ((src_ptr[3] & 0xc0) == 0x80)) { + dst_ptr[0] = src_ptr[0]; + dst_ptr[1] = src_ptr[1]; + dst_ptr[2] = src_ptr[2]; + dst_ptr[3] = src_ptr[3]; + dst_ptr += 4; + filtered_len += 4; + src_ptr += 3; + } + } + else if (!is_control) { + *dst_ptr = *src_ptr; + dst_ptr++; + filtered_len++; + } + src_ptr++; + } + + /* Send the paste stop sequence */ + if (term->bracketed_paste_mode) { + memcpy(dst_ptr, "\x1B[201~", 6); + dst_ptr += 6; + filtered_len += 6; + } + + int result = guac_terminal_send_data(term, filtered, filtered_len); + guac_mem_free(filtered); + return result; +} + static int __guac_terminal_send_key(guac_terminal* term, int keysym, int pressed) { /* Ignore user input if terminal is not started */ @@ -1529,7 +1611,7 @@ static int __guac_terminal_send_key(guac_terminal* term, int keysym, int pressed /* Ctrl+Shift+V shortcut for paste */ if (keysym == 'V' && term->mod_ctrl) - return guac_terminal_send_data(term, term->clipboard->buffer, term->clipboard->length); + return guac_terminal_send_clipboard(term); /* Shift+PgUp / Shift+PgDown shortcuts for scrolling */ if (term->mod_shift) { @@ -1722,7 +1804,7 @@ static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user, /* Paste contents of clipboard on right or middle mouse button up */ if ((released_mask & GUAC_CLIENT_MOUSE_RIGHT) || (released_mask & GUAC_CLIENT_MOUSE_MIDDLE)) - return guac_terminal_send_data(term, term->clipboard->buffer, term->clipboard->length); + return guac_terminal_send_clipboard(term); /* If left mouse button was just released, stop selection */ if (released_mask & GUAC_CLIENT_MOUSE_LEFT) diff --git a/src/terminal/terminal/terminal-priv.h b/src/terminal/terminal/terminal-priv.h index 08e2baea1f..e5736403e0 100644 --- a/src/terminal/terminal/terminal-priv.h +++ b/src/terminal/terminal/terminal-priv.h @@ -390,6 +390,11 @@ struct guac_terminal { */ bool automatic_carriage_return; + /** + * Whether the current application supports bracketed paste mode. + */ + bool bracketed_paste_mode; + /** * Whether insert mode is enabled (DECIM). */ diff --git a/src/terminal/terminal/terminal.h b/src/terminal/terminal/terminal.h index e71b8c56ac..f9905fbfbd 100644 --- a/src/terminal/terminal/terminal.h +++ b/src/terminal/terminal/terminal.h @@ -542,6 +542,22 @@ int guac_terminal_send_data(guac_terminal* term, const char* data, int length); */ int guac_terminal_send_string(guac_terminal* term, const char* data); +/** + * Sends the terminal clipboard contents after sanitisation. If terminal input + * is currently coming from a stream due to a prior call to + * guac_terminal_send_stream(), any input which would normally result from + * invoking this function is dropped. + * + * @param term + * The terminal which should receive the given data on STDIN. + * + * @return + * The number of bytes written to STDIN, or a negative value if an error + * occurs preventing the data from being written. This should always be + * the size of the data given unless data is intentionally dropped. + */ +int guac_terminal_send_clipboard(guac_terminal* term); + /** * Writes the given buffer to the given terminal's STDOUT. All requested bytes * will be written unless an error occurs. This function may block until space