src/terminal/buffer.c (259 lines of code) (raw):
/*
* 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 "terminal/buffer.h"
#include "terminal/common.h"
#include "terminal/terminal.h"
#include <guacamole/assert.h>
#include <guacamole/mem.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
/**
* The minimum number of columns to allocate for a buffer row, regardless of
* the terminal size. We set a minimum size here to reduce the memory
* reallocation overhead for small rows.
*/
#define GUAC_TERMINAL_BUFFER_ROW_MIN_SIZE 256
/**
* A single variable-length row of terminal data.
*/
typedef struct guac_terminal_buffer_row {
/**
* Array of guac_terminal_char representing the contents of the row.
*/
guac_terminal_char* characters;
/**
* The length of this row in characters. This is the number of initialized
* characters in the buffer, usually equal to the number of characters
* in the screen width at the time this row was created.
*/
unsigned int length;
/**
* The number of elements in the characters array. After the length
* equals this value, the array must be resized.
*/
unsigned int available;
/**
* True if the current row has been wrapped to avoid going off the screen.
* False otherwise.
*/
bool wrapped_row;
} guac_terminal_buffer_row;
struct guac_terminal_buffer {
/**
* The character to assign to newly-allocated cells.
*/
guac_terminal_char default_character;
/**
* Array of buffer rows. This array functions as a ring buffer.
* When a new row needs to be appended, the top reference is moved down
* and the old top row is replaced.
*/
guac_terminal_buffer_row* rows;
/**
* The index of the first row in the buffer (the row which represents row 0
* with respect to the terminal display). This is also the index of the row
* to replace when insufficient space remains in the buffer to add a new
* row.
*/
unsigned int top;
/**
* The number of rows currently stored in the buffer.
*/
unsigned int length;
/**
* The number of rows in the buffer. This is the total capacity
* of the buffer.
*/
unsigned int available;
};
guac_terminal_buffer* guac_terminal_buffer_alloc(int rows,
const guac_terminal_char* default_character) {
/* Allocate scrollback */
guac_terminal_buffer* buffer =
guac_mem_alloc(sizeof(guac_terminal_buffer));
int i;
guac_terminal_buffer_row* row;
/* Init scrollback data */
buffer->default_character = *default_character;
buffer->available = rows;
buffer->top = 0;
buffer->length = 0;
buffer->rows = guac_mem_alloc(sizeof(guac_terminal_buffer_row), buffer->available);
/* Init scrollback rows */
row = buffer->rows;
for (i=0; i<rows; i++) {
/* Allocate row */
row->available = GUAC_TERMINAL_BUFFER_ROW_MIN_SIZE;
row->length = 0;
row->wrapped_row = false;
row->characters = guac_mem_alloc(sizeof(guac_terminal_char), row->available);
/* Next row */
row++;
}
return buffer;
}
void guac_terminal_buffer_free(guac_terminal_buffer* buffer) {
int i;
guac_terminal_buffer_row* row = buffer->rows;
/* Free all rows */
for (i=0; i<buffer->available; i++) {
guac_mem_free(row->characters);
row++;
}
/* Free actual buffer */
guac_mem_free(buffer->rows);
guac_mem_free(buffer);
}
void guac_terminal_buffer_reset(guac_terminal_buffer* buffer) {
buffer->top = 0;
buffer->length = 0;
}
/**
* Returns the row at the given location. The row returned is guaranteed to be at least the given
* width.
*
* @param buffer
* The buffer to retrieve a row from.
*
* @param row
* The index of the row to retrieve, where zero is the top-most row.
* Negative indices represent rows in the scrollback buffer, above the
* top-most row.
*
* @return
* The buffer row at the given location, or NULL if there is no such row.
*/
static guac_terminal_buffer_row* guac_terminal_buffer_get_row(guac_terminal_buffer* buffer, int row) {
if (abs(row) >= buffer->available)
return NULL;
/* Normalize row index into a scrollback buffer index */
unsigned int index = (buffer->top + row) % buffer->available;
return &(buffer->rows[index]);
}
/**
* Rounds the given value up to the nearest possible row length. To avoid
* unnecessary, repeated resizing of rows, each row length is rounded up to the
* nearest power of two.
*
* @param value
* The value to round.
*
* @return
* The power of two that is closest to the given value without exceeding
* that value.
*/
static unsigned int guac_terminal_buffer_row_length(int value) {
GUAC_ASSERT(value >= 0);
GUAC_ASSERT(value <= GUAC_TERMINAL_MAX_COLUMNS);
unsigned int rounded = GUAC_TERMINAL_BUFFER_ROW_MIN_SIZE;
while (rounded < value)
rounded <<= 1;
return rounded;
}
/**
* Expands the amount of space allocated for the given row such that it
* may contain at least the given number of characters, if possible. If the row
* cannot be expanded due to buffer size limitations, it will be expanded to
* the greatest size allowed without exceeding those limits.
*
* @param row
* The row to expand.
*
* @param length
* The number of characters that the row must be able to store.
*
* @param default_character
* The character that should fill any newly-allocated character cells.
*/
static void guac_terminal_buffer_row_expand(guac_terminal_buffer_row* row, int length,
const guac_terminal_char* default_character) {
/* Bail out if no resize/init is necessary */
if (length <= row->length)
return;
/* Limit maximum possible row size to the limits of the terminal display */
if (length > GUAC_TERMINAL_MAX_COLUMNS)
length = GUAC_TERMINAL_MAX_COLUMNS;
/* Expand allocated memory if there is otherwise insufficient space to fit
* the provided length */
if (length > row->available) {
row->available = guac_terminal_buffer_row_length(length);
row->characters = guac_mem_realloc_or_die(row->characters,
sizeof(guac_terminal_char), row->available);
}
/* Initialize new part of row */
for (int i = row->length; i < row->available; i++)
row->characters[i] = *default_character;
row->length = length;
}
/**
* Enforces a character break at the given edge, ensuring that the left side
* of the edge is the final column of a character, and the right side of the
* edge is the initial column of a DIFFERENT character.
*
* @param buffer
* The buffer containing the character.
*
* @param row
* The row index of the row containing the character.
*
* @param edge
* The relative edge number where a break is required. For a character in
* column N, that character's left edge is N and the right edge is N+1.
*/
static void guac_terminal_buffer_force_break(guac_terminal_buffer* buffer, int row, int edge) {
guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row);
if (buffer_row == NULL)
return;
/* Ensure character to left of edge is unbroken */
if (edge > 0) {
int end_column = edge - 1;
int start_column = end_column;
guac_terminal_char* start_char = &(buffer_row->characters[start_column]);
/* Determine start column */
while (start_column > 0 && start_char->value == GUAC_CHAR_CONTINUATION) {
start_char--;
start_column--;
}
/* Advance to start of broken character if necessary */
if (start_char->value != GUAC_CHAR_CONTINUATION && start_char->width < end_column - start_column + 1) {
start_column += start_char->width;
start_char += start_char->width;
}
/* Clear character if broken */
if (start_char->value == GUAC_CHAR_CONTINUATION || start_char->width != end_column - start_column + 1) {
guac_terminal_char cleared_char;
cleared_char.value = ' ';
cleared_char.attributes = start_char->attributes;
cleared_char.width = 1;
guac_terminal_buffer_set_columns(buffer, row, start_column, end_column, &cleared_char);
}
}
/* Ensure character to right of edge is unbroken */
if (edge >= 0 && edge < buffer_row->length) {
int start_column = edge;
int end_column = start_column;
guac_terminal_char* start_char = &(buffer_row->characters[start_column]);
guac_terminal_char* end_char = &(buffer_row->characters[end_column]);
/* Determine end column */
while (end_column+1 < buffer_row->length && (end_char+1)->value == GUAC_CHAR_CONTINUATION) {
end_char++;
end_column++;
}
/* Advance to start of broken character if necessary */
if (start_char->value != GUAC_CHAR_CONTINUATION && start_char->width < end_column - start_column + 1) {
start_column += start_char->width;
start_char += start_char->width;
}
/* Clear character if broken */
if (start_char->value == GUAC_CHAR_CONTINUATION || start_char->width != end_column - start_column + 1) {
guac_terminal_char cleared_char;
cleared_char.value = ' ';
cleared_char.attributes = start_char->attributes;
cleared_char.width = 1;
guac_terminal_buffer_set_columns(buffer, row, start_column, end_column, &cleared_char);
}
}
}
void guac_terminal_buffer_copy_columns(guac_terminal_buffer* buffer, int row,
int start_column, int end_column, int offset) {
/* Get row */
guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row);
if (buffer_row == NULL)
return;
guac_terminal_buffer_row_expand(buffer_row, end_column + offset + 1, &buffer->default_character);
GUAC_ASSERT(buffer_row->length >= end_column + offset + 1);
/* Fit relevant extents of operation within bounds (NOTE: Because this
* operation is relative and represents the destination with an offset,
* there's no need to recalculate the destination region - the offset
* simply remains the same) */
if (offset >= 0) {
start_column = guac_terminal_fit_to_range(start_column, 0, buffer_row->length - offset - 1);
end_column = guac_terminal_fit_to_range(end_column, start_column, buffer_row->length - offset - 1);
}
else {
start_column = guac_terminal_fit_to_range(start_column, -offset, buffer_row->length - 1);
end_column = guac_terminal_fit_to_range(end_column, start_column, buffer_row->length - 1);
}
/* Determine source and destination locations */
guac_terminal_char* src = &(buffer_row->characters[start_column]);
guac_terminal_char* dst = &(buffer_row->characters[start_column + offset]);
/* Copy data */
memmove(dst, src, sizeof(guac_terminal_char) * (end_column - start_column + 1));
/* Force breaks around destination region */
guac_terminal_buffer_force_break(buffer, row, start_column + offset);
guac_terminal_buffer_force_break(buffer, row, end_column + offset + 1);
}
void guac_terminal_buffer_copy_rows(guac_terminal_buffer* buffer,
int start_row, int end_row, int offset) {
int i, current_row;
int step;
/* If shifting down, copy in reverse */
if (offset > 0) {
current_row = end_row;
step = -1;
}
/* Otherwise, copy forwards */
else {
current_row = start_row;
step = 1;
}
/* Copy each current_row individually */
for (i = start_row; i <= end_row; i++) {
/* Get source and destination rows */
guac_terminal_buffer_row* src_row = guac_terminal_buffer_get_row(buffer, current_row);
guac_terminal_buffer_row* dst_row = guac_terminal_buffer_get_row(buffer, current_row + offset);
if (src_row == NULL || dst_row == NULL)
continue;
guac_terminal_buffer_row_expand(dst_row, src_row->length, &buffer->default_character);
GUAC_ASSERT(dst_row->length >= src_row->length);
/* Copy data */
memcpy(dst_row->characters, src_row->characters, guac_mem_ckd_mul_or_die(sizeof(guac_terminal_char), src_row->length));
dst_row->length = src_row->length;
dst_row->wrapped_row = src_row->wrapped_row;
/* Reset src wrapped_row */
src_row->wrapped_row = false;
/* Next current_row */
current_row += step;
}
}
void guac_terminal_buffer_scroll_up(guac_terminal_buffer* buffer, int amount) {
if (amount <= 0)
return;
buffer->top = (buffer->top + amount) % buffer->available;
buffer->length += amount;
if (buffer->length > buffer->available)
buffer->length = buffer->available;
}
void guac_terminal_buffer_scroll_down(guac_terminal_buffer* buffer, int amount) {
if (amount <= 0)
return;
buffer->top = (buffer->top - amount) % buffer->available;
}
unsigned int guac_terminal_buffer_get_columns(guac_terminal_buffer* buffer,
guac_terminal_char** characters, bool* is_wrapped, int row) {
guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row);
if (buffer_row == NULL)
return 0;
if (characters != NULL)
*characters = buffer_row->characters;
if (is_wrapped != NULL)
*is_wrapped = buffer_row->wrapped_row;
return buffer_row->length;
}
void guac_terminal_buffer_set_columns(guac_terminal_buffer* buffer, int row,
int start_column, int end_column, guac_terminal_char* character) {
/* Do nothing if there's nothing to do (glyph is empty) or if nothing
* sanely can be done (row is impossibly large or glyph has an invalid
* width) */
if (character->width <= 0 || row >= GUAC_TERMINAL_MAX_ROWS || row <= -GUAC_TERMINAL_MAX_ROWS)
return;
/* Do nothing if there is no such row within the buffer (the given row index
* does not refer to an actual row, even considering scrollback) */
guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row);
if (buffer_row == NULL)
return;
/* Build continuation char (for multicolumn characters) */
guac_terminal_char continuation_char = {
.value = GUAC_CHAR_CONTINUATION,
.attributes = character->attributes,
.width = 0 /* Not applicable for GUAC_CHAR_CONTINUATION */
};
start_column = guac_terminal_fit_to_range(start_column, 0, GUAC_TERMINAL_MAX_COLUMNS - 1);
end_column = guac_terminal_fit_to_range(end_column, 0, GUAC_TERMINAL_MAX_COLUMNS - 1);
guac_terminal_buffer_row_expand(buffer_row, end_column + 1, &buffer->default_character);
GUAC_ASSERT(buffer_row->length >= end_column + 1);
int remaining_continuation_chars = 0;
for (int i = start_column; i <= end_column; i++) {
/* Store any required continuation characters */
if (remaining_continuation_chars > 0) {
buffer_row->characters[i] = continuation_char;
remaining_continuation_chars--;
}
else {
buffer_row->characters[i] = *character;
remaining_continuation_chars = character->width - 1;
}
}
/* Update length depending on row written */
if (character->value != 0 && row >= buffer->length)
buffer->length = row + 1;
/* Force breaks around destination region */
guac_terminal_buffer_force_break(buffer, row, start_column);
guac_terminal_buffer_force_break(buffer, row, end_column + 1);
}
void guac_terminal_buffer_set_cursor(guac_terminal_buffer* buffer, int row,
int column, bool is_cursor) {
/* Do if nothing sanely can be done (row is impossibly large) */
if (row >= GUAC_TERMINAL_MAX_ROWS || row <= -GUAC_TERMINAL_MAX_ROWS)
return;
/* Do nothing if there is no such row within the buffer (the given row index
* does not refer to an actual row, even considering scrollback) */
guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row);
if (buffer_row == NULL)
return;
column = guac_terminal_fit_to_range(column, 0, GUAC_TERMINAL_MAX_COLUMNS - 1);
guac_terminal_buffer_row_expand(buffer_row, column + 1, &buffer->default_character);
GUAC_ASSERT(buffer_row->length >= column + 1);
buffer_row->characters[column].attributes.cursor = is_cursor;
}
unsigned int guac_terminal_buffer_effective_length(guac_terminal_buffer* buffer, int scrollback) {
/* If the buffer contains more rows than requested, pretend it only
* contains the requested number of rows */
unsigned int effective_length = buffer->length;
if (effective_length > scrollback)
effective_length = scrollback;
return effective_length;
}
void guac_terminal_buffer_set_wrapped(guac_terminal_buffer* buffer, int row, bool wrapped) {
guac_terminal_buffer_row* buffer_row = guac_terminal_buffer_get_row(buffer, row);
if (buffer_row == NULL)
return;
buffer_row->wrapped_row = wrapped;
}