blob: 0a075ebd866334a691a80b850ba305383c5c76f5 [file] [log] [blame]
/*
* 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 <guacamole/client.h>
#include <guacamole/socket.h>
#include <guacamole/unicode.h>
#include <stdbool.h>
/**
* 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);
}
}