/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ #include "config.h" #include "common/clipboard.h" #include "terminal/buffer.h" #include "terminal/display.h" #include "terminal/select.h" #include "terminal/terminal.h" #include "terminal/types.h" #include #include #include #include /** * Returns the coordinates for the currently-selected range of text within the * given terminal, normalized such that the start coordinate is before the end * coordinate and the end coordinate takes into account character width. If no * text is currently selected, the behavior of this function is undefined. * * @param terminal * The guac_terminal instance whose selected text coordinates should be * retrieved in normalized form. * * @param start_row * A pointer to an int which should receive the row number of the first * character of text selected within the terminal, where the first * (top-most) row in the terminal is row 0. Rows within the scrollback * buffer (above the top-most row of the terminal) will be negative. * * @param start_col * A pointer to an int which should receive the column number of the first * character of text selected within terminal, where 0 is the first * (left-most) column within the row. * * @param end_row * A pointer to an int which should receive the row number of the last * character of text selected within the terminal, where the first * (top-most) row in the terminal is row 0. Rows within the scrollback * buffer (above the top-most row of the terminal) will be negative. * * @param end_col * A pointer to an int which should receive the column number of the first * character of text selected within terminal, taking into account the * width of that character, where 0 is the first (left-most) column within * the row. */ static void guac_terminal_select_normalized_range(guac_terminal* terminal, int* start_row, int* start_col, int* end_row, int* end_col) { /* 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)) { *start_row = terminal->selection_start_row; *start_col = terminal->selection_start_column; *end_row = terminal->selection_end_row; *end_col = terminal->selection_end_column + terminal->selection_end_width - 1; } /* Coordinates must otherwise be swapped in addition to adjusting for * final character width */ else { *end_row = terminal->selection_start_row; *end_col = terminal->selection_start_column + terminal->selection_start_width - 1; *start_row = terminal->selection_end_row; *start_col = terminal->selection_end_column; } } void guac_terminal_select_redraw(guac_terminal* terminal) { /* Update the selected region of the display if text is currently * selected */ if (terminal->text_selected) { int start_row = terminal->selection_start_row + terminal->scroll_offset; int start_column = terminal->selection_start_column; int end_row = terminal->selection_end_row + terminal->scroll_offset; int end_column = terminal->selection_end_column; /* Update start/end columns to include character width */ if (start_row > end_row || (start_row == end_row && start_column > end_column)) start_column += terminal->selection_start_width - 1; else end_column += terminal->selection_end_width - 1; guac_terminal_display_select(terminal->display, start_row, start_column, end_row, end_column); } /* Clear the display selection if no text is currently selected */ else guac_terminal_display_clear_select(terminal->display); } /** * Locates the beginning of the character at the given row and column, updating * the column to the starting column of that character. The width, if available, * is returned. If the character has no defined width, 1 is returned. * * @param terminal * The guac_terminal in which the character should be located. * * @param row * The row number of the desired character, where the first (top-most) row * in the terminal is row 0. Rows within the scrollback buffer (above the * top-most row of the terminal) will be negative. * * @param column * A pointer to an int containing the column number of the desired * character, where 0 is the first (left-most) column within the row. If * the character is a multi-column character, the value of this int will be * adjusted as necessary such that it contains the column number of the * first column containing the character. * * @return * The width of the specified character, in columns, or 1 if the character * has no defined width. */ static int guac_terminal_find_char(guac_terminal* terminal, int row, int* column) { int start_column = *column; guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(terminal->buffer, row, 0); if (start_column < buffer_row->length) { /* Find beginning of character */ guac_terminal_char* start_char = &(buffer_row->characters[start_column]); while (start_column > 0 && start_char->value == GUAC_CHAR_CONTINUATION) { start_char--; start_column--; } /* Use width, if available */ if (start_char->value != GUAC_CHAR_CONTINUATION) { *column = start_column; return start_char->width; } } /* Default to one column wide */ return 1; } void guac_terminal_select_start(guac_terminal* terminal, int row, int column) { int width = guac_terminal_find_char(terminal, row, &column); terminal->selection_start_row = terminal->selection_end_row = row; terminal->selection_start_column = terminal->selection_end_column = column; terminal->selection_start_width = terminal->selection_end_width = width; terminal->text_selected = false; terminal->selection_committed = false; guac_terminal_notify(terminal); } void guac_terminal_select_update(guac_terminal* terminal, int row, int column) { /* Only update if selection has changed */ if (row != terminal->selection_end_row || column < terminal->selection_end_column || column >= terminal->selection_end_column + terminal->selection_end_width) { int width = guac_terminal_find_char(terminal, row, &column); terminal->selection_end_row = row; terminal->selection_end_column = column; terminal->selection_end_width = width; terminal->text_selected = true; guac_terminal_notify(terminal); } } void guac_terminal_select_resume(guac_terminal* terminal, int row, int column) { int selection_start_row; int selection_start_column; int selection_end_row; int selection_end_column; /* No need to test coordinates if no text is selected at all */ if (!terminal->text_selected) return; /* Use normalized coordinates for sake of simple comparison */ guac_terminal_select_normalized_range(terminal, &selection_start_row, &selection_start_column, &selection_end_row, &selection_end_column); /* Prefer to expand from start, such that attempting to resume a selection * within the existing selection preserves the top-most portion of the * selection */ if (row > selection_start_row || (row == selection_start_row && column > selection_start_column)) { terminal->selection_start_row = selection_start_row; terminal->selection_start_column = selection_start_column; } /* Expand from bottom-most portion of selection if doing otherwise would * reduce the size of the selection */ else { terminal->selection_start_row = selection_end_row; terminal->selection_start_column = selection_end_column; } /* Selection is again in-progress */ terminal->selection_committed = false; /* Update selection to contain given character */ guac_terminal_select_update(terminal, row, column); } /** * Appends the text within the given subsection of a terminal row to the * clipboard. The provided coordinates are considered inclusiveley (the * characters at the start and end column are included in the copied * text). Any out-of-bounds coordinates will be automatically clipped within * the bounds of the given row. * * @param terminal * The guac_terminal instance associated with the buffer containing the * text being copied and the clipboard receiving the copied text. * * @param row * The row number of the text within the terminal to be copied into the * clipboard, where the first (top-most) row in the terminal is row 0. Rows * within the scrollback buffer (above the top-most row of the terminal) * will be negative. * * @param start * The first column of the text to be copied from the given row into the * clipboard associated with the given terminal, where 0 is the first * (left-most) column within the row. * * @param end * The last column of the text to be copied from the given row into the * clipboard associated with the given terminal, where 0 is the first * (left-most) column within the row, or a negative value to denote that * the last column in the row should be used. */ static void guac_terminal_clipboard_append_row(guac_terminal* terminal, int row, int start, int end) { char buffer[1024]; int i = start; guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(terminal->buffer, row, 0); /* If selection is entirely outside the bounds of the row, then there is * nothing to append */ if (start < 0 || start > buffer_row->length - 1) return; /* Clip given range to actual bounds of row */ if (end < 0 || end > buffer_row->length - 1) end = buffer_row->length - 1; /* Repeatedly convert chunks of terminal buffer rows until entire specified * region has been appended to clipboard */ while (i <= end) { int remaining = sizeof(buffer); char* current = buffer; /* Convert as many codepoints within the given range as possible */ for (i = start; i <= end; i++) { int codepoint = buffer_row->characters[i].value; /* Ignore null (blank) characters */ if (codepoint == 0 || codepoint == GUAC_CHAR_CONTINUATION) continue; /* Encode current codepoint as UTF-8 */ int bytes = guac_utf8_write(codepoint, current, remaining); if (bytes == 0) break; current += bytes; remaining -= bytes; } /* Append converted buffer to clipboard */ guac_common_clipboard_append(terminal->clipboard, buffer, current - buffer); } } void guac_terminal_select_end(guac_terminal* terminal) { guac_client* client = terminal->client; guac_socket* socket = client->socket; /* If no text is selected, nothing to do */ if (!terminal->text_selected) return; /* Selection is now committed */ terminal->selection_committed = true; /* Reset current clipboard contents */ guac_common_clipboard_reset(terminal->clipboard, "text/plain"); int start_row, start_col; int end_row, end_col; /* Ensure proper ordering of start and end coords */ guac_terminal_select_normalized_range(terminal, &start_row, &start_col, &end_row, &end_col); /* If only one row, simply copy */ if (end_row == start_row) guac_terminal_clipboard_append_row(terminal, start_row, start_col, end_col); /* Otherwise, copy multiple rows */ else { /* Store first row */ guac_terminal_clipboard_append_row(terminal, start_row, start_col, -1); /* Store all middle rows */ for (int row = start_row + 1; row < end_row; row++) { guac_common_clipboard_append(terminal->clipboard, "\n", 1); guac_terminal_clipboard_append_row(terminal, row, 0, -1); } /* Store last row */ guac_common_clipboard_append(terminal->clipboard, "\n", 1); guac_terminal_clipboard_append_row(terminal, end_row, 0, end_col); } /* Send data */ if (!terminal->disable_copy) { guac_common_clipboard_send(terminal->clipboard, client); guac_socket_flush(socket); } guac_terminal_notify(terminal); } bool guac_terminal_select_contains(guac_terminal* terminal, int start_row, int start_column, int end_row, int end_column) { int selection_start_row; int selection_start_column; int selection_end_row; int selection_end_column; /* No need to test coordinates if no text is selected at all */ if (!terminal->text_selected) return false; /* Use normalized coordinates for sake of simple comparison */ guac_terminal_select_normalized_range(terminal, &selection_start_row, &selection_start_column, &selection_end_row, &selection_end_column); /* If test range starts after highlight ends, does not intersect */ if (start_row > selection_end_row) return false; if (start_row == selection_end_row && start_column > selection_end_column) return false; /* If test range ends before highlight starts, does not intersect */ if (end_row < selection_start_row) return false; if (end_row == selection_start_row && end_column < selection_start_column) return false; /* Otherwise, does intersect */ return true; } void guac_terminal_select_touch(guac_terminal* terminal, int start_row, int start_column, int end_row, int end_column) { /* Only clear selection if selection is committed */ if (!terminal->selection_committed) return; /* Clear selection if it contains any characters within the given region */ if (guac_terminal_select_contains(terminal, start_row, start_column, end_row, end_column)) { /* Text is no longer selected */ terminal->text_selected = false; terminal->selection_committed = false; guac_terminal_notify(terminal); } }