select.c (15448B)
1/* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, 13 * software distributed under the License is distributed on an 14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 * KIND, either express or implied. See the License for the 16 * specific language governing permissions and limitations 17 * under the License. 18 */ 19 20 21#include "common/clipboard.h" 22#include "terminal/buffer.h" 23#include "terminal/display.h" 24#include "terminal/select.h" 25#include "terminal/terminal.h" 26#include "terminal/terminal-priv.h" 27#include "terminal/types.h" 28 29#include <guacamole/client.h> 30#include <guacamole/socket.h> 31#include <guacamole/unicode.h> 32 33#include <stdbool.h> 34 35/** 36 * Returns the coordinates for the currently-selected range of text within the 37 * given terminal, normalized such that the start coordinate is before the end 38 * coordinate and the end coordinate takes into account character width. If no 39 * text is currently selected, the behavior of this function is undefined. 40 * 41 * @param terminal 42 * The guac_terminal instance whose selected text coordinates should be 43 * retrieved in normalized form. 44 * 45 * @param start_row 46 * A pointer to an int which should receive the row number of the first 47 * character of text selected within the terminal, where the first 48 * (top-most) row in the terminal is row 0. Rows within the scrollback 49 * buffer (above the top-most row of the terminal) will be negative. 50 * 51 * @param start_col 52 * A pointer to an int which should receive the column number of the first 53 * character of text selected within terminal, where 0 is the first 54 * (left-most) column within the row. 55 * 56 * @param end_row 57 * A pointer to an int which should receive the row number of the last 58 * character of text selected within the terminal, where the first 59 * (top-most) row in the terminal is row 0. Rows within the scrollback 60 * buffer (above the top-most row of the terminal) will be negative. 61 * 62 * @param end_col 63 * A pointer to an int which should receive the column number of the first 64 * character of text selected within terminal, taking into account the 65 * width of that character, where 0 is the first (left-most) column within 66 * the row. 67 */ 68static void guac_terminal_select_normalized_range(guac_terminal* terminal, 69 int* start_row, int* start_col, int* end_row, int* end_col) { 70 71 /* Pass through start/end coordinates if they are already in the expected 72 * order, adjusting only for final character width */ 73 if (terminal->selection_start_row < terminal->selection_end_row 74 || (terminal->selection_start_row == terminal->selection_end_row 75 && terminal->selection_start_column < terminal->selection_end_column)) { 76 77 *start_row = terminal->selection_start_row; 78 *start_col = terminal->selection_start_column; 79 *end_row = terminal->selection_end_row; 80 *end_col = terminal->selection_end_column + terminal->selection_end_width - 1; 81 82 } 83 84 /* Coordinates must otherwise be swapped in addition to adjusting for 85 * final character width */ 86 else { 87 *end_row = terminal->selection_start_row; 88 *end_col = terminal->selection_start_column + terminal->selection_start_width - 1; 89 *start_row = terminal->selection_end_row; 90 *start_col = terminal->selection_end_column; 91 } 92 93} 94 95void guac_terminal_select_redraw(guac_terminal* terminal) { 96 97 /* Update the selected region of the display if text is currently 98 * selected */ 99 if (terminal->text_selected) { 100 101 int start_row = terminal->selection_start_row + terminal->scroll_offset; 102 int start_column = terminal->selection_start_column; 103 104 int end_row = terminal->selection_end_row + terminal->scroll_offset; 105 int end_column = terminal->selection_end_column; 106 107 /* Update start/end columns to include character width */ 108 if (start_row > end_row || (start_row == end_row && start_column > end_column)) 109 start_column += terminal->selection_start_width - 1; 110 else 111 end_column += terminal->selection_end_width - 1; 112 113 guac_terminal_display_select(terminal->display, start_row, start_column, end_row, end_column); 114 115 } 116 117 /* Clear the display selection if no text is currently selected */ 118 else 119 guac_terminal_display_clear_select(terminal->display); 120 121} 122 123/** 124 * Locates the beginning of the character at the given row and column, updating 125 * the column to the starting column of that character. The width, if available, 126 * is returned. If the character has no defined width, 1 is returned. 127 * 128 * @param terminal 129 * The guac_terminal in which the character should be located. 130 * 131 * @param row 132 * The row number of the desired character, where the first (top-most) row 133 * in the terminal is row 0. Rows within the scrollback buffer (above the 134 * top-most row of the terminal) will be negative. 135 * 136 * @param column 137 * A pointer to an int containing the column number of the desired 138 * character, where 0 is the first (left-most) column within the row. If 139 * the character is a multi-column character, the value of this int will be 140 * adjusted as necessary such that it contains the column number of the 141 * first column containing the character. 142 * 143 * @return 144 * The width of the specified character, in columns, or 1 if the character 145 * has no defined width. 146 */ 147static int guac_terminal_find_char(guac_terminal* terminal, 148 int row, int* column) { 149 150 int start_column = *column; 151 152 guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(terminal->buffer, row, 0); 153 if (start_column < buffer_row->length) { 154 155 /* Find beginning of character */ 156 guac_terminal_char* start_char = &(buffer_row->characters[start_column]); 157 while (start_column > 0 && start_char->value == GUAC_CHAR_CONTINUATION) { 158 start_char--; 159 start_column--; 160 } 161 162 /* Use width, if available */ 163 if (start_char->value != GUAC_CHAR_CONTINUATION) { 164 *column = start_column; 165 return start_char->width; 166 } 167 168 } 169 170 /* Default to one column wide */ 171 return 1; 172 173} 174 175void guac_terminal_select_start(guac_terminal* terminal, int row, int column) { 176 177 int width = guac_terminal_find_char(terminal, row, &column); 178 179 terminal->selection_start_row = 180 terminal->selection_end_row = row; 181 182 terminal->selection_start_column = 183 terminal->selection_end_column = column; 184 185 terminal->selection_start_width = 186 terminal->selection_end_width = width; 187 188 terminal->text_selected = false; 189 terminal->selection_committed = false; 190 guac_terminal_notify(terminal); 191 192} 193 194void guac_terminal_select_update(guac_terminal* terminal, int row, int column) { 195 196 /* Only update if selection has changed */ 197 if (row != terminal->selection_end_row 198 || column < terminal->selection_end_column 199 || column >= terminal->selection_end_column + terminal->selection_end_width) { 200 201 int width = guac_terminal_find_char(terminal, row, &column); 202 203 terminal->selection_end_row = row; 204 terminal->selection_end_column = column; 205 terminal->selection_end_width = width; 206 terminal->text_selected = true; 207 208 guac_terminal_notify(terminal); 209 210 } 211 212} 213 214void guac_terminal_select_resume(guac_terminal* terminal, int row, int column) { 215 216 int selection_start_row; 217 int selection_start_column; 218 int selection_end_row; 219 int selection_end_column; 220 221 /* No need to test coordinates if no text is selected at all */ 222 if (!terminal->text_selected) 223 return; 224 225 /* Use normalized coordinates for sake of simple comparison */ 226 guac_terminal_select_normalized_range(terminal, 227 &selection_start_row, &selection_start_column, 228 &selection_end_row, &selection_end_column); 229 230 /* Prefer to expand from start, such that attempting to resume a selection 231 * within the existing selection preserves the top-most portion of the 232 * selection */ 233 if (row > selection_start_row || 234 (row == selection_start_row && column > selection_start_column)) { 235 terminal->selection_start_row = selection_start_row; 236 terminal->selection_start_column = selection_start_column; 237 } 238 239 /* Expand from bottom-most portion of selection if doing otherwise would 240 * reduce the size of the selection */ 241 else { 242 terminal->selection_start_row = selection_end_row; 243 terminal->selection_start_column = selection_end_column; 244 } 245 246 /* Selection is again in-progress */ 247 terminal->selection_committed = false; 248 249 /* Update selection to contain given character */ 250 guac_terminal_select_update(terminal, row, column); 251 252} 253 254/** 255 * Appends the text within the given subsection of a terminal row to the 256 * clipboard. The provided coordinates are considered inclusiveley (the 257 * characters at the start and end column are included in the copied 258 * text). Any out-of-bounds coordinates will be automatically clipped within 259 * the bounds of the given row. 260 * 261 * @param terminal 262 * The guac_terminal instance associated with the buffer containing the 263 * text being copied and the clipboard receiving the copied text. 264 * 265 * @param row 266 * The row number of the text within the terminal to be copied into the 267 * clipboard, where the first (top-most) row in the terminal is row 0. Rows 268 * within the scrollback buffer (above the top-most row of the terminal) 269 * will be negative. 270 * 271 * @param start 272 * The first column of the text to be copied from the given row into the 273 * clipboard associated with the given terminal, where 0 is the first 274 * (left-most) column within the row. 275 * 276 * @param end 277 * The last column of the text to be copied from the given row into the 278 * clipboard associated with the given terminal, where 0 is the first 279 * (left-most) column within the row, or a negative value to denote that 280 * the last column in the row should be used. 281 */ 282static void guac_terminal_clipboard_append_row(guac_terminal* terminal, 283 int row, int start, int end) { 284 285 char buffer[1024]; 286 int i = start; 287 288 guac_terminal_buffer_row* buffer_row = 289 guac_terminal_buffer_get_row(terminal->buffer, row, 0); 290 291 /* If selection is entirely outside the bounds of the row, then there is 292 * nothing to append */ 293 if (start < 0 || start > buffer_row->length - 1) 294 return; 295 296 /* Clip given range to actual bounds of row */ 297 if (end < 0 || end > buffer_row->length - 1) 298 end = buffer_row->length - 1; 299 300 /* Repeatedly convert chunks of terminal buffer rows until entire specified 301 * region has been appended to clipboard */ 302 while (i <= end) { 303 304 int remaining = sizeof(buffer); 305 char* current = buffer; 306 307 /* Convert as many codepoints within the given range as possible */ 308 for (i = start; i <= end; i++) { 309 310 int codepoint = buffer_row->characters[i].value; 311 312 /* Ignore null (blank) characters */ 313 if (codepoint == 0 || codepoint == GUAC_CHAR_CONTINUATION) 314 continue; 315 316 /* Encode current codepoint as UTF-8 */ 317 int bytes = guac_utf8_write(codepoint, current, remaining); 318 if (bytes == 0) 319 break; 320 321 current += bytes; 322 remaining -= bytes; 323 324 } 325 326 /* Append converted buffer to clipboard */ 327 guac_common_clipboard_append(terminal->clipboard, buffer, current - buffer); 328 329 } 330 331} 332 333void guac_terminal_select_end(guac_terminal* terminal) { 334 335 guac_client* client = terminal->client; 336 guac_socket* socket = client->socket; 337 338 /* If no text is selected, nothing to do */ 339 if (!terminal->text_selected) 340 return; 341 342 /* Selection is now committed */ 343 terminal->selection_committed = true; 344 345 /* Reset current clipboard contents */ 346 guac_common_clipboard_reset(terminal->clipboard, "text/plain"); 347 348 int start_row, start_col; 349 int end_row, end_col; 350 351 /* Ensure proper ordering of start and end coords */ 352 guac_terminal_select_normalized_range(terminal, 353 &start_row, &start_col, &end_row, &end_col); 354 355 /* If only one row, simply copy */ 356 if (end_row == start_row) 357 guac_terminal_clipboard_append_row(terminal, start_row, start_col, end_col); 358 359 /* Otherwise, copy multiple rows */ 360 else { 361 362 /* Store first row */ 363 guac_terminal_clipboard_append_row(terminal, start_row, start_col, -1); 364 365 /* Store all middle rows */ 366 for (int row = start_row + 1; row < end_row; row++) { 367 guac_common_clipboard_append(terminal->clipboard, "\n", 1); 368 guac_terminal_clipboard_append_row(terminal, row, 0, -1); 369 } 370 371 /* Store last row */ 372 guac_common_clipboard_append(terminal->clipboard, "\n", 1); 373 guac_terminal_clipboard_append_row(terminal, end_row, 0, end_col); 374 375 } 376 377 /* Send data */ 378 if (!terminal->disable_copy) { 379 guac_common_clipboard_send(terminal->clipboard, client); 380 guac_socket_flush(socket); 381 } 382 383 guac_terminal_notify(terminal); 384 385} 386 387bool guac_terminal_select_contains(guac_terminal* terminal, 388 int start_row, int start_column, int end_row, int end_column) { 389 390 int selection_start_row; 391 int selection_start_column; 392 int selection_end_row; 393 int selection_end_column; 394 395 /* No need to test coordinates if no text is selected at all */ 396 if (!terminal->text_selected) 397 return false; 398 399 /* Use normalized coordinates for sake of simple comparison */ 400 guac_terminal_select_normalized_range(terminal, 401 &selection_start_row, &selection_start_column, 402 &selection_end_row, &selection_end_column); 403 404 /* If test range starts after highlight ends, does not intersect */ 405 if (start_row > selection_end_row) 406 return false; 407 408 if (start_row == selection_end_row && start_column > selection_end_column) 409 return false; 410 411 /* If test range ends before highlight starts, does not intersect */ 412 if (end_row < selection_start_row) 413 return false; 414 415 if (end_row == selection_start_row && end_column < selection_start_column) 416 return false; 417 418 /* Otherwise, does intersect */ 419 return true; 420 421} 422 423void guac_terminal_select_touch(guac_terminal* terminal, 424 int start_row, int start_column, int end_row, int end_column) { 425 426 /* Only clear selection if selection is committed */ 427 if (!terminal->selection_committed) 428 return; 429 430 /* Clear selection if it contains any characters within the given region */ 431 if (guac_terminal_select_contains(terminal, start_row, start_column, 432 end_row, end_column)) { 433 434 /* Text is no longer selected */ 435 terminal->text_selected = false; 436 terminal->selection_committed = false; 437 guac_terminal_notify(terminal); 438 439 } 440 441} 442