cscg24-guacamole

CSCG 2024 Challenge 'Guacamole Mashup'
git clone https://git.sinitax.com/sinitax/cscg24-guacamole
Log | Files | Refs | sfeed.txt

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