From 657ad14d67ff37dd4695354366a773a25c130b61 Mon Sep 17 00:00:00 2001 From: corentin-soriano Date: Tue, 18 Jun 2024 14:27:05 +0200 Subject: [PATCH 1/7] GUACAMOLE-1961: Allow selection on multiple lines when doubleclicking wrapped word. --- src/terminal/terminal.c | 96 +++++++++++++++++++++++++++++++++++------ 1 file changed, 82 insertions(+), 14 deletions(-) diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c index 7e52a4b91..8c9ae36e8 100644 --- a/src/terminal/terminal.c +++ b/src/terminal/terminal.c @@ -1680,11 +1680,17 @@ static void guac_terminal_double_click(guac_terminal* terminal, int row, int col if (col >= length) return; - /* (char)10 behind cursor */ - int current_char = characters[col].value; + /* Get buffer row and char behind cursor */ + guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(terminal->buffer, row, 0); + int cursor_char = buffer_row->characters[col].value; /* Position of the word behind cursor. - * Default = col required to select a char if not a word and not blank. */ + * Default = col/row required to select a char if not a word and not blank. */ + int word_col_head = col; + int word_col_tail = col; + int word_row_head = row; + int word_row_tail = row; + int flag; /* The function used to calculate the word borders */ bool (*is_part_of_word)(int) = NULL; @@ -1697,28 +1703,90 @@ static void guac_terminal_double_click(guac_terminal* terminal, int row, int col else if (guac_terminal_is_blank(current_char)) is_part_of_word = guac_terminal_is_blank; - int word_head = col; - int word_tail = col; - if (is_part_of_word != NULL) { /* Get word head*/ - for (; word_head - 1 >= 0; word_head--) { - if (!is_part_of_word(characters[word_head - 1].value)) + do { + + /* Buffer row to get */ + int current_row = word_row_head; + int current_col = word_col_head; + + /* Bound of screen reached: get previous row */ + if (word_col_head == 0) + current_row--; + + /* Get current buffer row */ + buffer_row = guac_terminal_buffer_get_row(terminal->buffer, current_row, 0); + + /* If we are on the previous row */ + if (current_row < word_row_head) { + + /* Line not wrapped: stop, it's the word boundary */ + if (!buffer_row->wrapped_row) + break; + + /* Go to last column of this row */ + current_col = buffer_row->length; + } + + /* Get char of the current row/column */ + flag = buffer_row->characters[current_col-1].value; + + /* Word boundary reached, stop */ + if (!is_part_of_word(flag)) break; - } + + /* Store new position on previous row */ + if (current_row < word_row_head) { + word_row_head = current_row; + word_col_head = current_col; + } + + } while (word_col_head >= 0 && word_col_head--); /* Get word tail */ - for (; word_tail + 1 < terminal->display->width && word_tail + 1 < length; word_tail++) { - if (!is_part_of_word(characters[word_tail + 1].value)) + do { + + /* Get current buffer row */ + buffer_row = guac_terminal_buffer_get_row(terminal->buffer, word_row_tail, 0); + + /* Bound of screen reached and row is wrapped: get next row */ + if (word_col_tail == buffer_row->length - 1 && buffer_row->wrapped_row) { + + /* Get next buffer row */ + guac_terminal_buffer_row* next_buffer_row = + guac_terminal_buffer_get_row(terminal->buffer, word_row_tail + 1, 0); + + /* Get first char of the next row */ + flag = next_buffer_row->characters[0].value; + + } + + /* Otherwise, get char of next column on current row */ + else + flag = buffer_row->characters[word_col_tail+1].value; + + /* Word boundary reached, stop */ + if (!is_part_of_word(flag)) break; - } + /* Store new position on next row */ + if (word_col_tail == buffer_row->length - 1 && buffer_row->wrapped_row) { + word_row_tail++; + word_col_tail = 0; + } + + /* Or go to next column of current row */ + else + word_col_tail++; + + } while (word_col_tail <= buffer_row->length); } /* Select and add to clipboard the "word" */ - guac_terminal_select_start(terminal, row, word_head); - guac_terminal_select_update(terminal, row, word_tail); + guac_terminal_select_start(terminal, word_row_head, word_col_head); + guac_terminal_select_update(terminal, word_row_tail, word_col_tail); } From ce5d1f0ae1582e6f5cb867f9a9abfd7234bd9573 Mon Sep 17 00:00:00 2001 From: corentin-soriano Date: Tue, 18 Jun 2024 14:29:59 +0200 Subject: [PATCH 2/7] GUACAMOLE-1961: Allow rectangular selection when ALT key is pressed. --- src/terminal/display.c | 67 +++++++++++++++++++-------- src/terminal/select.c | 42 ++++++++++++++--- src/terminal/terminal.c | 5 ++ src/terminal/terminal/display.h | 22 ++++++++- src/terminal/terminal/terminal-priv.h | 5 ++ 5 files changed, 114 insertions(+), 27 deletions(-) diff --git a/src/terminal/display.c b/src/terminal/display.c index 402b849dc..dae9e98a6 100644 --- a/src/terminal/display.c +++ b/src/terminal/display.c @@ -889,7 +889,7 @@ void guac_terminal_display_dup( } void guac_terminal_display_select(guac_terminal_display* display, - int start_row, int start_col, int end_row, int end_col) { + int start_row, int start_col, int end_row, int end_col, bool rectangle) { guac_socket* socket = display->client->socket; guac_layer* select_layer = display->select_layer; @@ -943,38 +943,67 @@ void guac_terminal_display_select(guac_terminal_display* display, start_row = end_row; end_row = temp; + /* Don't swap columns if it's a rectangular selection and start_col + * is less than end_col */ + if (!rectangle || start_col > end_col) { + temp = start_col; + start_col = end_col; + end_col = temp; + } + + } + + /* Swap columns on rectangular selection to bottom-left direction */ + else if (rectangle && start_col > end_col) { + + int temp; + temp = start_col; start_col = end_col; end_col = temp; } - /* First row */ - guac_protocol_send_rect(socket, select_layer, + /* Multilines rectangular selection */ + if (rectangle) { + guac_protocol_send_rect(socket, select_layer, - start_col * display->char_width, - start_row * display->char_height, + start_col * display->char_width, + start_row * display->char_height, - display->width * display->char_width, - display->char_height); + (end_col - start_col + 1) * display->char_width, + (end_row - start_row + 1) * display->char_height); + } - /* Middle */ - guac_protocol_send_rect(socket, select_layer, + /* Multilines standard selection */ + else { + /* First row */ + guac_protocol_send_rect(socket, select_layer, - 0, - (start_row + 1) * display->char_height, + start_col * display->char_width, + start_row * display->char_height, - display->width * display->char_width, - (end_row - start_row - 1) * display->char_height); + display->width * display->char_width, + display->char_height); - /* Last row */ - guac_protocol_send_rect(socket, select_layer, + /* Middle */ + guac_protocol_send_rect(socket, select_layer, - 0, - end_row * display->char_height, + 0, + (start_row + 1) * display->char_height, - (end_col + 1) * display->char_width, - display->char_height); + display->width * display->char_width, + (end_row - start_row - 1) * display->char_height); + + /* Last row */ + guac_protocol_send_rect(socket, select_layer, + + 0, + end_row * display->char_height, + + (end_col + 1) * display->char_width, + display->char_height); + } } diff --git a/src/terminal/select.c b/src/terminal/select.c index 406a0c43f..a1b557d1b 100644 --- a/src/terminal/select.c +++ b/src/terminal/select.c @@ -67,11 +67,38 @@ static void guac_terminal_select_normalized_range(guac_terminal* terminal, int* start_row, int* start_col, int* end_row, int* end_col) { + + /* Columns coordinates must be swapped when selecting to bottom-left direction + * on rectangular selection mode. */ + if (terminal->rectangle_selection + && terminal->selection_start_row < terminal->selection_end_row + && terminal->selection_start_column > terminal->selection_end_column) { + + *start_row = terminal->selection_start_row; + *end_col = terminal->selection_start_column + terminal->selection_start_width - 1; + *end_row = terminal->selection_end_row; + *start_col = terminal->selection_end_column; + + } + + /* Rows coordinates must be swapped when selecting to top-right direction + * on rectangular selection mode. */ + else if (terminal->rectangle_selection + && terminal->selection_start_row > terminal->selection_end_row + && terminal->selection_start_column < terminal->selection_end_column) { + + *end_row = terminal->selection_start_row; + *start_col = terminal->selection_start_column; + *start_row = terminal->selection_end_row; + *end_col = terminal->selection_end_column + terminal->selection_end_width - 1; + + } + /* Pass through start/end coordinates if they are already in the expected * order, adjusting only for final character width */ - if (terminal->selection_start_row < terminal->selection_end_row - || (terminal->selection_start_row == terminal->selection_end_row - && terminal->selection_start_column < terminal->selection_end_column)) { + else if (terminal->selection_start_row < terminal->selection_end_row + || (terminal->selection_start_row == terminal->selection_end_row + && terminal->selection_start_column < terminal->selection_end_column)) { *start_row = terminal->selection_start_row; *start_col = terminal->selection_start_column; @@ -109,7 +136,8 @@ void guac_terminal_select_redraw(guac_terminal* terminal) { else end_column += terminal->selection_end_width - 1; - guac_terminal_display_select(terminal->display, start_row, start_column, end_row, end_column); + guac_terminal_display_select(terminal->display, start_row, + start_column, end_row, end_column, terminal->rectangle_selection); } @@ -364,7 +392,7 @@ void guac_terminal_select_end(guac_terminal* terminal) { for (int row = start_row; row <= end_row; row++) { /* Add a newline only if the previous line was not wrapped */ - if (!last_row_was_wrapped) + if (!last_row_was_wrapped || (terminal->rectangle_selection && row != start_row)) guac_common_clipboard_append(terminal->clipboard, "\n", 1); /* Append next row from desired region, adjusting the start/end column @@ -373,8 +401,8 @@ void guac_terminal_select_end(guac_terminal* terminal) { * copied in their entirety. */ int length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, &last_row_was_wrapped, row); guac_terminal_clipboard_append_characters(terminal, characters, length, - (row == start_row) ? start_col : 0, - (row == end_row) ? end_col : length - 1); + (row == start_row || terminal->rectangle_selection) ? start_col : 0, + (row == end_row || terminal->rectangle_selection) ? end_col : length - 1); } diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c index 8c9ae36e8..cb1fbfbf6 100644 --- a/src/terminal/terminal.c +++ b/src/terminal/terminal.c @@ -1868,6 +1868,11 @@ static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user, /* First click = start selection */ case 0: + /* The rectangular selection is requested by pressing + * the ALT key at the start of the selection */ + term->rectangle_selection = term->mod_alt; + + /* Start selection */ guac_terminal_select_start(term, row, col); break; diff --git a/src/terminal/terminal/display.h b/src/terminal/terminal/display.h index 635536088..13b17370f 100644 --- a/src/terminal/terminal/display.h +++ b/src/terminal/terminal/display.h @@ -342,9 +342,29 @@ void guac_terminal_display_dup( /** * Draws the text selection rectangle from the given coordinates to the given end coordinates. + * + * @param display + * The terminal display to draw selection. + * + * @param start_row + * The row to start draw selection. + * + * @param start_col + * The column to start draw selection. + * + * @param end_row + * The row to end draw selection. + * + * @param end_col + * The column to end draw selection. + * + * @param rectangle + * True if rectangular selection (selection is always start to end col), + * False if normal selection (All columns for middle-rows). + * */ void guac_terminal_display_select(guac_terminal_display* display, - int start_row, int start_col, int end_row, int end_col); + int start_row, int start_col, int end_row, int end_col, bool rectangle); /** * Clears the currently-selected region, removing the highlight. diff --git a/src/terminal/terminal/terminal-priv.h b/src/terminal/terminal/terminal-priv.h index 52462ce54..55afa2ea1 100644 --- a/src/terminal/terminal/terminal-priv.h +++ b/src/terminal/terminal/terminal-priv.h @@ -491,6 +491,11 @@ struct guac_terminal { */ int click_counter; + /** + * Rectangular selection when ALT key is pressed when starting selection. + */ + bool rectangle_selection; + }; /** From 6772809f1a37101e362cfd43dbf25b45186bb822 Mon Sep 17 00:00:00 2001 From: corentin-soriano Date: Tue, 30 Jul 2024 08:22:49 +0200 Subject: [PATCH 3/7] GUACAMOLE-1961: Allow selection on multiple lines when tripleclicking on wrapped row. squash triple click buffer_row --- src/terminal/terminal.c | 66 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c index cb1fbfbf6..0497546c0 100644 --- a/src/terminal/terminal.c +++ b/src/terminal/terminal.c @@ -1675,7 +1675,8 @@ static bool guac_terminal_is_blank(int ascii_char) { static void guac_terminal_double_click(guac_terminal* terminal, int row, int col) { guac_terminal_char* characters; - int length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, NULL, row); + bool is_wrapped; + int length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, &is_wrapped, row); if (col >= length) return; @@ -1716,8 +1717,8 @@ static void guac_terminal_double_click(guac_terminal* terminal, int row, int col if (word_col_head == 0) current_row--; - /* Get current buffer row */ - buffer_row = guac_terminal_buffer_get_row(terminal->buffer, current_row, 0); + /* Get current buffer row */ + length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, &is_wrapped, current_row); /* If we are on the previous row */ if (current_row < word_row_head) { @@ -1748,8 +1749,8 @@ static void guac_terminal_double_click(guac_terminal* terminal, int row, int col /* Get word tail */ do { - /* Get current buffer row */ - buffer_row = guac_terminal_buffer_get_row(terminal->buffer, word_row_tail, 0); + /* Get current buffer row */ + length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, &is_wrapped, word_row_tail); /* Bound of screen reached and row is wrapped: get next row */ if (word_col_tail == buffer_row->length - 1 && buffer_row->wrapped_row) { @@ -1790,6 +1791,58 @@ static void guac_terminal_double_click(guac_terminal* terminal, int row, int col } +/** + * Selection of a line during a triple click event. + * - Get buffer row boundaries if it has been wrapped. + * - Visual selection of the line. + * - Adding it to clipboard. + * + * @param terminal + * The terminal that received a triple click event. + * + * @param row + * The row where is the mouse at the triple click event. + * + * @param col + * The column where is the mouse at the triple click event. + */ +static void guac_terminal_triple_click(guac_terminal* terminal, int row, int col) { + + /* Temporarily reading previous and next lines */ + guac_terminal_char* characters; + bool is_wrapped; + int length; + + + /* Final boundary rows */ + int top_row = row; + int bottom_row = row; + + /* Get top boundary */ + do { + + /* Read previous buffer row */ + length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, &is_wrapped, top_row - 1); + + /* Go to the previous row if it is wrapped */ + } while (is_wrapped && top_row--); + + /* Get bottom boundary */ + do { + + /* Read current buffer row */ + length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, &is_wrapped, bottom_row); + + /* Go to the next row if current row is wrapped */ + } while (is_wrapped && bottom_row++); + + /* Start selection on first col of top_row */ + guac_terminal_select_start(terminal, top_row, 0); + + /* End selection on last col of bottom_row */ + guac_terminal_select_update(terminal, bottom_row, length - 1); +} + static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user, int x, int y, int mask) { @@ -1883,8 +1936,7 @@ static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user, /* third click or more = line selection */ default: - guac_terminal_select_start(term, row, 0); - guac_terminal_select_update(term, row, term->display->width); + guac_terminal_triple_click(term, row, col); break; } } From 03d0d3ff7137da537190c252e7f3c872f3bfdecb Mon Sep 17 00:00:00 2001 From: corentin-soriano Date: Tue, 30 Jul 2024 11:03:37 +0200 Subject: [PATCH 4/7] GUACAMOLE-1961: Improve selection by differentiating URIs from words. --- src/terminal/terminal.c | 230 +++++++++++++++++++++++++++++----------- 1 file changed, 170 insertions(+), 60 deletions(-) diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c index 0497546c0..5a48f08b2 100644 --- a/src/terminal/terminal.c +++ b/src/terminal/terminal.c @@ -1610,8 +1610,8 @@ int guac_terminal_send_key(guac_terminal* term, int keysym, int pressed) { /** * Determines if the given character is part of a word. - * Match these chars :[0-9A-Za-z\$\%\&\-\.\/\:\=\?\\_~] - * This allows a path, URL, variable name or IP address to be treated as a word. + * Match these chars :[0-9A-Za-z\$\-\.\/_~] + * This allows a path, variable name or IP address to be treated as a word. * * @param ascii_char * The character to check. @@ -1625,17 +1625,42 @@ static bool guac_terminal_is_part_of_word(int ascii_char) { (ascii_char >= 'A' && ascii_char <= 'Z') || (ascii_char >= 'a' && ascii_char <= 'z') || (ascii_char == '$') || - (ascii_char == '%') || - (ascii_char == '&') || (ascii_char == '-') || (ascii_char == '.') || (ascii_char == '/') || + (ascii_char == '_') || + (ascii_char == '~')); +} + +/** + * Determines if the given character is part of URI. + * Match these chars :[%\&\+:;,=\?\!\*\\\(\)\[\]#] and word chars. + * + * @param ascii_char + * The character to check. + * + * @return + * true if match a "word" or "uri" char, + * false otherwise. + */ +static bool guac_terminal_is_part_of_word_or_uri(int ascii_char) { + return (guac_terminal_is_part_of_word(ascii_char) || + (ascii_char == '%') || + (ascii_char == '&') || + (ascii_char == '+') || (ascii_char == ':') || + (ascii_char == ';') || + (ascii_char == ',') || (ascii_char == '=') || (ascii_char == '?') || + (ascii_char == '!') || + (ascii_char == '*') || (ascii_char == '\\') || - (ascii_char == '_') || - (ascii_char == '~')); + (ascii_char == '(') || + (ascii_char == ')') || + (ascii_char == '[') || + (ascii_char == ']') || + (ascii_char == '#')); } /** @@ -1682,8 +1707,7 @@ static void guac_terminal_double_click(guac_terminal* terminal, int row, int col return; /* Get buffer row and char behind cursor */ - guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(terminal->buffer, row, 0); - int cursor_char = buffer_row->characters[col].value; + int current_char = characters[col].value; /* Position of the word behind cursor. * Default = col/row required to select a char if not a word and not blank. */ @@ -1697,8 +1721,8 @@ static void guac_terminal_double_click(guac_terminal* terminal, int row, int col bool (*is_part_of_word)(int) = NULL; /* If selection is on a word, get its borders */ - if (guac_terminal_is_part_of_word(current_char)) - is_part_of_word = guac_terminal_is_part_of_word; + if (guac_terminal_is_part_of_word_or_uri(current_char)) + is_part_of_word = guac_terminal_is_part_of_word_or_uri; /* If selection is on a blank, get its borders */ else if (guac_terminal_is_blank(current_char)) @@ -1706,83 +1730,169 @@ static void guac_terminal_double_click(guac_terminal* terminal, int row, int col if (is_part_of_word != NULL) { - /* Get word head*/ + /* Event to exit loop */ + bool exit_loop = false; + do { - /* Buffer row to get */ - int current_row = word_row_head; - int current_col = word_col_head; + /* Position of the word behind cursor. + * Default = col/row required to select a char if not a word and not blank. */ + word_col_head = col; + word_col_tail = col; + word_row_head = row; + word_row_tail = row; + + /* Get word head*/ + do { - /* Bound of screen reached: get previous row */ - if (word_col_head == 0) - current_row--; + /* Buffer row to get */ + int current_row = word_row_head; + int current_col = word_col_head; + + /* Bound of screen reached: get previous row */ + if (word_col_head == 0) + current_row--; /* Get current buffer row */ length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, &is_wrapped, current_row); - /* If we are on the previous row */ - if (current_row < word_row_head) { + /* If we are on the previous row */ + if (current_row < word_row_head) { - /* Line not wrapped: stop, it's the word boundary */ - if (!buffer_row->wrapped_row) - break; + /* Line not wrapped: stop, it's the word boundary */ + if (!is_wrapped) + break; - /* Go to last column of this row */ - current_col = buffer_row->length; - } + /* Go to last column of this row */ + current_col = length; + } - /* Get char of the current row/column */ - flag = buffer_row->characters[current_col-1].value; + /* Get char of the current row/column */ + flag = characters[current_col-1].value; - /* Word boundary reached, stop */ - if (!is_part_of_word(flag)) - break; + /* Word boundary reached, stop */ + if (!is_part_of_word(flag)) + break; - /* Store new position on previous row */ - if (current_row < word_row_head) { - word_row_head = current_row; - word_col_head = current_col; - } + /* Store new position on previous row */ + if (current_row < word_row_head) { + word_row_head = current_row; + word_col_head = current_col; + } - } while (word_col_head >= 0 && word_col_head--); + } while (word_col_head >= 0 && word_col_head--); - /* Get word tail */ - do { + /* Get word tail */ + do { /* Get current buffer row */ length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, &is_wrapped, word_row_tail); - /* Bound of screen reached and row is wrapped: get next row */ - if (word_col_tail == buffer_row->length - 1 && buffer_row->wrapped_row) { + /* Bound of screen reached and row is wrapped: get next row */ + if (word_col_tail == length - 1 && is_wrapped) { - /* Get next buffer row */ - guac_terminal_buffer_row* next_buffer_row = - guac_terminal_buffer_get_row(terminal->buffer, word_row_tail + 1, 0); + /* Get next buffer row */ + bool next_is_wrapped; + guac_terminal_char* next_characters; + guac_terminal_buffer_get_columns(terminal->current_buffer, &next_characters, &next_is_wrapped, word_row_tail + 1); - /* Get first char of the next row */ - flag = next_buffer_row->characters[0].value; + /* Get first char of the next row */ + flag = next_characters[0].value; - } + } - /* Otherwise, get char of next column on current row */ - else - flag = buffer_row->characters[word_col_tail+1].value; + /* Otherwise, get char of next column on current row */ + else + flag = characters[word_col_tail+1].value; + + /* Word boundary reached, stop */ + if (!is_part_of_word(flag)) + break; + + /* Store new position on next row */ + if (word_col_tail == length - 1 && is_wrapped) { + word_row_tail++; + word_col_tail = 0; + } + + /* Or go to next column of current row */ + else + word_col_tail++; - /* Word boundary reached, stop */ - if (!is_part_of_word(flag)) + } while (word_col_tail <= length); + + /* The following is only for URL scheme validation */ + if (is_part_of_word != guac_terminal_is_part_of_word_or_uri) break; - /* Store new position on next row */ - if (word_col_tail == buffer_row->length - 1 && buffer_row->wrapped_row) { - word_row_tail++; - word_col_tail = 0; - } + /* Temp vars to avoid overwrite word_row_head and word_col_head */ + int tmp_row = word_row_head; + int tmp_col = word_col_head; - /* Or go to next column of current row */ - else - word_col_tail++; + /* Check for the presence of a uri scheme like /^[a-z]+\:\/{2}/ */ + do { + + /* Get first char of first row */ + length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, &is_wrapped, tmp_row); + current_char = characters[tmp_col].value; + + /* End of [a-z]+ part */ + if (current_char < 'a' || current_char > 'z') { + + /* URI scheme delimiter :// foud */ + if (current_char == ':' && + characters[tmp_col+1].value == '/' && + characters[tmp_col+2].value == '/') { + + /* Set exit event */ + exit_loop = true; + break; + } + + /* Not URI scheme */ + else + break; + } + + /* End of buffer row */ + else if (tmp_col == length-1) { + + /* Confinue only if current buffer row is wrapped */ + if (is_wrapped) { + + /* Stop if latest row */ + if (tmp_row == word_row_tail) + break; + + /* Go to next row */ + tmp_row++; + + /* Go to first row (-1 for auto increment on next iteration) */ + tmp_col = 0; + + /* Don't do further tests for this iteration */ + continue; + + } + + /* End of selection without matching uri scheme */ + else + break; + + } + + /* End of selection without matching uri scheme */ + else if (tmp_row == word_row_tail && tmp_col == word_col_tail) + break; + + tmp_col++; + //printf("tmp_row = %d | word_row_tail = %d | tmp_col = %d | word_col_tail = %d\n", tmp_row, word_row_tail, tmp_col, word_col_tail); + } while (true); + + /* Get word boundaries instead of URI */ + is_part_of_word = guac_terminal_is_part_of_word; - } while (word_col_tail <= buffer_row->length); + } while (!exit_loop); } /* Select and add to clipboard the "word" */ From 8b7ab07e48c71bc3ec8a97e546858437fdf71f6f Mon Sep 17 00:00:00 2001 From: corentin-soriano Date: Tue, 8 Oct 2024 13:58:25 +0200 Subject: [PATCH 5/7] GUACAMOLE-1961: Detect accented letters as a part of word. --- src/terminal/terminal.c | 4 +++- src/terminal/terminal/terminal.h | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c index 5a48f08b2..648d6255d 100644 --- a/src/terminal/terminal.c +++ b/src/terminal/terminal.c @@ -1610,7 +1610,7 @@ int guac_terminal_send_key(guac_terminal* term, int keysym, int pressed) { /** * Determines if the given character is part of a word. - * Match these chars :[0-9A-Za-z\$\-\.\/_~] + * Match these chars :[0-9A-Za-z\$\-\.\/_~] and accented letters. * This allows a path, variable name or IP address to be treated as a word. * * @param ascii_char @@ -1624,6 +1624,8 @@ static bool guac_terminal_is_part_of_word(int ascii_char) { return ((ascii_char >= '0' && ascii_char <= '9') || (ascii_char >= 'A' && ascii_char <= 'Z') || (ascii_char >= 'a' && ascii_char <= 'z') || + (ascii_char >= GUAC_TERMINAL_LATIN1_CAPITAL_AGRAVE && + ascii_char <= GUAC_TERMINAL_LATIN1_Y_UMLAUT) || (ascii_char == '$') || (ascii_char == '-') || (ascii_char == '.') || diff --git a/src/terminal/terminal/terminal.h b/src/terminal/terminal/terminal.h index 11c8fb5a3..981dea0de 100644 --- a/src/terminal/terminal/terminal.h +++ b/src/terminal/terminal/terminal.h @@ -111,6 +111,16 @@ */ #define GUAC_TERMINAL_PIPE_AUTOFLUSH 2 +/** + * Latin 1 capital A grave, it's the first character of the accented range. + */ +#define GUAC_TERMINAL_LATIN1_CAPITAL_AGRAVE 192 + +/** + * Latin 1 small y umlaut, it's the latest character of the accented range. + */ +#define GUAC_TERMINAL_LATIN1_Y_UMLAUT 255 + /** * Represents a terminal emulator which uses a given Guacamole client to * render itself. From a6a04a51a9dbba84e93f8ae4955c9184518d3fe1 Mon Sep 17 00:00:00 2001 From: corentin-soriano Date: Tue, 1 Oct 2024 16:59:48 +0200 Subject: [PATCH 6/7] GUACAMOLE-1961: Update the selection when dragging after multiple click with words or lines. --- src/terminal/terminal.c | 337 ++++++++++++++++++-------- src/terminal/terminal/terminal-priv.h | 10 + 2 files changed, 247 insertions(+), 100 deletions(-) diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c index 648d6255d..3dc3c9c2a 100644 --- a/src/terminal/terminal.c +++ b/src/terminal/terminal.c @@ -1679,6 +1679,60 @@ static bool guac_terminal_is_blank(int ascii_char) { return (ascii_char == '\0' || ascii_char == ' '); } +/** + * Set the row tail/head and col tail/head according to the value of current + * row/col and selection_initial_row/selection_initial_column. This aims to + * keep the initially selected word when dragging mouse after a double click. + * + * @param terminal + * The terminal that received a double click event. + * + * @param col + * The column where is currently the mouse. + * + * @param row + * The row where is currently the mouse. + * + * @param col_head + * Pointer where to write the calculated head column. + * + * @param col_tail + * Pointer where to write the calculated tail column. + * + * @param row_head + * Pointer where to write the calculated head row. + * + * @param row_tail + * Pointer where to write the calculated tail row. + */ +static void guac_terminal_word_initial_position(guac_terminal* terminal, + int col, int row, int* col_head, int* col_tail, int* row_head, int* row_tail) { + + /* The mouse is still in the intial row */ + if (row == terminal->selection_initial_row) { + + /* Mouse on the right of initial column */ + if (col > terminal->selection_initial_column) + *col_head = terminal->selection_initial_column; + + /* Mouse on the left of initial column */ + else + *col_tail = terminal->selection_initial_column; + } + + /* Use initial row as bottom-right of the selection and go up/left */ + if (row < terminal->selection_initial_row) { + *row_tail = terminal->selection_initial_row; + *col_tail = terminal->selection_initial_column; + } + + /* Use initial row/col as top-left of the selection and go down/right */ + if (row > terminal->selection_initial_row) { + *row_head = terminal->selection_initial_row; + *col_head = terminal->selection_initial_column; + } +} + /** * Selection of a word during a double click event. * - Fetching the character under the mouse cursor. @@ -1698,53 +1752,96 @@ static bool guac_terminal_is_blank(int ascii_char) { * * @param col * The column where is the mouse at the double click event. + * + * @param hold + * True when user hold left click, false otherwise. */ -static void guac_terminal_double_click(guac_terminal* terminal, int row, int col) { +static void guac_terminal_double_click(guac_terminal* terminal, int row, int col, bool hold) { + /* To store buffer row characters */ guac_terminal_char* characters; + /* To get wrapped buffer row status */ bool is_wrapped; - int length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, &is_wrapped, row); + /* Length of the buffer row */ + int length; + /* Character read at a position */ + int current_char; - if (col >= length) - return; + /* Position of the word behind cursor */ + int word_col_head; + int word_col_tail; + int word_row_head; + int word_row_tail; - /* Get buffer row and char behind cursor */ - int current_char = characters[col].value; + /* The function used to calculate the word borders */ + bool (*is_part_of_word_top_left)(int) = NULL; + bool (*is_part_of_word_bottom_right)(int) = NULL; - /* Position of the word behind cursor. - * Default = col/row required to select a char if not a word and not blank. */ - int word_col_head = col; - int word_col_tail = col; - int word_row_head = row; - int word_row_tail = row; - int flag; + /* Event to exit main loop */ + bool exit_loop = false; - /* The function used to calculate the word borders */ - bool (*is_part_of_word)(int) = NULL; + do { + /* Default = col/row required to select a char if not a word and not blank */ + word_col_head = col; + word_col_tail = col; + word_row_head = row; + word_row_tail = row; + + /* User holds left click: update default selection boundaries */ + if (hold) + guac_terminal_word_initial_position(terminal, col, row, &word_col_head, + &word_col_tail, &word_row_head, &word_row_tail); + + /* Get the right function when is_part_of_word_top_left is NULL */ + if (is_part_of_word_top_left == NULL) { + + /* Get top row */ + length = guac_terminal_buffer_get_columns(terminal->current_buffer, + &characters, &is_wrapped, word_row_head); + + /* Out of bounds */ + if (col >= length) + return; + + /* Get char on top-left of default selection */ + current_char = characters[word_col_head].value; + + /* If selection is on a word, get its borders */ + if (guac_terminal_is_part_of_word_or_uri(current_char)) + is_part_of_word_top_left = guac_terminal_is_part_of_word_or_uri; + + /* If selection is on a blank, get its borders */ + else if (guac_terminal_is_blank(current_char)) + is_part_of_word_top_left = guac_terminal_is_blank; + } - /* If selection is on a word, get its borders */ - if (guac_terminal_is_part_of_word_or_uri(current_char)) - is_part_of_word = guac_terminal_is_part_of_word_or_uri; + /* Get the right function when is_part_of_word_bottom_right is NULL */ + if (is_part_of_word_bottom_right == NULL) { - /* If selection is on a blank, get its borders */ - else if (guac_terminal_is_blank(current_char)) - is_part_of_word = guac_terminal_is_blank; + /* Get top row */ + length = guac_terminal_buffer_get_columns(terminal->current_buffer, + &characters, &is_wrapped, word_row_tail); - if (is_part_of_word != NULL) { + /* Out of bounds */ + if (col >= length) + return; - /* Event to exit loop */ - bool exit_loop = false; + /* Get char on top-left of default selection */ + current_char = characters[word_col_tail].value; - do { + /* If selection is on a word, get its borders */ + if (guac_terminal_is_part_of_word_or_uri(current_char)) + is_part_of_word_bottom_right = guac_terminal_is_part_of_word_or_uri; + + /* If selection is on a blank, get its borders */ + else if (guac_terminal_is_blank(current_char)) + is_part_of_word_bottom_right = guac_terminal_is_blank; + } - /* Position of the word behind cursor. - * Default = col/row required to select a char if not a word and not blank. */ - word_col_head = col; - word_col_tail = col; - word_row_head = row; - word_row_tail = row; + /* Get top left word bounds whether current char is a part of word or URI */ + if (is_part_of_word_top_left != NULL) { - /* Get word head*/ + /* Get word head */ do { /* Buffer row to get */ @@ -1756,7 +1853,8 @@ static void guac_terminal_double_click(guac_terminal* terminal, int row, int col current_row--; /* Get current buffer row */ - length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, &is_wrapped, current_row); + length = guac_terminal_buffer_get_columns(terminal->current_buffer, + &characters, &is_wrapped, current_row); /* If we are on the previous row */ if (current_row < word_row_head) { @@ -1770,10 +1868,10 @@ static void guac_terminal_double_click(guac_terminal* terminal, int row, int col } /* Get char of the current row/column */ - flag = characters[current_col-1].value; + current_char = characters[current_col-1].value; /* Word boundary reached, stop */ - if (!is_part_of_word(flag)) + if (!is_part_of_word_top_left(current_char)) break; /* Store new position on previous row */ @@ -1783,12 +1881,17 @@ static void guac_terminal_double_click(guac_terminal* terminal, int row, int col } } while (word_col_head >= 0 && word_col_head--); + } + + /* Get bottom right word bounds whether current char is a part of word or URI */ + if (is_part_of_word_bottom_right != NULL) { /* Get word tail */ do { /* Get current buffer row */ - length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, &is_wrapped, word_row_tail); + length = guac_terminal_buffer_get_columns(terminal->current_buffer, + &characters, &is_wrapped, word_row_tail); /* Bound of screen reached and row is wrapped: get next row */ if (word_col_tail == length - 1 && is_wrapped) { @@ -1796,19 +1899,20 @@ static void guac_terminal_double_click(guac_terminal* terminal, int row, int col /* Get next buffer row */ bool next_is_wrapped; guac_terminal_char* next_characters; - guac_terminal_buffer_get_columns(terminal->current_buffer, &next_characters, &next_is_wrapped, word_row_tail + 1); + guac_terminal_buffer_get_columns(terminal->current_buffer, + &next_characters, &next_is_wrapped, word_row_tail + 1); /* Get first char of the next row */ - flag = next_characters[0].value; + current_char = next_characters[0].value; } /* Otherwise, get char of next column on current row */ else - flag = characters[word_col_tail+1].value; + current_char = characters[word_col_tail+1].value; /* Word boundary reached, stop */ - if (!is_part_of_word(flag)) + if (!is_part_of_word_bottom_right(current_char)) break; /* Store new position on next row */ @@ -1822,80 +1926,82 @@ static void guac_terminal_double_click(guac_terminal* terminal, int row, int col word_col_tail++; } while (word_col_tail <= length); + } - /* The following is only for URL scheme validation */ - if (is_part_of_word != guac_terminal_is_part_of_word_or_uri) - break; - - /* Temp vars to avoid overwrite word_row_head and word_col_head */ - int tmp_row = word_row_head; - int tmp_col = word_col_head; + /* The following is only for URL scheme validation */ + if (is_part_of_word_top_left != guac_terminal_is_part_of_word_or_uri + && is_part_of_word_bottom_right != guac_terminal_is_part_of_word_or_uri) + break; - /* Check for the presence of a uri scheme like /^[a-z]+\:\/{2}/ */ - do { + /* Temp vars to avoid overwrite word_row_head and word_col_head */ + int tmp_row = word_row_head; + int tmp_col = word_col_head; - /* Get first char of first row */ - length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, &is_wrapped, tmp_row); - current_char = characters[tmp_col].value; + /* Check for the presence of a uri scheme like /^[a-z]+\:\/{2}/ */ + do { - /* End of [a-z]+ part */ - if (current_char < 'a' || current_char > 'z') { + /* Get first char of first row */ + length = guac_terminal_buffer_get_columns(terminal->current_buffer, + &characters, &is_wrapped, tmp_row); + current_char = characters[tmp_col].value; - /* URI scheme delimiter :// foud */ - if (current_char == ':' && - characters[tmp_col+1].value == '/' && - characters[tmp_col+2].value == '/') { + /* End of [a-z]+ part */ + if (current_char < 'a' || current_char > 'z') { - /* Set exit event */ - exit_loop = true; - break; - } + /* URI scheme delimiter :// foud */ + if (current_char == ':' && + characters[tmp_col+1].value == '/' && + characters[tmp_col+2].value == '/') { - /* Not URI scheme */ - else - break; + /* Set exit event */ + exit_loop = true; + break; } - /* End of buffer row */ - else if (tmp_col == length-1) { - - /* Confinue only if current buffer row is wrapped */ - if (is_wrapped) { + /* Not URI scheme */ + else + break; + } - /* Stop if latest row */ - if (tmp_row == word_row_tail) - break; + /* End of buffer row */ + else if (tmp_col == length-1) { - /* Go to next row */ - tmp_row++; + /* Confinue only if current buffer row is wrapped */ + if (is_wrapped) { - /* Go to first row (-1 for auto increment on next iteration) */ - tmp_col = 0; + /* Stop if latest row */ + if (tmp_row == word_row_tail) + break; - /* Don't do further tests for this iteration */ - continue; + /* Go to next row */ + tmp_row++; - } + /* Go to first row (-1 for auto increment on next iteration) */ + tmp_col = 0; - /* End of selection without matching uri scheme */ - else - break; + /* Don't do further tests for this iteration */ + continue; } /* End of selection without matching uri scheme */ - else if (tmp_row == word_row_tail && tmp_col == word_col_tail) + else break; - tmp_col++; - //printf("tmp_row = %d | word_row_tail = %d | tmp_col = %d | word_col_tail = %d\n", tmp_row, word_row_tail, tmp_col, word_col_tail); - } while (true); + } - /* Get word boundaries instead of URI */ - is_part_of_word = guac_terminal_is_part_of_word; + /* End of selection without matching uri scheme */ + else if (tmp_row == word_row_tail && tmp_col == word_col_tail) + break; - } while (!exit_loop); - } + tmp_col++; + } while (true); + + /* Get word boundaries instead of URI */ + is_part_of_word_top_left = + is_part_of_word_bottom_right = guac_terminal_is_part_of_word; + + } while (!exit_loop); /* Select and add to clipboard the "word" */ guac_terminal_select_start(terminal, word_row_head, word_col_head); @@ -1917,24 +2023,39 @@ static void guac_terminal_double_click(guac_terminal* terminal, int row, int col * * @param col * The column where is the mouse at the triple click event. + * + * @param hold + * True when user hold left click. */ -static void guac_terminal_triple_click(guac_terminal* terminal, int row, int col) { +static void guac_terminal_triple_click(guac_terminal* terminal, int row, int col, bool hold) { /* Temporarily reading previous and next lines */ guac_terminal_char* characters; bool is_wrapped; int length; - /* Final boundary rows */ int top_row = row; int bottom_row = row; + /* User holds left click */ + if (hold) { + + /* Use initial row as bottom of the selection and go up */ + if (row <= terminal->selection_initial_row) + bottom_row = terminal->selection_initial_row; + + /* Use initial row as top of the selection and go down */ + if (row > terminal->selection_initial_row) + top_row = terminal->selection_initial_row; + } + /* Get top boundary */ do { /* Read previous buffer row */ - length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, &is_wrapped, top_row - 1); + length = guac_terminal_buffer_get_columns(terminal->current_buffer, + &characters, &is_wrapped, top_row - 1); /* Go to the previous row if it is wrapped */ } while (is_wrapped && top_row--); @@ -1943,7 +2064,8 @@ static void guac_terminal_triple_click(guac_terminal* terminal, int row, int col do { /* Read current buffer row */ - length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, &is_wrapped, bottom_row); + length = guac_terminal_buffer_get_columns(terminal->current_buffer, + &characters, &is_wrapped, bottom_row); /* Go to the next row if current row is wrapped */ } while (is_wrapped && bottom_row++); @@ -2037,27 +2159,42 @@ static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user, * the ALT key at the start of the selection */ term->rectangle_selection = term->mod_alt; + /* Save initial mouse position */ + term->selection_initial_row = row; + term->selection_initial_column = col; + /* Start selection */ guac_terminal_select_start(term, row, col); break; /* Second click = word selection */ case 1: - guac_terminal_double_click(term, row, col); + guac_terminal_double_click(term, row, col, false); break; /* third click or more = line selection */ default: - guac_terminal_triple_click(term, row, col); + guac_terminal_triple_click(term, row, col, false); break; } } } - /* In all other cases, simply update the existing selection as long as - * the mouse button is pressed */ + /* Hold left click */ else - guac_terminal_select_update(term, row, col); + + /* Simply update the existing selection as long as + * the mouse button is pressed */ + if (term->click_counter <= 1) + guac_terminal_select_update(term, row, col); + + /* Second click + hold = word selection update */ + else if (term->click_counter == 2) + guac_terminal_double_click(term, row, col, true); + + /* third click or more + hold = line selection update */ + else + guac_terminal_triple_click(term, row, col, true); } diff --git a/src/terminal/terminal/terminal-priv.h b/src/terminal/terminal/terminal-priv.h index 55afa2ea1..8ff8177e4 100644 --- a/src/terminal/terminal/terminal-priv.h +++ b/src/terminal/terminal/terminal-priv.h @@ -365,6 +365,16 @@ struct guac_terminal { */ bool selection_committed; + /** + * The row where the first click of selection was made. + */ + int selection_initial_row; + + /** + * The column where the first click of selection was made. + */ + int selection_initial_column; + /** * The row that the selection starts at. */ From 891875d15352fe5e63d1e5d3d1e87fdc2f94cdea Mon Sep 17 00:00:00 2001 From: corentin-soriano Date: Sun, 6 Oct 2024 12:36:14 +0200 Subject: [PATCH 7/7] GUACAMOLE-1961: Add generic function to get word bounds on a terminal buffer. --- src/terminal/terminal.c | 449 +++++++++++++++++++++------------------- 1 file changed, 237 insertions(+), 212 deletions(-) diff --git a/src/terminal/terminal.c b/src/terminal/terminal.c index 3dc3c9c2a..8877ac3b1 100644 --- a/src/terminal/terminal.c +++ b/src/terminal/terminal.c @@ -1645,7 +1645,7 @@ static bool guac_terminal_is_part_of_word(int ascii_char) { * true if match a "word" or "uri" char, * false otherwise. */ -static bool guac_terminal_is_part_of_word_or_uri(int ascii_char) { +static bool guac_terminal_is_part_of_uri(int ascii_char) { return (guac_terminal_is_part_of_word(ascii_char) || (ascii_char == '%') || (ascii_char == '&') || @@ -1734,29 +1734,32 @@ static void guac_terminal_word_initial_position(guac_terminal* terminal, } /** - * Selection of a word during a double click event. - * - Fetching the character under the mouse cursor. - * - Determining the type of character : - * Letter, digit, acceptable symbol within a word, - * or space/NULL, - * all other chars are treated as single. - * - Calculating the word boundaries. - * - Visual selection of the found word. - * - Adding it to clipboard. + * Get word/URI/blank boundaries on a terminal buffer depending on given position: + * existing selection to update or mouse position. Gets the boundaries in the + * left/up direction and then in the right/down direction, continuing if the + * row is wrapped. + * A sequence of the same character whatever it is will be treated as a word. * - * @param terminal - * The terminal that received a double click event. + * @param buffer + * The terminal buffer where to search word boundaries. * - * @param row - * The row where is the mouse at the double click event. - * - * @param col - * The column where is the mouse at the double click event. + * @param detector + * Pointer to the desired function for detecting boundaries. * - * @param hold - * True when user hold left click, false otherwise. + * @param col_head + * Pointer where to write the head column of the word. + * + * @param col_tail + * Pointer where to write the tail column of the word. + * + * @param row_head + * Pointer where to write the head row of the word. + * + * @param row_tail + * Pointer where to write the tail row of the word. */ -static void guac_terminal_double_click(guac_terminal* terminal, int row, int col, bool hold) { +static void guac_terminal_get_word_bounds(guac_terminal_buffer* buffer, bool (*detector)(int), + int* col_head, int* col_tail, int* row_head, int* row_tail) { /* To store buffer row characters */ guac_terminal_char* characters; @@ -1766,242 +1769,267 @@ static void guac_terminal_double_click(guac_terminal* terminal, int row, int col int length; /* Character read at a position */ int current_char; + /* The char behind the given row/col */ + int initial_char; - /* Position of the word behind cursor */ - int word_col_head; - int word_col_tail; - int word_row_head; - int word_row_tail; - - /* The function used to calculate the word borders */ - bool (*is_part_of_word_top_left)(int) = NULL; - bool (*is_part_of_word_bottom_right)(int) = NULL; - - /* Event to exit main loop */ - bool exit_loop = false; + /* Detect the correct function regardless of direction */ + bool (*is_part_of)(int) = NULL; + /* Get word head */ do { - /* Default = col/row required to select a char if not a word and not blank */ - word_col_head = col; - word_col_tail = col; - word_row_head = row; - word_row_tail = row; - - /* User holds left click: update default selection boundaries */ - if (hold) - guac_terminal_word_initial_position(terminal, col, row, &word_col_head, - &word_col_tail, &word_row_head, &word_row_tail); - - /* Get the right function when is_part_of_word_top_left is NULL */ - if (is_part_of_word_top_left == NULL) { - - /* Get top row */ - length = guac_terminal_buffer_get_columns(terminal->current_buffer, - &characters, &is_wrapped, word_row_head); - - /* Out of bounds */ - if (col >= length) - return; - - /* Get char on top-left of default selection */ - current_char = characters[word_col_head].value; - - /* If selection is on a word, get its borders */ - if (guac_terminal_is_part_of_word_or_uri(current_char)) - is_part_of_word_top_left = guac_terminal_is_part_of_word_or_uri; - - /* If selection is on a blank, get its borders */ - else if (guac_terminal_is_blank(current_char)) - is_part_of_word_top_left = guac_terminal_is_blank; - } - /* Get the right function when is_part_of_word_bottom_right is NULL */ - if (is_part_of_word_bottom_right == NULL) { + /* Buffer row to get */ + int current_row = *row_head; + int current_col = *col_head; - /* Get top row */ - length = guac_terminal_buffer_get_columns(terminal->current_buffer, - &characters, &is_wrapped, word_row_tail); + /* Bound of screen reached: get previous row */ + if (*col_head == 0) + current_row--; - /* Out of bounds */ - if (col >= length) - return; + /* Get current buffer row */ + length = guac_terminal_buffer_get_columns(buffer, + &characters, &is_wrapped, current_row); - /* Get char on top-left of default selection */ - current_char = characters[word_col_tail].value; + /* If we are on the previous row */ + if (current_row < *row_head) { - /* If selection is on a word, get its borders */ - if (guac_terminal_is_part_of_word_or_uri(current_char)) - is_part_of_word_bottom_right = guac_terminal_is_part_of_word_or_uri; + /* Line not wrapped: stop, it's the word boundary */ + if (!is_wrapped) + break; - /* If selection is on a blank, get its borders */ - else if (guac_terminal_is_blank(current_char)) - is_part_of_word_bottom_right = guac_terminal_is_blank; + /* Go to last column of this row */ + current_col = length; } - /* Get top left word bounds whether current char is a part of word or URI */ - if (is_part_of_word_top_left != NULL) { - - /* Get word head */ - do { - - /* Buffer row to get */ - int current_row = word_row_head; - int current_col = word_col_head; - - /* Bound of screen reached: get previous row */ - if (word_col_head == 0) - current_row--; - - /* Get current buffer row */ - length = guac_terminal_buffer_get_columns(terminal->current_buffer, - &characters, &is_wrapped, current_row); - - /* If we are on the previous row */ - if (current_row < word_row_head) { - - /* Line not wrapped: stop, it's the word boundary */ - if (!is_wrapped) - break; + /* Get char of the previous column on current row */ + current_char = characters[current_col-1].value; - /* Go to last column of this row */ - current_col = length; - } - - /* Get char of the current row/column */ - current_char = characters[current_col-1].value; + /* Init is_part_of on first iteration according to the char on the + * current row and column */ + if (is_part_of == NULL) { + initial_char = characters[current_col].value; + is_part_of = guac_terminal_is_blank(initial_char) + ? guac_terminal_is_blank + : detector; + } - /* Word boundary reached, stop */ - if (!is_part_of_word_top_left(current_char)) - break; + /* End of sequence of the same character */ + if (!is_part_of(initial_char) && current_char != initial_char) + break; - /* Store new position on previous row */ - if (current_row < word_row_head) { - word_row_head = current_row; - word_col_head = current_col; - } + /* Word boundary reached */ + if (is_part_of(initial_char) && !is_part_of(current_char)) + break; - } while (word_col_head >= 0 && word_col_head--); + /* Store new position on previous row */ + if (current_row < *row_head) { + *row_head = current_row; + *col_head = current_col; } - /* Get bottom right word bounds whether current char is a part of word or URI */ - if (is_part_of_word_bottom_right != NULL) { + } while (*col_head >= 0 && (*col_head)--); - /* Get word tail */ - do { + /* Reset to detect the correct function while moving forward */ + is_part_of = NULL; - /* Get current buffer row */ - length = guac_terminal_buffer_get_columns(terminal->current_buffer, - &characters, &is_wrapped, word_row_tail); + /* Get word tail */ + do { - /* Bound of screen reached and row is wrapped: get next row */ - if (word_col_tail == length - 1 && is_wrapped) { + /* Get current buffer row */ + length = guac_terminal_buffer_get_columns(buffer, + &characters, &is_wrapped, *row_tail); - /* Get next buffer row */ - bool next_is_wrapped; - guac_terminal_char* next_characters; - guac_terminal_buffer_get_columns(terminal->current_buffer, - &next_characters, &next_is_wrapped, word_row_tail + 1); + /* Bound of screen reached and row is wrapped: get next row */ + if (*col_tail == length - 1 && is_wrapped) { - /* Get first char of the next row */ - current_char = next_characters[0].value; + /* Get next buffer row */ + bool next_is_wrapped; + guac_terminal_char* next_characters; + guac_terminal_buffer_get_columns(buffer, + &next_characters, &next_is_wrapped, *row_tail + 1); - } + /* Get first char of the next row */ + current_char = next_characters[0].value; - /* Otherwise, get char of next column on current row */ - else - current_char = characters[word_col_tail+1].value; + } - /* Word boundary reached, stop */ - if (!is_part_of_word_bottom_right(current_char)) - break; + /* Otherwise, get char of next column on current row */ + else + current_char = characters[*col_tail+1].value; + + /* Init is_part_of on first iteration according to the char on the + * current row and column */ + if (is_part_of == NULL) { + initial_char = characters[*col_tail].value; + is_part_of = guac_terminal_is_blank(initial_char) + ? guac_terminal_is_blank + : detector; + } - /* Store new position on next row */ - if (word_col_tail == length - 1 && is_wrapped) { - word_row_tail++; - word_col_tail = 0; - } + /* End of sequence of the same character */ + if (!is_part_of(initial_char) && current_char != initial_char) + break; - /* Or go to next column of current row */ - else - word_col_tail++; + /* Word boundary reached */ + if (is_part_of(initial_char) && !is_part_of(current_char)) + break; - } while (word_col_tail <= length); + /* Store new position on next row */ + if (*col_tail == length - 1 && is_wrapped) { + (*row_tail)++; + *col_tail = 0; } - /* The following is only for URL scheme validation */ - if (is_part_of_word_top_left != guac_terminal_is_part_of_word_or_uri - && is_part_of_word_bottom_right != guac_terminal_is_part_of_word_or_uri) - break; + /* Or go to next column of current row */ + else + (*col_tail)++; - /* Temp vars to avoid overwrite word_row_head and word_col_head */ - int tmp_row = word_row_head; - int tmp_col = word_col_head; + } while (*col_tail <= length); - /* Check for the presence of a uri scheme like /^[a-z]+\:\/{2}/ */ - do { +} + +/** + * Selection of a word during a double click event. + * - Fetching the character under the mouse cursor. + * - Determining the type of character : + * Letter, digit, acceptable symbol within a word, + * or space/NULL, + * all other chars are treated as single. + * - Calculating the word boundaries. + * - Visual selection of the found word. + * - Adding it to clipboard. + * + * @param terminal + * The terminal that received a double click event. + * + * @param row + * The row where is the mouse at the double click event. + * + * @param col + * The column where is the mouse at the double click event. + * + * @param hold + * True when user hold left click, false otherwise. + */ +static void guac_terminal_double_click(guac_terminal* terminal, int row, int col, bool hold) { - /* Get first char of first row */ - length = guac_terminal_buffer_get_columns(terminal->current_buffer, - &characters, &is_wrapped, tmp_row); - current_char = characters[tmp_col].value; + /* To store buffer row characters */ + guac_terminal_char* characters; + /* To get wrapped buffer row status */ + bool is_wrapped; + /* Length of the buffer row */ + int length; + /* Character read at a position */ + int current_char; - /* End of [a-z]+ part */ - if (current_char < 'a' || current_char > 'z') { + /* Position of the detected word. Default = col/row required to select + * a char if not a word and not blank */ + int word_col_head = col; + int word_col_tail = col; + int word_row_head = row; + int word_row_tail = row; + + /* Position of the detected URI */ + int uri_col_head; + int uri_col_tail; + int uri_row_head; + int uri_row_tail; + + /* User holds left click: update default selection boundaries */ + if (hold) + guac_terminal_word_initial_position(terminal, col, row, &word_col_head, + &word_col_tail, &word_row_head, &word_row_tail); + + /* Try to get boundaries of a word */ + guac_terminal_get_word_bounds(terminal->current_buffer, guac_terminal_is_part_of_word, + &word_col_head, &word_col_tail, &word_row_head, &word_row_tail); + + /* Search for URI only when user don't hold left click + * to unconditionally extend selection to word pattern */ + if (!hold) { + + /* Begin uri search on previously found word position, + * this avoids going through the same characters twice */ + uri_col_head = word_col_head; + uri_col_tail = word_col_tail; + uri_row_head = word_row_head; + uri_row_tail = word_row_tail; + + /* Get boundaries of potential URI */ + guac_terminal_get_word_bounds(terminal->current_buffer, guac_terminal_is_part_of_uri, + &uri_col_head, &uri_col_tail, &uri_row_head, &uri_row_tail); + + /* Check if uri dected */ + if ((uri_col_head != word_col_head || uri_col_tail != word_col_tail || + uri_row_head != word_row_head || uri_row_tail != word_row_tail)) { + + /* Temp vars to avoid overwrite uri_row_head and uri_col_head values */ + int tmp_row = uri_row_head; + int tmp_col = uri_col_head; + + /* Check for the presence of a uri scheme like /^[a-z]+\:\/{2}/ */ + do { - /* URI scheme delimiter :// foud */ - if (current_char == ':' && - characters[tmp_col+1].value == '/' && - characters[tmp_col+2].value == '/') { + /* Get first char of first row */ + length = guac_terminal_buffer_get_columns(terminal->current_buffer, + &characters, &is_wrapped, tmp_row); + current_char = characters[tmp_col].value; + + /* End of [a-z]+ part */ + if (current_char < 'a' || current_char > 'z') { + + /* URI scheme delimiter :// foud */ + if (current_char == ':' && + characters[tmp_col+1].value == '/' && + characters[tmp_col+2].value == '/') { + + /* Use URI limits instead of word limits */ + word_col_head = uri_col_head; + word_col_tail = uri_col_tail; + word_row_head = uri_row_head; + word_row_tail = uri_row_tail; + break; + } - /* Set exit event */ - exit_loop = true; - break; + /* Not URI scheme */ + else + break; } - /* Not URI scheme */ - else - break; - } + /* End of buffer row */ + else if (tmp_col == length-1) { - /* End of buffer row */ - else if (tmp_col == length-1) { + /* Confinue only if current buffer row is wrapped */ + if (is_wrapped) { - /* Confinue only if current buffer row is wrapped */ - if (is_wrapped) { + /* Stop if latest row */ + if (tmp_row == uri_row_tail) + break; - /* Stop if latest row */ - if (tmp_row == word_row_tail) - break; + /* Go to first col of next row */ + tmp_col = 0; + tmp_row++; - /* Go to next row */ - tmp_row++; + /* Don't do further tests for this iteration */ + continue; - /* Go to first row (-1 for auto increment on next iteration) */ - tmp_col = 0; + } - /* Don't do further tests for this iteration */ - continue; + /* End of selection without matching uri scheme */ + else + break; } /* End of selection without matching uri scheme */ - else + else if (tmp_row == uri_row_tail && tmp_col == uri_col_tail) break; - } - - /* End of selection without matching uri scheme */ - else if (tmp_row == word_row_tail && tmp_col == word_col_tail) - break; - - tmp_col++; - } while (true); - - /* Get word boundaries instead of URI */ - is_part_of_word_top_left = - is_part_of_word_bottom_right = guac_terminal_is_part_of_word; - - } while (!exit_loop); + /* Go to next col on current row */ + tmp_col++; + } while (true); + } + } /* Select and add to clipboard the "word" */ guac_terminal_select_start(terminal, word_row_head, word_col_head); @@ -2020,14 +2048,11 @@ static void guac_terminal_double_click(guac_terminal* terminal, int row, int col * * @param row * The row where is the mouse at the triple click event. - * - * @param col - * The column where is the mouse at the triple click event. * * @param hold * True when user hold left click. */ -static void guac_terminal_triple_click(guac_terminal* terminal, int row, int col, bool hold) { +static void guac_terminal_triple_click(guac_terminal* terminal, int row, bool hold) { /* Temporarily reading previous and next lines */ guac_terminal_char* characters; @@ -2174,7 +2199,7 @@ static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user, /* third click or more = line selection */ default: - guac_terminal_triple_click(term, row, col, false); + guac_terminal_triple_click(term, row, false); break; } } @@ -2194,7 +2219,7 @@ static int __guac_terminal_send_mouse(guac_terminal* term, guac_user* user, /* third click or more + hold = line selection update */ else - guac_terminal_triple_click(term, row, col, true); + guac_terminal_triple_click(term, row, true); }