diff --git a/src/terminal/display.c b/src/terminal/display.c index 402b849dcf..dae9e98a62 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 406a0c43f5..a1b557d1be 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 7e52a4b916..8877ac3b1b 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\$\-\.\/_~] and accented letters. + * This allows a path, variable name or IP address to be treated as a word. * * @param ascii_char * The character to check. @@ -1624,18 +1624,45 @@ 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 == '&') || (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_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 == '#')); } /** @@ -1652,6 +1679,215 @@ 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; + } +} + +/** + * 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 buffer + * The terminal buffer where to search word boundaries. + * + * @param detector + * Pointer to the desired function for detecting boundaries. + * + * @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_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; + /* To get wrapped buffer row status */ + bool is_wrapped; + /* Length of the buffer row */ + int length; + /* Character read at a position */ + int current_char; + /* The char behind the given row/col */ + int initial_char; + + /* Detect the correct function regardless of direction */ + bool (*is_part_of)(int) = NULL; + + /* Get word head */ + do { + + /* Buffer row to get */ + int current_row = *row_head; + int current_col = *col_head; + + /* Bound of screen reached: get previous row */ + if (*col_head == 0) + current_row--; + + /* Get current buffer row */ + length = guac_terminal_buffer_get_columns(buffer, + &characters, &is_wrapped, current_row); + + /* If we are on the previous row */ + if (current_row < *row_head) { + + /* Line not wrapped: stop, it's the word boundary */ + if (!is_wrapped) + break; + + /* Go to last column of this row */ + current_col = length; + } + + /* Get char of the previous column on current row */ + 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; + } + + /* End of sequence of the same character */ + if (!is_part_of(initial_char) && current_char != initial_char) + break; + + /* Word boundary reached */ + if (is_part_of(initial_char) && !is_part_of(current_char)) + break; + + /* Store new position on previous row */ + if (current_row < *row_head) { + *row_head = current_row; + *col_head = current_col; + } + + } while (*col_head >= 0 && (*col_head)--); + + /* Reset to detect the correct function while moving forward */ + is_part_of = NULL; + + /* Get word tail */ + do { + + /* Get current buffer row */ + length = guac_terminal_buffer_get_columns(buffer, + &characters, &is_wrapped, *row_tail); + + /* Bound of screen reached and row is wrapped: get next row */ + if (*col_tail == length - 1 && is_wrapped) { + + /* 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[*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; + } + + /* End of sequence of the same character */ + if (!is_part_of(initial_char) && current_char != initial_char) + break; + + /* Word boundary reached */ + if (is_part_of(initial_char) && !is_part_of(current_char)) + break; + + /* Store new position on next row */ + if (*col_tail == length - 1 && is_wrapped) { + (*row_tail)++; + *col_tail = 0; + } + + /* Or go to next column of current row */ + else + (*col_tail)++; + + } while (*col_tail <= length); + +} + /** * Selection of a word during a double click event. * - Fetching the character under the mouse cursor. @@ -1671,55 +1907,199 @@ 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; - int length = guac_terminal_buffer_get_columns(terminal->current_buffer, &characters, NULL, row); + /* To get wrapped buffer row status */ + bool is_wrapped; + /* Length of the buffer row */ + int length; + /* Character read at a position */ + int current_char; + + /* 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 { + + /* 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; + } - if (col >= length) - return; + /* Not URI scheme */ + else + break; + } - /* (char)10 behind cursor */ - int current_char = characters[col].value; + /* End of buffer row */ + else if (tmp_col == length-1) { - /* Position of the word behind cursor. - * Default = col required to select a char if not a word and not blank. */ + /* Confinue only if current buffer row is wrapped */ + if (is_wrapped) { - /* The function used to calculate the word borders */ - bool (*is_part_of_word)(int) = NULL; + /* Stop if latest row */ + if (tmp_row == uri_row_tail) + break; - /* 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; + /* Go to first col of next row */ + tmp_col = 0; + tmp_row++; - /* 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; + /* Don't do further tests for this iteration */ + continue; - int word_head = col; - int word_tail = col; + } - if (is_part_of_word != NULL) { + /* End of selection without matching uri scheme */ + else + break; - /* Get word head*/ - for (; word_head - 1 >= 0; word_head--) { - if (!is_part_of_word(characters[word_head - 1].value)) - break; - } + } - /* 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)) - break; - } + /* End of selection without matching uri scheme */ + else if (tmp_row == uri_row_tail && tmp_col == uri_col_tail) + break; + /* Go to next col on current row */ + tmp_col++; + } while (true); + } } /* 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); + +} + +/** + * 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 hold + * True when user hold left click. + */ +static void guac_terminal_triple_click(guac_terminal* terminal, int row, 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); + + /* 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, @@ -1800,27 +2180,46 @@ 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; + + /* 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_select_start(term, row, 0); - guac_terminal_select_update(term, row, term->display->width); + guac_terminal_triple_click(term, row, 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, true); } diff --git a/src/terminal/terminal/display.h b/src/terminal/terminal/display.h index 6355360888..13b17370f3 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 52462ce54c..8ff8177e42 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. */ @@ -491,6 +501,11 @@ struct guac_terminal { */ int click_counter; + /** + * Rectangular selection when ALT key is pressed when starting selection. + */ + bool rectangle_selection; + }; /** diff --git a/src/terminal/terminal/terminal.h b/src/terminal/terminal/terminal.h index 11c8fb5a36..981dea0de1 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.